本文详细介绍了在Solana链上程序中如何读取不属于自己的账户数据,通过创建data_holder
和data_reader
两个程序,展示了如何初始化并读取PDA中的数据,并探讨了Anchor框架下的数据反序列化机制及其限制。
在 Solidity 中,读取另一个合约的存储需要调用 view
函数或者存储变量是公共的。在 Solana 中,离线客户端可以直接读取存储账户。这个教程展示了一个链上 Solana 程序如何读取它不拥有的账户中的数据。
我们将设置两个程序: data_holder
和 data_reader
。 data_holder
将初始化并拥有一个 PDA,其数据将被 data_reader
读取。
data_holder
程序: Shell 1以下代码是一个基本的 Solana 程序,它初始化账户 Storage
,其中包含 u64
字段 x
,并在初始化时将值 9 存储在其中:
Typescript 代码:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { DataHolder } from "../target/types/data_holder";
describe("data-holder", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.DataHolder as Program<DataHolder>;
it("Is initialized!", async () => {
const seeds = [];
const [storage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(
seeds,
program.programId
);
await program.methods
.initialize()
.accounts({ storage: storage })
.rpc();
let storageStruct = await program.account.storage.fetch(
storage
);
console.log("The value of x is: ",storageStruct.x.toString());
console.log("Storage account address: ", storage.toBase58());
});
});
测试将打印出 PDA 的地址,我们稍后会提到这个地址:
为了让 data_reader
读取另一个账户,必须通过 Context
结构将该账户的公共密钥作为交易的一部分传递。这与传递任何其他类型的账户没有区别。
账户中的数据以序列化字节的形式存储。 为了反序列化账户,data_reader
程序需要读取它的 Rust 结构定义。我们需要以下账户定义,并且它与 data_holder
中的 Storage
结构完全相同:
#[account]
pub struct Storage {
x: u64,
}
这个结构与 data_reader
中的结构完全相同——连名称也必须相同(稍后我们会介绍为什么)。读取账户的代码在以下两行中:
let mut data_slice: &[u8] = &data_account.data.borrow();
let data_struct: Storage =
AccountDeserialize::try_deserialize(
&mut data_slice,
)?;
data_slice
是账户中数据的原始字节。如果你运行 solana account
<pda address>
(使用我们在部署 data_holder
时生成的 PDA 地址),你可以在那里看到数据,包括我们储存在红框中的数字 9:
黄框中前 8 个字节是账户鉴别符,稍后我们将对此进行描述。
反序列化发生在此步骤:
let data_struct: Storage =
AccountDeserialize::try_deserialize(
&mut data_slice,
)?;
在这里传递类型 Storage
(我们上面定义的结构),告诉 Solana 如何(尝试)反序列化数据。
现在让我们在新文件夹中创建一个单独的 anchor 项目 anchor new data_reader
。
完整的 Rust 代码如下:
use anchor_lang::prelude::*;
declare_id!("HjJ1Rqsth5uxA6HKNGy8VVRvwK4W7aFgmQsss7UxePBw");
#[program]
pub mod data_reader {
use super::*;
pub fn read_other_data(
ctx: Context<ReadOtherData>,
) -> Result<()> {
let data_account = &ctx.accounts.other_data;
if data_account.data_is_empty() {
return err!(MyError::NoData);
}
let mut data_slice: &[u8] = &data_account.data.borrow();
let data_struct: Storage =
AccountDeserialize::try_deserialize(
&mut data_slice,
)?;
msg!("The value of x is: {}", data_struct.x);
Ok(())
}
}
#[error_code]
pub enum MyError {
#[msg("No data")]
NoData,
}
#[derive(Accounts)]
pub struct ReadOtherData<'info> {
/// CHECK: We do not own this account so
// we must be very cautious with how we
// use the data
other_data: UncheckedAccount<'info>,
}
#[account]
pub struct Storage {
x: u64,
}
以下是要运行的测试代码。确保在下面的代码中更改 PDA 的地址:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { DataReader } from "../target/types/data_reader";
describe("data-reader", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace
.DataReader as Program<DataReader>;
it("Is initialized!", async () => {
// CHANGE THIS TO THE ADDRESS OF THE PDA OF
// DATA ACCOUNT HOLDER
const otherStorageAddress ="HRGqGCLXxLryZav2SeKJKqBWYs8Ne7ppJxf3MLM3Y71E";
const pub_key_other_storage = new anchor.web3.PublicKey(
otherStorageAddress
);
const tx = await program.methods
.readOtherData()
.accounts({ otherData: pub_key_other_storage })
.rpc();
});
});
要测试读取另一个账户的数据:
solana-test-validator
启动 data_holder
测试。Storage
账户打印的公共密钥。data_reader
测试的 otherStorageAddress
中。data_reader
的测试以读取数据。以下内容应在 Solana 日志中可见:
如果你将 data_reader
中的 Storage
结构改为其他名称,如 Storage2
并尝试读取该账户,则会发生以下错误:
Anchor 计算的账户鉴别符是结构名称的 sha256 的前八个字节。账户鉴别符不依赖于结构中的变量。
当 Anchor 读取账户时,它检查前八个字节(账户鉴别符)以查看它是否与它在本地用于反序列化数据的结构定义的账户鉴别符匹配。如果它们不匹配,Anchor 将不会反序列化数据。
检查账户鉴别符是防止客户端错误地传入错误账户或数据格式与 Anchor 预期不符的账户数据的一种保障。
Anchor 仅 检查账户鉴别符是否匹配——它不会验证被读取的账户内部的字段。
让我们将 data_reader
中的 Storage
结构中的 x
字段改为 y
,保持 data_holder
中的 Storage
结构不变:
// data_reader
#[account]
pub struct Storage {
y: u64,
}
我们还需要将日志行更改如下:
msg!("The value of y is: {}", data_struct.y);
当我们重新运行测试时,它成功读取了数据:
Program log: Instruction: ReadOtherData
Program log: The value of y is: 9
现在让我们将 y
在 data_reader
中的 Storage
结构的数据类型更改为 u32
,尽管原始结构是 u64
。
// data_reader
#[account]
pub struct Storage {
y: u32,
}
当我们运行测试时,Anchor 仍然成功解析账户数据。
Program log: Instruction: ReadOtherData
Program log: The value of y using u32 is: 9
之所以“成功”,是因为数据的布局如下:
7 里的 9
位于前几个字节中——u32
将在前 4 个字节中查找数据,因此它能够“看到”9
。
当然,如果我们要存储 u32
无法容纳的值,例如 $2^{32}$,那么我们的读取程序将打印错误的数字。
练习 : 重置验证器并重新部署 data_holder
,值设置为 $2^{32}$。在 Rust 中幂运算的方式是 let result = u64::pow(base, exponent)
。例如,let result = u64::pow(2, 32);
查看 data_reader
记录了什么值。
存储账户的大小为 16 字节。它存储 8 字节给账户鉴别符,以及 8 字节给 u64
变量。如果我们尝试读取比实际大更多的数据,例如通过定义一个需要超过 16 字节来存储的结构,反序列化将失败:
#[account]
pub struct Storage {
y: u64,
z: u64,
}
上述结构需要 16 字节来存储 y 和 z,但还需要额外的 8 字节来保存账户鉴别符,使得账户大小达到 24 字节。
在从外部账户读取数据时,Anchor 将检查账户鉴别符是否匹配,以及账户中是否有足够的数据可反序列化为用作 try_deserialize
类型的结构:
let data_struct: Storage =
AccountDeserialize::try_deserialize(
&mut data_slice,
)?;
Anchor 不检查变量的名称或长度。
在底层,Anchor 不存储任何如何解释账户中数据的元数据。它只是存储变量的字节,按顺序存储。
Solana 不要求使用账户鉴别符。用原始 Rust 编写的 Solana 程序——没有 Anchor 框架——可能以与 Anchor 的序列化方法(即 AccountDeserialize::try_deserialize
)不直接兼容的方式存储数据。要反序列化非 Anchor 数据,开发者必须提前知道使用的序列化方法——在 Solana 生态系统中并没有强制的通用约定。
Solana 程序默认是可升级的。它们如何在帐户中存储数据的方式可能随时改变,这可能会破坏正在读取它们的程序。
接受来自任意账户的数据是危险的——在读取其数据之前,通常应检查该账户是否由受信任的程序拥有。
原创发布于 2024 年 5 月 7 日
- 原文链接: rareskills.io/post/ancho...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!