使用不同签名者修改账户

文章详细介绍了在Solana区块链上如何使用不同的签名者初始化账户并进行更新操作,通过Rust代码和客户端代码示例,展示了如何实现账户管理和权限控制。

展示 Anchor Signer:使用不同签名者修改账户的英雄图像

在我们迄今为止的 Solana 教程中,我们只初始化并写入一个账户。

在实际操作中,这非常有限制。举个例子,如果用户 Alice 正在将积分转移给 Bob,Alice 必须能够写入由用户 Bob 初始化的账户。

在本教程中,我们将演示如何使用一个钱包初始化一个账户,并使用另一个钱包更新它。

初始化步骤

我们用于初始化账户的 Rust 代码没有改变:

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("61As9Y8pREgvFZzps6rpFai8UkageeHT6kW1dnGRiefb");

#[program]
pub mod other_write {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init,
              payer = signer,
              space=size_of::<MyStorage>() + 8,
              seeds = [],
              bump)]
    pub my_storage: Account<'info, MyStorage>,

    #[account(mut)]
    pub signer: Signer<'info>,

    pub system_program: Program<'info, System>,
}

#[account]
pub struct MyStorage {
    x: u64,
}

使用备用钱包进行初始化事务

然而,客户端代码中有一个重要的变化:– 为了测试,我们创建了一个名为 newKeypair 的新钱包。与 Anchor 默认提供的一个不同。– 我们向新钱包空投 1 SOL,这样它可以支付交易费用。– 请注意注释 // THIS MUST BE EXPLICITLY SPECIFIED。我们正在将该钱包的 publicKey 传递给 Signer 字段。当我们使用内置 Anchor 的默认签名者时,Anchor 在后台为我们传递这个。但是,当我们使用不同的钱包时,我们需要显式提供这个。– 我们通过 .signers([newKeypair]) 配置设置签名者为 newKeypair

在这个代码片段之后我们将解释为何我们看起来是在两次指定签名者:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { OtherWrite } from "../target/types/other_write";

// 此函数向地址空投 SOL
async function airdropSol(publicKey, amount) {
  let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
  await confirmTransaction(airdropTx);
}

async function confirmTransaction(tx) {
  const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
  await anchor.getProvider().connection.confirmTransaction({
    blockhash: latestBlockHash.blockhash,
    lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
    signature: tx,
  });
}

describe("other_write", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.OtherWrite as Program<OtherWrite>;

  it("Is initialized!", async () => {
    const newKeypair = anchor.web3.Keypair.generate();
    await airdropSol(newKeypair.publicKey, 1e9); // 1 SOL

    let seeds = [];
    const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    await program.methods.initialize().accounts({
      myStorage: myStorage,
      signer: newKeypair.publicKey // ** THIS MUST BE EXPLICITLY SPECIFIED **
    }).signers([newKeypair]).rpc();
  });
});

signer 这个键并不一定要叫 signer

练习:在 Rust 代码中,将 payer = signer 改为 payer = fren,并将 pub signer: Signer<'info> 改为 pub fren: Signer<'info>,然后在测试中将 signer: newKeypair.publicKey 改为 fren: newKeypair.publicKey。初始化应该会成功,测试应该通过。

为什么 Anchor 需要指定 Signer 和 publicKey?

乍一看,似乎我们在两次指定签名者是多余的,但让我们仔细看看:

公共密钥在此传递

在红色框中,我们看到 fren 字段被指定为一个 Signer 账户。Signer 类型意味着 Anchor 将查看交易的签名,并确保签名与此处传递的地址匹配。

稍后我们将看到如何使用它来验证签名者是否被授权进行某些交易。

Anchor 一直在幕后执行这一操作,但由于我们传入了一个与 Anchor 默认使用的签名者不同的 Signer,所以我们必须明确指定 Signer 是哪个账户。

错误:在 Solana Anchor 中未知的签名者

unknown signer 错误发生在交易的签名者与传递给 Signer 的 public key 不匹配时。

假设我们修改了测试,去除 .signers([newKeypair]) 规范。Anchor 将使用默认签名者,而默认签名者不会匹配我们的 newKeypair 钱包的 publicKey

去掉 .signers\(\[newKeypair\]\)

我们将得到以下错误:

错误:签名验证失败

类似地,如果我们没有明确传入 publicKey,Anchor 将默默地使用默认签名者:

未传入 publicKey

我们将得到以下 错误:未知的签名者

错误:未知的签名者

有些误导性地,Anchor 并不是因为没有指定签名者而说签名者是未知的。Anchor 能够推断出如果没有指定签名者,则将使用默认签名者。如果我们同时去除 .signers([newKeypair]) 代码和 fren: newKeypair.publicKey 代码,那么 Anchor 将为验证公共密钥进行签名,并且验证签名者的签名是否与公共密钥匹配。

以下代码将导致成功初始化,因为 Signer 的公共密钥和签署交易的账户都是 Anchor 默认签名者。

await program.methods.initialize().accounts({
  myStorage: myStorage
}).rpc();

other_write 初始化

Bob 可以写入 Alice 初始化的账户

下面我们展示一个带有初始化账户和写入功能的 Anchor 程序。

这将让人熟悉我们的 Solana 计数器程序教程,但注意底部标记的 // THIS FIELD MUST BE INCLUDED 注释中的小更改:

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("61As9Y8pREgvFZzps6rpFai8UkageeHT6kW1dnGRiefb");

#[program]
pub mod other_write {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }

    pub fn update_value(ctx: Context<UpdateValue>, new_value: u64) -> Result<()> {
        ctx.accounts.my_storage.x = new_value;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init,
              payer = fren,
              space=size_of::<MyStorage>() + 8,
              seeds = [],
              bump)]
    pub my_storage: Account<'info, MyStorage>,

    #[account(mut)]
    pub fren: Signer<'info>, // 此处传递了一个公共密钥

    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct UpdateValue<'info> {
    #[account(mut, seeds = [], bump)]
    pub my_storage: Account<'info, MyStorage>,

    // THIS FIELD MUST BE INCLUDED
    #[account(mut)]
    pub fren: Signer<'info>,
}

#[account]
pub struct MyStorage {
    x: u64,
}

以下客户端代码将为 Alice 和 Bob 创建钱包,并向他们分别空投 1 SOL。Alice 将初始化 MyStorage 账户,Bob 将写入它:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { OtherWrite } from "../target/types/other_write";

// 此函数向地址空投 SOL
async function airdropSol(publicKey, amount) {
  let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
  await confirmTransaction(airdropTx);
}

async function confirmTransaction(tx) {
  const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
  await anchor.getProvider().connection.confirmTransaction({
    blockhash: latestBlockHash.blockhash,
    lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
    signature: tx,
  });
}

describe("other_write", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.OtherWrite as Program<OtherWrite>;

  it("Is initialized!", async () => {
    const alice = anchor.web3.Keypair.generate();
    const bob = anchor.web3.Keypair.generate();

    const airdrop_alice_tx = await anchor.getProvider().connection.requestAirdrop(alice.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
    await confirmTransaction(airdrop_alice_tx);

    const airdrop_alice_bob = await anchor.getProvider().connection.requestAirdrop(bob.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
    await confirmTransaction(airdrop_alice_bob);

    let seeds = [];
    const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    // ALICE 初始化账户
    await program.methods.initialize().accounts({
      myStorage: myStorage,
      fren: alice.publicKey
    }).signers([alice]).rpc();

    // BOB 写入账户
    await program.methods.updateValue(new anchor.BN(3)).accounts({
      myStorage: myStorage,
      fren: bob.publicKey
    }).signers([bob]).rpc();

    let value = await program.account.myStorage.fetch(myStorage);
    console.log(`存储的值为 ${value.x}`);
  });
});

限制对 Solana 账户的写入

在实际应用中,我们不希望 Bob 随意向任意账户写入数据。让我们创建一个基本示例,用户可以用 10 积分初始化一个账户,并将这些积分转移到另一个账户。(显而易见的问题是黑客可以使用不同钱包创建任意数量的账户,但这超出了我们示例的范围)。

构建一个原型 ERC20 程序

Alice 应该能够修改她的账户和 Bob 的账户。也就是说,她应该能够扣除她的积分并转到 Bob 的积分。她不应该扣除 Bob 的积分——只有 Bob 才能这样做。

根据惯例,我们称一个能够对账户进行特权更改的地址为 Solana 中的“authority”。在账户结构中存储“authority”字段是一种常见模式,以表示只有该账户才能对该账户进行敏感操作(如在我们的示例中扣除积分)。

这在一定程度上类似于 Solidity 中的 onlyOwner 模式,不过它适用于单个账户,而不是整个合约:

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("HFmGQX4wPgPYVMFe4WrBi925NKvGySrEG2LGyRXsXJ4Z");

const STARTING_POINTS: u32 = 10;

#[program]
pub mod points {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        ctx.accounts.player.points = STARTING_POINTS;
        ctx.accounts.player.authority = ctx.accounts.signer.key();
        Ok(())
    }

    pub fn transfer_points(ctx: Context<TransferPoints>,
                           amount: u32) -> Result<()> {
        require!(ctx.accounts.from.authority == ctx.accounts.signer.key(), Errors::SignerIsNotAuthority);
        require!(ctx.accounts.from.points >= amount, Errors::InsufficientPoints);

        ctx.accounts.from.points -= amount;
        ctx.accounts.to.points += amount;
        Ok(())
    }
}

#[error_code]
pub enum Errors {
    #[msg("SignerIsNotAuthority")]
    SignerIsNotAuthority,
    #[msg("InsufficientPoints")]
    InsufficientPoints
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init,
              payer = signer,
              space = size_of::<Player>() + 8,
              seeds = [&(signer.as_ref().key().to_bytes())],
              bump)]
    player: Account<'info, Player>,
    #[account(mut)]
    signer: Signer<'info>,
    system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct TransferPoints<'info> {
    #[account(mut)]
    from: Account<'info, Player>,
    #[account(mut)]
    to: Account<'info, Player>,
    #[account(mut)]
    signer: Signer<'info>,
}

#[account]
pub struct Player {
    points: u32,
    authority: Pubkey
}

请注意,我们使用签名者的地址(&(signer.as_ref().key().to_bytes()))来推导存储其积分的账户的地址。这类似于 Solidity 中的 mapping in Solana,其中 Solana “msg.sender / tx.origin” 是键。

initialize 函数中,程序将初始积分设置为 10,并将 authority 设置为 signer。用户无法控制这些初始值。

transfer_points 函数使用 Solana Anchor require 宏和错误代码宏 来确保:1)交易的签名者是正在扣减余额的账户的管理员;2)该账户有足够的积分余额进行转移。

测试代码库应该易于理解。Alice 和 Bob 初始化他们的账户,然后 Alice 把 5 分转移给 Bob:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Points } from "../target/types/points";

// 此函数向地址空投 SOL
async function airdropSol(publicKey, amount) {
  let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount);
  await confirmTransaction(airdropTx);
}

async function confirmTransaction(tx) {
  const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
  await anchor.getProvider().connection.confirmTransaction({
    blockhash: latestBlockHash.blockhash,
    lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
    signature: tx,
  });
}

describe("points", () => {
  anchor.setProvider(anchor.AnchorProvider.env());
  const program = anchor.workspace.Points as Program<Points>;

  it("Alice transfers points to Bob", async () => {
    const alice = anchor.web3.Keypair.generate();
    const bob = anchor.web3.Keypair.generate();

    const airdrop_alice_tx = await anchor.getProvider().connection.requestAirdrop(alice.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
    await confirmTransaction(airdrop_alice_tx);

    const airdrop_alice_bob = await anchor.getProvider().connection.requestAirdrop(bob.publicKey, 1 * anchor.web3.LAMPORTS_PER_SOL);
    await confirmTransaction(airdrop_alice_bob);

    let seeds_alice = [alice.publicKey.toBytes()];
    const [playerAlice, _bumpA] = anchor.web3.PublicKey.findProgramAddressSync(seeds_alice, program.programId);

    let seeds_bob = [bob.publicKey.toBytes()];
    const [playerBob, _bumpB] = anchor.web3.PublicKey.findProgramAddressSync(seeds_bob, program.programId);

    // Alice 和 Bob 初始化他们的账户
    await program.methods.initialize().accounts({
      player: playerAlice,
      signer: alice.publicKey,
    }).signers([alice]).rpc();

    await program.methods.initialize().accounts({
      player: playerBob,
      signer: bob.publicKey,
    }).signers([bob]).rpc();

    // Alice 将 5 积分转移给 Bob。请注意,这是一个 u32
    // 所以我们不需要一个 BigNum
    await program.methods.transferPoints(5).accounts({
      from: playerAlice,
      to: playerBob,
      signer: alice.publicKey,
    }).signers([alice]).rpc();

    console.log(`Alice 现在有 ${(await program.account.player.fetch(playerAlice)).points} 积分`);
    console.log(`Bob 现在有 ${(await program.account.player.fetch(playerBob)).points} 积分`);
  });
});

练习:创建一个 keypair mallory,尝试通过在 .signers([mallory]) 中使用 mallory 来窃取 Alice 或 Bob 的积分。你的攻击应该会失败,但你还是应该尝试。

使用 Anchor 约束替代 require! 宏

另一种替代写法 require!(ctx.accounts.from.authority == ctx.accounts.signer.key(), Errors::SignerIsNotAuthority); 的方法是使用 Anchor 约束。Anchor 账户文档提供了一系列可用的约束。

Anchor has_one 约束

has_one 约束假设在 #[derive(Accounts)]#[account] 之间存在“共享键”,并检查这两个键的值是否相等。最好的展示方式是用图片:

从属结构 TransferPoints

在幕后,如果作为交易的一部分传递的 authority 账户(作为 Signer)不等于账户中存储的 authority,则 Anchor 将阻止该交易。

在我们上面的实现中,我们在账户中使用了字符 authority,而在 #[derive(Accounts)] 中用 signer。这种键名不匹配将阻止这个宏的工作,因此,上面的代码将 signer 的键更改为 authorityauthority 不是一个特殊的关键字,只是一种约定。作为练习,你可以将所有 authority 实例改为 fren,代码将会以同样的方式工作。

Anchor constraint 约束

我们还可以将宏 require!(ctx.accounts.from.points >= amount, Errors::InsufficientPoints); 替换为 Anchor 约束。

constraint 宏允许我们对传递给交易的账户和账户中的数据施加任意约束。在我们的情况下,我们希望确保发送者有足够的积分:

#[derive(Accounts)]
#[instruction(amount: u32)] // amount 必须做为指令传递
pub struct TransferPoints<'info> {
    #[account(mut,
              has_one = authority,
              constraint = from.points >= amount)]
    from: Account<'info, Player>,
    #[account(mut)]
    to: Account<'info, Player>,
    authority: Signer<'info>,
}

#[account]
pub struct Player {
    points: u32,
    authority: Pubkey
}

宏足够聪明,能识别出 from 是基于传递进来的 from 键的账户,并且该账户有 points 字段。来自 transfer_points 函数参数的 amount 必须通过 instruction 宏进行传递,以便 constraint 宏将 amount 与账户中的积分余额进行比较。

向 Anchor 约束添加自定义错误消息

我们可以通过添加自定义错误来提高约束违规时错误消息的可读性,这些自定义错误与我们通过 @ 符号传递给 require! 宏的错误相同:

#[derive(Accounts)]
#[instruction(amount: u32)]
pub struct TransferPoints<'info> {
    #[account(mut,
              has_one = authority @ Errors::SignerIsNotAuthority,
              constraint = from.points >= amount @ Errors::InsufficientPoints)]
    from: Account<'info, Player>,
    #[account(mut)]
    to: Account<'info, Player>,
    authority: Signer<'info>,
}

#[account]
pub struct Player {
    points: u32,
    authority: Pubkey
}

之前在 Rust 代码中定义的 Errors 枚举使用了这些错误,并在 require! 宏中用到了它们。

练习:修改测试以违反 has_oneconstraint 宏,并观察错误消息。

在 RareSkills 上深入了解 Solana

我们的 Solana 教程 涉及如何学习 Solana ,作为以太坊或 EVM 开发者的具体指导。

最初发布于 2024 年 3 月 5 日

  • 原文链接: rareskills.io/post/ancho...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/