到目前为止,在我们的 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";
// this airdrops sol to an address
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 将检查交易的签名,并确保该签名与此处传递的地址相匹配。
稍后我们将看到如何使用它来验证 Signer 是否被授权执行某些交易。
Anchor 一直在幕后执行此操作,但因为我们传入的 Signer 不是 Anchor 默认使用的签名者,所以我们必须显式指明 Signer 是哪个账户。
错误:Solana Anchor 中的 unknown signer
当交易的签名者与传递给 Signer 的公钥不匹配时,就会出现 unknown signer 错误。
假设我们修改测试,移除 .signers([newKeypair]) 规范。Anchor 将转而使用默认签名者,而默认签名者将与我们 newKeypair 钱包的 publicKey 不匹配:
![Removing .signers([newKeypair])](https://static.wixstatic.com/media/935a00_2b7cffddcb2a490aaea4396ee71a47a1~mv2.png/v1/fill/w_1480,h_334,al_c,lg_1,q_90,enc_auto/935a00_2b7cffddcb2a490aaea4396ee71a47a1~mv2.png)
我们会得到以下错误:

类似地,如果我们不显式传入 publicKey,Anchor 也会默认使用默认签名者:

然后我们将收到以下 Error: unknown signer:

有些误导的是,Anchor 并不是说因为没有明确指定签名者所以它是未知的。Anchor 能够推断出,如果没有指定签名者,那么它将使用默认签名者。如果我们同时删除 .signers([newKeypair]) 代码和 fren: newKeypair.publicKey 代码,那么 Anchor 会将默认签名者同时用作要核对的公钥,以及验证是否与公钥匹配的签名者签名。
以下代码将成功初始化,因为 Signer 公钥和签署交易的账户都是 Anchor 默认签名者。
await program.methods.initialize().accounts({
myStorage: myStorage
}).rpc();
});

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>, // A public key is passed here
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";
// this airdrops sol to an address
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", () => {
// Configure the client to use the local cluster.
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 INITIALIZE ACCOUNT
await program.methods.initialize().accounts({
myStorage: myStorage,
fren: alice.publicKey
}).signers([alice]).rpc();
// BOB WRITE TO ACCOUNT
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 stored is ${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()))来派生存储其积分的账户地址。这类似于 Solana 中的 Solidity 映射(mapping),其中 Solana “msg.sender / tx.origin” 是键。
在 initialize 函数中,程序将初始积分设置为 10,并将权限(authority)设置为 signer。用户无法控制这些初始值。
transfer_points 函数使用 Solana Anchor 的 require 宏和错误代码宏来确保:1)交易的 Signer 是余额被扣除的账户的权限;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";
// this airdrops sol to an address
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 and Bob initialize their accounts
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 transfers 5 points to Bob. Note that this is a u32
// so we don't need a BigNum
await program.methods.transferPoints(5).accounts({
from: playerAlice,
to: playerBob,
signer: alice.publicKey,
}).signers([alice]).rpc();
console.log(`Alice has ${(await program.account.player.fetch(playerAlice)).points} points`);
console.log(`Bob has ${(await program.account.player.fetch(playerBob)).points} points`)
});
});
练习:创建一个密钥对 mallory,并尝试让 mallory 在 .signers([mallory]) 中作为签名者使用,从而窃取 Alice 或 Bob 的积分。你的攻击应该会失败,但无论如何你都应该尝试一下。
使用 Anchor Constraints 替换 require! 宏
与编写 require!(ctx.accounts.from.authority == ctx.accounts.signer.key(), Errors::SignerIsNotAuthority); 相对的一种替代方法是使用 Anchor 约束(Constraint)。Anchor 账户文档为我们提供了一个可用的约束列表。
Anchor has_one 约束
has_one 约束假设在 #[derive(Accounts)] 和 #[account] 之间存在“共享键”,并检查这两个键是否具有相同的值。演示这一点的最好方法是通过图片:

在幕后,如果作为交易一部分传递(作为 Signer)的 authority 账户不等于存储在账户中的 authority,Anchor 将阻止该交易。
在我们上面的实现中,我们在账户中使用了键 authority,在 #[derive(Accounts)] 中使用了 signer。这种键名不匹配将阻止此宏工作,因此上面的代码将键 signer 更改为 authority。Authority 不是特殊关键字,仅是一种约定。作为练习,你可以将所有 authority 实例更改为 fren,代码将同样有效。
Anchor constraint 约束
我们也可以用 Anchor 约束替换宏 require!(ctx.accounts.from.points >= amount, Errors::InsufficientPoints);。
constraint 宏允许我们对传递给交易的账户以及账户中的数据施加任意约束。在我们的案例中,我们要确保发送者有足够的积分:
#[derive(Accounts)]
#[instruction(amount: u32)] // amount must be passed as an instruction
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 字段。必须通过 instruction 宏传递来自 transfer_points 函数参数的 amount,以便 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
}
Errors 枚举是在前面的 Rust 代码中定义的,并在前面的 require! 宏中使用了它们。
练习:修改测试以违反 has_one 和 constraint 宏并观察错误消息。
在 RareSkills 上学习更多关于 Solana 的知识
我们的 Solana 教程涵盖了如何作为 Ethereum 或 EVM 开发者学习 Solana。
最初发布于 2024 年 3 月 5 日