Solana Anchor 中的 #[derive(Accounts)] 是一个用于结构体的类似属性的宏,它保存了函数在执行期间将访问的所有账户的引用。
在 Solana 中,必须提前指定交易将访问的所有账户
Solana 速度如此之快的一个原因是它并行执行交易。也就是说,如果 Alice 和 Bob 都想进行交易,Solana 会尝试同时处理他们的交易。然而,如果他们的交易由于访问同一存储而发生冲突,就会出现问题。例如,假设 Alice 和 Bob 都试图向同一个账户写入数据。显然,他们的交易不能并行运行。
为了让 Solana 知道 Alice 和 Bob 的交易不能并行处理,Alice 和 Bob 必须提前指定他们的交易将更新的所有账户。
由于 Alice 和 Bob 都指定了一个(存储)账户,Solana 运行时可以推断出这两笔交易存在冲突。必须选择其中一个(大概率是支付了更高优先费的那个),而另一个最终将失败。
这就是为什么每个函数都有自己独立的 #[derive(Accounts)] 结构体。结构体中的每个字段代表了程序在执行期间打算(但不强制要求)访问的账户。
一些 Ethereum 开发者可能会注意到,这个要求与 EIP 2930 访问列表交易 存在相似之处。
账户的类型向 Anchor 传达了你打算如何与该账户进行交互。
你将最常使用的账户类型:Account、Unchecked Account、System Program 和 Signer
在初始化存储的代码中,我们看到了三种不同“种类”的账户:
AccountSignerProgram
以下是再次展示的代码:

当读取账户余额时,我们看到了第四种类型:
UncheckedAccount
这是我们使用的代码:

我们用绿色方框高亮显示的每个项目,都是通过文件顶部的 anchor_lang::prelude::*; 导入的。
Account、UncheckedAccount、Signer 和 Program 的作用是在继续执行之前对传入的账户执行某种检查,并暴露用于与这些账户进行交互的函数。
我们将在以下几节中进一步解释这四种类型中的每一种。
Account
Account 类型会检查正在加载的账户是否确实属于该程序所有。如果所有者不匹配,它将不会加载。作为一项重要的安全措施,这可以避免意外读取非本程序创建的数据。
在下面的示例中,我们创建了一个密钥对账户并尝试将其传递给 foo。因为该账户并不属于程序所有,所以交易会失败。
Rust:
use anchor_lang::prelude::*;
declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs");
#[program]
pub mod account_types {
use super::*;
pub fn foo(ctx: Context<Foo>) -> Result<()> {
// we don't do anything with the account SomeAccount
Ok(())
}
}
#[derive(Accounts)]
pub struct Foo<'info> {
some_account: Account<'info, SomeAccount>,
}
#[account]
pub struct SomeAccount {}
Typescript:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { AccountTypes } from "../target/types/account_types";
describe("account_types", () => {
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor
.getProvider()
.connection.requestAirdrop(
publicKey,
amount * anchor.web3.LAMPORTS_PER_SOL
);
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,
});
}
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.AccountTypes as Program<AccountTypes>;
it("Wrong owner with Account", async () => {
const newKeypair = anchor.web3.Keypair.generate();
await airdropSol(newKeypair.publicKey, 10);
await program.methods
.foo()
.accounts({someAccount: newKeypair
.publicKey}).rpc();
});
});
以下是执行测试的输出:

如果我们向 Account 添加一个 init 宏,那么它将尝试把所有权从系统程序转移给此程序。然而,上面的代码并没有 init 宏。
可以在文档中找到更多关于 Account 类型的信息:https://docs.rs/anchor-lang/latest/anchor_lang/accounts/account/struct.Account.html
UncheckedAccount 或 AccountInfo
UncheckedAccount 是 AccountInfo 的别名。它不检查所有权,因此必须小心使用,因为它会接受任意账户。
以下是使用 UncheckedAccount 读取它不拥有的账户数据的示例。
use anchor_lang::prelude::*;
declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs");
#[program]
pub mod account_types {
use super::*;
pub fn foo(ctx: Context<Foo>) -> Result<()> {
let data = &ctx.accounts.some_account.try_borrow_data()?;
msg!("{:?}", data);
Ok(())
}
}
#[derive(Accounts)]
pub struct Foo<'info> {
/// CHECK: we are just printing the data
some_account: AccountInfo<'info>,
}
这是我们的 Typescript 代码。请注意,我们直接调用系统程序来创建密钥对账户,以便能够分配 16 字节的数据。
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { AccountTypes } from "../target/types/account_types";
describe("account_types", () => {
const wallet = anchor.workspace.AccountTypes.provider.wallet;
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.AccountTypes as Program<AccountTypes>;
it("Load account with accountInfo", async () => {
// CREATE AN ACCOUNT NOT OWNED BY THE PROGRAM
const newKeypair = anchor.web3.Keypair.generate();
const tx = new anchor.web3.Transaction().add(
anchor.web3.SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: newKeypair.publicKey,
space: 16,
lamports: await anchor
.getProvider()
.connection
.getMinimumBalanceForRentExemption(32),
programId: program.programId,
})
);
await anchor.web3.sendAndConfirmTransaction(
anchor.getProvider().connection,
tx,
[wallet.payer, newKeypair]
);
// READ THE DATA IN THE ACCOUNT
await program.methods
.foo()
.accounts({ someAccount: newKeypair.publicKey })
.rpc();
});
});
程序运行后,我们可以看到它打印出了该账户中的数据,里面包含 16 个零字节:
Transaction executed in slot 14298:
Signature: 64fv6NqYB4tji9UfLpH8PgFDY1QV4vbMovrnnpw3271vStg7J5g1z1bm9YbE8Lobzozkc6y2YzLdgMjGdftCGKqv
Status: Ok
Log Messages:
Program ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs invoke [1]
Program log: Instruction: Foo
Program log: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Program ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs consumed 5334 of 200000 compute units
Program ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs success
当传入任意地址时,我们需要使用这种账户类型,但必须极其小心地处理这些数据,因为黑客可能会在账户中构造恶意数据,然后将其传递给 Solana 程序。
Signer
此类型将检查 Signer 账户是否对交易进行了签名;它会检查签名是否与该账户的公钥匹配。
因为签名者(signer)也是一个账户,你可以读取存储在该账户中的 Signer 余额或数据(如果有的话),尽管其主要目的是为了验证签名。
根据文档 https://docs.rs/anchor-lang/latest/anchor_lang/accounts/signer/struct.Signer.html,Signer 是一个验证账户是否对交易进行了签名的类型。它不做任何其他的所有权或类型检查。如果使用了它,就不应该尝试访问底层的账户数据。
Rust 示例:
use anchor_lang::prelude::*;
declare_id!("ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs");#
[program]
pub mod account_types {
use super::*;
pub fn hello(ctx: Context<Hello>) -> Result<()> {
let lamports = ctx.accounts.signer.lamports();
let address = &ctx.accounts
.signer
.signer_key().unwrap();
msg!(
"hello {:?} you have {} lamports",
address,
lamports
);
Ok(())
}
}
#[derive(Accounts)]
pub struct Hello<'info> {
pub signer: Signer<'info>,
}
Typescript:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { AccountTypes } from "../target/types/account_types";
describe("account_types", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.AccountTypes as Program<AccountTypes>;
it("Wrong owner with Account", async () => {
await program.methods.hello().rpc();
});
});
以下是程序的输出:
Transaction executed in slot 11184:
Signature: 4xipobKHHp7a3N4durXN4YPGUesDAJNg7wsatBemdJAm7U1dXYG3gveLwnuY39iCTEZvaj6nnAViVJwDS8124uJJ
Status: Ok
Log Messages:
Program ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs invoke [1]
Program log: Instruction: Hello
Program log: hello 5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj you have 499999994602666000 lamports
Program ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs consumed 13096 of 200000 compute units
Program ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs success
Program
这应该是不言自明的。它向 Anchor 发出信号,表明该账户是一个可执行账户,即一个程序,你可以对它发起跨程序调用(cross program invocation)。我们一直使用的是系统程序(system program),不过稍后我们将使用我们自己的程序。
了解更多
在我们的 Ethereum 到 Solana 课程中学习 Solana 开发。
原载于 2024 年 4 月 6 日