在链上读取另一个锚点程序账户数据

本文详细介绍了在Solana链上程序中如何读取不属于自己的账户数据,通过创建data_holderdata_reader两个程序,展示了如何初始化并读取PDA中的数据,并探讨了Anchor框架下的数据反序列化机制及其限制。

在 Solidity 中,读取另一个合约的存储需要调用 view 函数或者存储变量是公共的。在 Solana 中,离线客户端可以直接读取存储账户。这个教程展示了一个链上 Solana 程序如何读取它不拥有的账户中的数据。

我们将设置两个程序: data_holderdata_readerdata_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 的地址,我们稍后会提到这个地址:

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:

终端输出 solana \\<pda address\\> 包含数字 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();
  });
});

要测试读取另一个账户的数据:

  1. 在后台运行 solana-test-validator 启动 data_holder 测试。
  2. 复制并粘贴 Storage 账户打印的公共密钥。
  3. 将该公共密钥放入 data_reader 测试的 otherStorageAddress 中。
  4. 在另一个 shell 中运行 Solana 日志。
  5. 运行 data_reader 的测试以读取数据。

以下内容应在 Solana 日志中可见:

程序日志 : x 的值是: 9

如果我们不给结构体相同的名称,会发生什么?

如果你将 data_reader 中的 Storage 结构改为其他名称,如 Storage2 并尝试读取该账户,则会发生以下错误:

错误: 由于账户而引起的 AnchorError: Storage2

Anchor 计算的账户鉴别符是结构名称的 sha256 的前八个字节。账户鉴别符不依赖于结构中的变量

当 Anchor 读取账户时,它检查前八个字节(账户鉴别符)以查看它是否与它在本地用于反序列化数据的结构定义的账户鉴别符匹配。如果它们不匹配,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 不检查数据类型

现在让我们将 ydata_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

之所以“成功”,是因为数据的布局如下:

终端输出 solana \\<pda address\\> 包含数字 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 记录了什么值。

案例 3: 解析的字段数据超出存储

存储账户的大小为 16 字节。它存储 8 字节给账户鉴别符,以及 8 字节给 u64 变量。如果我们尝试读取比实际大更多的数据,例如通过定义一个需要超过 16 字节来存储的结构,反序列化将失败:

#[account]
pub struct Storage {
    y: u64,
    z: u64,
}

上述结构需要 16 字节来存储 y 和 z,但还需要额外的 8 字节来保存账户鉴别符,使得账户大小达到 24 字节。

错误: AnchorError 发生. 错误代码: AccountDidNotDeserialize

解析 Anchor 账户数据总结

在从外部账户读取数据时,Anchor 将检查账户鉴别符是否匹配,以及账户中是否有足够的数据可反序列化为用作 try_deserialize 类型的结构:

let data_struct: Storage =
    AccountDeserialize::try_deserialize(
        &mut data_slice,
    )?;

Anchor 不检查变量的名称或长度。

在底层,Anchor 不存储任何如何解释账户中数据的元数据。它只是存储变量的字节,按顺序存储。

并非所有数据账户都遵循 Anchor 的约定

Solana 不要求使用账户鉴别符。用原始 Rust 编写的 Solana 程序——没有 Anchor 框架——可能以与 Anchor 的序列化方法(即 AccountDeserialize::try_deserialize)不直接兼容的方式存储数据。要反序列化非 Anchor 数据,开发者必须提前知道使用的序列化方法——在 Solana 生态系统中并没有强制的通用约定。

读取任意账户数据时要小心

Solana 程序默认是可升级的。它们如何在帐户中存储数据的方式可能随时改变,这可能会破坏正在读取它们的程序。

接受来自任意账户的数据是危险的——在读取其数据之前,通常应检查该账户是否由受信任的程序拥有。

原创发布于 2024 年 5 月 7 日

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

0 条评论

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