在 Solidity 中,读取另一个合约的存储需要调用 view 函数或确保存储变量是 public 的。在 Solana 中,链下客户端可以直接读取存储账户。本教程将展示链上 Solana 程序如何读取非其所有的账户中的数据。
我们将设置两个程序:data_holder 和 data_reader。data_holder 将初始化并拥有一个包含数据的 PDA,而 data_reader 将读取该数据。
设置存储数据的 data_holder 程序:Shell 1
以下代码是一个基础的 Solana 程序,它在初始化时初始化带有 u64 字段 x 的 Storage 账户,并将值 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 的地址,我们稍后将用到这个地址:

Reader
为了让 data_reader 读取另一个账户,该账户的公钥需要通过 Context 结构体作为交易的一部分传入。这与传入任何其他类型的账户没有区别。
账户中的数据以序列化字节的形式存储。为了反序列化该账户,data_reader 程序需要其正在读取的结构体的 Rust 定义。我们需要为 data_reader 提供以下账户定义,它与 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 个字节是账户鉴别器(account discriminator),我们稍后会进行说明。
反序列化发生在这一步:
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中。 - 在另一个 Shell 中运行 Solana 日志。
- 运行
data_reader的测试以读取数据。
在 Solana 日志中应该可以看到以下内容:

如果我们不给结构体赋予相同的名称会怎样?
如果你将 data_reader 中的 Storage 结构体改为 Storage 以外的名称,例如 Storage2,然后尝试读取该账户,将会出现以下错误:

Anchor 计算出的账户鉴别器是结构体名称的 sha256 哈希值的前 8 个字节。账户鉴别器不依赖于结构体中的变量。
当 Anchor 读取账户时,它会检查前 8 个字节(账户鉴别器),看它是否与其正在本地用于反序列化数据的结构体定义的账户鉴别器相匹配。如果不匹配,Anchor 将不会反序列化该数据。
检查账户鉴别器是一种安全机制,旨在防止客户端意外传入错误的账户,或传入数据格式与 Anchor 预期不符的账户。
如果解析较大的结构体,反序列化不会被回退
Anchor 只检查账户鉴别器是否匹配——它不会验证被读取账户内部的字段。
案例 1: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
案例 2:Anchor 不检查数据类型
现在让我们将 data_reader 的 Storage 中 y 的数据类型改为 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
这之所以能“成功”,是因为数据的布局方式:

这里的 9 存在于最前面的几个字节中——u32 会在前 4 个字节中寻找数据,因此它能够“看到”这个 9。
当然,如果我们在 x 中存储了一个 u32 无法容纳的值,比如 ,那么我们的读取程序将打印出错误的数字。
练习:重置验证者节点并使用值 重新部署 data_holder。在 Rust 中进行幂运算的方法是 let result = u64::pow(base, exponent)。例如,let result = u64::pow(2, 32);。看看 data_reader 会输出什么值。
案例 3:解析的数据量超出现有数据
该存储账户的大小为 16 字节。其中 8 个字节用于账户鉴别器,另外 8 个字节用于 u64 变量。如果我们尝试读取超出现有容量的数据,例如定义了一个包含需要超过 16 字节才能容纳的值的结构体,那么读取时的反序列化将会失败:
#[account]
pub struct Storage {
y: u64,
z: u64,
}
上面的结构体需要 16 字节来存储 y 和 z,但还需要额外的 8 字节来保存账户鉴别器,这使得该账户需要 24 字节的大小。

解析 Anchor 账户数据总结
从外部账户读取数据时,Anchor 会检查账户鉴别器是否匹配,以及账户中是否有足够的数据能够被反序列化为 try_deserialize 的目标结构体类型:
let data_struct: Storage =
AccountDeserialize::try_deserialize(
&mut data_slice,
)?;
Anchor 不会检查变量的名称或它们的长度。
在底层,Anchor 并不存储任何关于如何解释账户数据的元数据。它仅仅是将变量的字节首尾相连进行存储。
并非所有数据账户都遵循 Anchor 的规范
Solana 并不强制要求使用账户鉴别器。使用原生 Rust 编写(不使用 Anchor 框架)的 Solana 程序很可能以一种与 Anchor 的 AccountDeserialize::try_deserialize 实现的序列化方法不直接兼容的方式存储数据。要反序列化非 Anchor 数据,开发者必须提前了解所使用的序列化方法——在 Solana 生态系统中并没有强制执行的通用规范。
从任意账户读取数据时需格外谨慎
Solana 程序默认是可升级的。它们在账户中存储数据的方式可能会随时发生变化,这有可能会破坏从中读取数据的程序。
接受来自任意账户的数据是危险的——在从中读取数据之前,通常应该检查该账户是否由受信任的程序拥有。
最初发布于 2024 年 5 月 7 日