您正在查看: Surou 发布的文章

Solana基础 - 在 Solana 和 Anchor 中初始化账户

文章详细介绍了Solana区块链中数据存储的机制,特别是如何通过账户和程序来管理和初始化存储数据。文章通过对比以太坊的存储方式,深入探讨了Solana的存储模型和使用Rust语言进行账户初始化的具体步骤。

迄今为止,我们的所有教程都没有使用“存储变量”或存储任何永久性内容。

在 Solidity 和 Ethereum 中,另一种更为特殊的设计模式是 SSTORE2 或 SSTORE3,其中数据存储在另一个智能合约的字节码中。

在 Solana 中,这并不是一种特殊的设计模式,而是一种常规做法!

回想一下,除非程序被标记为不可变,否则我们可以随意更新 Solana 程序的字节码(如果我们是原始部署者)。

Solana 使用相同的机制来存储数据。

以太坊中的存储槽实际上是一个巨大的键值存储:

{
    key: [smart_contract_address, storage slot]
    value: 32_byte_slot // (例如: 0x00)
}

Solana 的模型类似:它是一个巨大的键值存储,其中“键”是一个 base 58 编码的地址,而值是一个可以大到 10MB 的数据块(或可选择不存储任何内容)。它可以这样可视化:

{
    // key 是一个 base58 编码的 32 字节序列
    key: ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs
    value: {
        data: 020000006ad1897139ac2bdb67a3c66a...
        // 其他字段省略
    }
}

在 Ethereum 中,一个智能合约的字节码和存储变量是分别存储的,即它们是不同索引的,必须使用不同的 API 加载。

以下图表展示了以太坊如何维护状态。每个帐户都是 Merkle 树中的一个叶子。请注意,“存储变量”被存储在智能合约的帐户(帐户 1)“内部”。Ethereum Storage在 Solana 中,一切都是帐户,可以潜在地保存数据。有时我们将一个帐户称为“程序帐户”,另一个帐户称为“存储帐户”,但唯一的区别在于可执行标志是否设置为真,以及我们打算如何使用帐户的数据字段。

下面,我们可以看到 Solana 存储是一个从 Solana 地址到帐户的巨大键值存储:Solana Accounts

想象一下,如果以太坊没有存储变量,智能合约默认是可变的。要存储数据,你必须创建其他“智能合约”,并将数据保存在它们的字节码中,然后在必要时修改它。这是 Solana 的一种心理模型。

另一种心理模型是 Unix 中的一切都是文件,有些文件是可执行的。可以将 Solana 帐户视为文件。它们保存内容,但它们也具有元数据,指示谁拥有该文件,它是否可执行,等等。

在 Ethereum 中,存储变量直接与智能合约耦合。除非智能合约通过公共变量、delegatecall 或某些设置方法授予读写访问权限,否则存储变量默认只能由单个合约写入或读取(尽管任何人都可以从链下读取存储变量)。在 Solana 中,所有“存储变量”可以被任何程序读取,但只有其所有者程序可以写入它。

存储与程序“绑定”的方式是通过所有者字段。

在下图中,我们看到帐户 B 是由程序帐户 A 所拥有。我们知道 A 是程序帐户,因为“可执行”设置为 true。这表明 B 的数据字段将存储 A 的数据:program storage for solana

Solana 程序必须在使用之前初始化

在 Ethereum 中,我们可以直接写入一个我们之前未使用的存储变量。然而,Solana 程序需要一个显式的初始化事务。也就是说,我们必须先创建帐户,然后才能向其中写入数据。

可以在一个事务中初始化并写入一个 Solana 帐户——然而这会引入安全问题,这将使讨论变得复杂。如果我们现在处理它,暂时只需说 Solana 帐户必须在使用之前进行初始化。

基本存储示例

让我们将以下 Solidity 代码翻译为 Solana:

contract BasicStorage {
    Struct MyStorage {
        uint64 x;
    }

    MyStorage public myStorage;

    function set(uint64 _x) external {
        myStorage.x = _x;
    }
} 

将单个变量包裹在一个结构中可能看起来很奇怪。

但在 Solana 程序中,特别是在 Anchor 中,所有存储,或者说帐户数据,都被视为结构。原因在于帐户数据的灵活性。由于帐户是可以非常大的数据块(最大可达 10MB),我们需要某种“结构”来解读数据,否则它只是一个毫无意义的字节序列。

在后台,Anchor 在我们尝试读取或写入数据时会对帐户数据进行反序列化和序列化为结构。

如上所述,我们需要在使用 Solana 帐户之前对其进行初始化,因此在实现 set() 函数之前,我们需要编写 initialize() 函数。

帐户初始化样板代码

让我们创建一个名为 basic_storage 的新 Anchor 项目。

以下是我们编写的最小代码,以初始化一个仅保存一个数字 xMyStorage 结构。 (请参见底部代码中的结构 MyStorage):

use anchor_lang::prelude::*;
use std::mem::size_of;

declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");

#[program]
pub mod basic_storage {
    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,
}

1) initialize 函数

请注意,initialize() 函数中没有代码——实际上它所做的只是返回 Ok(())

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    Ok(())
}

初始化帐户的函数并不一定必须为空,我们可以有自定义逻辑。但在我们的示例中,它是空的。用于“初始化”的函数不必命名为 initialize,但这个名字是有帮助的。

2) Initialize 结构

Initialize 结构包含初始化帐户所需资源的引用:

  • my_storage: 我们正在初始化的 MyStorage 类型的结构。
  • signer: 负责为结构的存储支付“Gas费”的钱包。 (关于存储的Gas费用将在后面讨论)。
  • system_program: 我们将在本教程稍后讨论。Annotated Initialize struct

'info 关键字是一个 Rust 生命周期。这是一个重要的主题,目前我们最好将其视为样板代码。

我们将专注于 my_storage 上方的宏,因为这就是初始化工作的地方。

3) Initialize 结构中的 my_storage 字段

位于 my_storage 字段上方的属性宏(紫色箭头)是 Anchor 知道此事务旨在初始化此帐户的方式(请记住,属性宏以 # 开头,并通过额外功能增强结构):annotation of struct fields

这里重要的关键字是 init

当我们 init 一个帐户时,必须提供额外的信息:

  • payer(蓝框):谁在为分配存储支付 SOL。签名者被指定为 mut,因为他们的帐户余额将会变化,即会从他们的帐户中扣除一些 SOL。因此,我们将他们的帐户注释为“可变”。
  • space(橙框):这指示帐户将占用多少空间。我们可以使用 std::mem::size_of 工具,并将我们正在尝试存储的结构 MyStorage(绿色框)作为参数。+ 8(粉色框)的含义将在下一点中讨论。
  • seedsbump(红框):一个程序可以拥有多个帐户,它通过“种子”在帐户之间进行“区分”,该种子用于计算“鉴别符”。“鉴别符”占 8 个字节,这就是为什么除了我们结构所占的空间外还需要分配额外的 8 个字节。bump 目前可以视为样板代码。

这可能听起来有点复杂,但不用担心。初始化帐户在很大程度上可以视为样板代码。

4) 什么是系统程序?

system program 是一个内置于 Solana 运行时的程序(有点像 Ethereum 预编译),它从一个帐户向另一个帐户转移 SOL。我们将在后面的教程中重新访问这个概念。现在,我们需要将 SOL 从支付 MyStruct 存储的签名者那里转移,因此 system program 总是初始化事务的一部分。

5) MyStorage 结构

回想一下 Solana 帐户内的数据字段:data highlighted in solana account在幕后,这是一串字节序列。上面示例中的结构:

#[account]
pub struct MyStorage {
    x: u64,
}

在写入时被序列化为字节序列并存储在 data 字段中。在写入期间,data 字段根据该结构被反序列化。

在我们的示例中,我们仅使用了结构中的一个变量,尽管如果我们想的话,可以添加更多变量或其他类型的变量。

Solana 运行时并不强制我们使用结构来存储数据。从 Solana 的角度来看,帐户只是保存数据块。但是,Rust 有很多方便的库将结构转换为数据块和反之亦然,因此结构是惯例。Anchor 在幕后利用这些库。

你不需要使用结构来使用 Solana 帐户。可以直接写入字节序列,但这不是存储数据的便捷方式。

#[account] 宏透明地实现了所有魔法。

6) 单元测试初始化

以下 Typescript 代码将运行上述 Rust 代码。

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { BasicStorage } from "../target/types/basic_storage";

describe("basic_storage", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.BasicStorage as Program<BasicStorage>;

  it("Is initialized!", async () => {
    const seeds = []
    const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    console.log("the storage account address is", myStorage.toBase58());

    await program.methods.initialize().accounts({ myStorage: myStorage }).rpc();
  });
});

这是单元测试的输出:solana account initialize test passing我们将在后面的教程中学习更多,但 Solana 要求我们提前指定一笔交易将与哪些帐户交互。由于我们正在与存储 MyStruct 的帐户交互,因此我们需要提前计算其“地址”,并将其传递给 initialize() 函数。这可以通过以下 Typescript 代码来完成:

seeds = []
const [myStorage, _bump] = 
    anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

请注意,seeds 是一个空数组,就像它在 Anchor 程序中一样。

预测 Solana 中的帐户地址就像以太坊中的 create2

在以太坊中,使用 create2 创建的合约地址依赖于:

  • 部署合约的地址
  • 一个盐值
  • 以及创建合约的字节码

预测 Solana 中初始化帐户的地址则非常相似,只是忽略了“字节码”。具体而言,它依赖于:

  • 拥有存储帐户的程序 basic_storage(这类似于部署合约的地址)
  • 以及 seeds(这类似于 create2 的“盐”)

在本教程中的所有示例中,seeds 是一个空数组,但我们将在后面的教程中探讨非空数组。

不要忘记将 my_storage 转为 myStorage

Anchor 将 Rust 的蛇形命名法默默转换为 Typescript 的驼峰命名法。当我们在 Typescript 中向 initialize 函数提供 .accounts({myStorage: myStorage}) 时,它是在“填写” Rust 中 Initialize 结构的 my_storage 键(下方绿色圆圈)。system_programSigner 会由 Anchor 静默填充:snake case to camel case conversions

帐户不能被初始化两次

如果我们可以重新初始化一个帐户,那将非常麻烦,因为用户可能会清除系统中的数据!幸运的是,Anchor 在后台对此进行了防护。

如果你在第二次运行测试(不重置本地验证器)时会得到下图中的错误。

或者,如果你不使用本地验证器,可以运行以下测试:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { BasicStorage } from "../target/types/basic_storage";

describe("basic_storage", () => {
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.BasicStorage as Program<BasicStorage>;

  it("Is initialized!", async () => {
    const seeds = []
    const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);

    // ********************************************
    // **** 请注意,我们调用了初始化两次 ****
    // ********************************************
    await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
    await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
  });
});

当我们运行测试时,测试失败,因为第二次调用 initialize 抛出了错误。预期输出如下所示:solana account cannot be initialized twice

不要忘记在多次运行测试时重置验证器

因为 solana-test-validator 会仍然记住第一次单元测试中的帐户,所以你需要使用 solana-test-validator --reset 在测试之间重置验证器。否则,你会遇到上述错误。

初始化帐户的总结

对大多数 EVM 开发者来说,初始化帐户的必要性可能感觉不自然。

别担心,你会不断看到这一代码序列,它会随着时间的推移而成为你的第二天性。

在本教程中,我们只考虑了初始化存储,在接下来的教程中,我们将学习读取、写入和删除存储。将有很多机会让你直观地了解我们今天查看的所有代码的作用。

练习: 修改 MyStorage 以保存 xy,就像它是一个笛卡尔坐标。这意味着在 MyStorage 结构中添加 y 并将它们的类型从 u64 改为 i64。你无需修改代码的其他部分,因为 size_of 会为你重新计算大小。确保重置验证器,以便原始存储帐户被擦除,从而不阻止你再次初始化该帐户。

转载: https://learnblockchain.cn/article/11402

Solana基础 - Solana 计算单元与交易费用介绍

文章详细介绍了Solana区块链中的计算单元(Compute Units)概念,与以太坊的gas机制进行了对比,并探讨了计算单元的优化策略及其对交易费用的影响。

在以太坊中,交易的价格计算为 gasUsed×gasPricegasUsed×gasPrice。这告诉我们,将交易包含在区块链中将花费多少 Ether。在发送交易之前,会指定并预付 gasLimit。如果交易用尽了 gas,它将被回滚。

与 EVM 链不同,Solana 的操作码/指令消耗的是“计算单元”(可以说是一个更好的名称),而不是 gas,每笔交易的软上限为 200,000 个计算单元。如果交易消耗的计算单元超过 200,000,它将被回滚。

在以太坊中,计算所需的 gas 成本与存储相关的 gas 成本是相同的。在 Solana 中,存储的处理方式不同,因此 Solana 中持久化数据的定价是一个不同的讨论话题。

然而,从运行操作码的定价角度来看,以太坊和 Solana 的行为是相似的。

两条链都执行编译后的字节码,并对每条执行的指令收取费用。以太坊使用 EVM 字节码,但 Solana 运行的是 Berkeley Packet Filter 的修改版本,称为 Solana Packet Filter。

以太坊根据执行时间的长短对不同操作码收取不同的费用,范围从 1 gas 到数千 gas。在 Solana 中,每个操作码消耗 1 个计算单元。

当计算单元不足时该怎么办

在执行无法在限制内完成的重计算操作时,传统策略是“保存你的工作”并在多个交易中完成。

“保存你的工作”部分需要放入永久存储中,这是我们尚未涉及的内容。这类似于在以太坊中尝试遍历一个巨大的循环;你会有一个存储变量来保存你离开时的索引,以及一个存储变量来保存到该点为止的计算结果。

计算单元优化

正如我们所知,Solana 使用计算单元来防止停机问题,并防止运行永远执行的代码。每笔交易的计算单元限制为 200,000 CU(可以以额外成本增加到 1.4m CU),如果超过(选择的限制),程序将终止,所有更改的状态将回滚,费用不会退还给调用者。这可以防止攻击者意图在节点上运行永不结束或计算密集型的程序以减慢或停止链。

然而,与 EVM 链不同,交易中使用的计算资源不会影响为该交易支付的费用。无论你使用了整个限制还是只使用了很少的一部分,你都将被收取相同的费用。例如,400 计算单元的交易与 200,000 计算单元的交易费用相同。

除了计算单元之外,Solana 交易的签名者数量也会影响计算单元成本。根据 Solana 文档

“目前,交易费用仅由交易中需要验证的签名数量决定。交易中签名数量的唯一限制是交易本身的最大大小。交易中的每个签名(64 字节)(最大 1232 字节)必须引用一个唯一的公钥(32 字节),因此单个交易最多可以包含 12 个签名(不确定为什么要这样做)。”

我们可以通过这个小例子看到这一点。从一个空的 Solana 程序开始:

use anchor_lang::prelude::*;

declare_id!("6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC");

##[program]
pub mod compute_unit {
    use super::*;

    pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

##[derive(Accounts)]
pub struct Initialize {}

更新测试文件:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ComputeUnit } from "../target/types/compute_unit";

describe("compute_unit", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());
  const program = anchor.workspace.ComputeUnit as Program<ComputeUnit>;
  const defaultKeyPair = new anchor.web3.PublicKey(
    // 将此替换为你的默认提供者密钥对,你可以通过在终端中运行 `solana address` 来获取它
    "EXJupeVMqDbHk7xY4XP4TVXq22L3ZJxJ9Gm68hJccpLp"
  );

  it("Is initialized!", async () => {
    // 记录密钥对的初始余额
    let bal_before = await program.provider.connection.getBalance(
      defaultKeyPair
    );
    console.log("before:", bal_before);

    // 调用我们程序的初始化函数
    const tx = await program.methods.initialize().rpc();

    // 记录密钥对的余额之后
    let bal_after = await program.provider.connection.getBalance(
      defaultKeyPair
    );
    console.log("after:", bal_after);

    // 记录差异
    console.log(
      "diff:",
      BigInt(bal_before.toString()) - BigInt(bal_after.toString())
    );
  });
});

注意: 在 JavaScript 中,数字末尾的“n”表示它是一个 BigInt

运行:solana logs,如果你还没有运行它的话。

当我们运行 anchor test --skip-local-validator 时,我们得到以下输出作为测试日志和 Solana 验证器日志:

## 测试日志
        compute_unit
before: 15538436120
after: 15538431120
diff: 5000n


## solana 日志
Status: Ok
Log Messages:
  Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC invoke [1]
  Program log: Instruction: Initialize
  Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC consumed 320 of 200000 compute units
  Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC success

余额差异为 5000 lamports,因为我们在发送此交易时只需要/使用 1 个签名(我们的默认提供者地址的签名)。这与我们上面建立的结论一致,即 1 * 5000 = 5000。另外请注意,这消耗了 320 个计算单元,但这个数量不会影响我们的交易费用。

现在,让我们为我们的程序增加一些复杂性,看看会发生什么:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let mut a = Vec::new();
    a.push(1);
    a.push(2);
    a.push(3);
    a.push(4);
    a.push(5);

    Ok(())
}

当然,这应该会对我们的交易费用产生影响,对吧?

当我们运行 anchor test --skip-local-validator 时,我们得到以下输出作为测试日志和 Solana 验证器日志:

## 测试日志
compute_unit
before: 15538436120
after: 15538431120
diff: 5000n


## solana 日志
Status: Ok
Log Messages:
  Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC invoke [1]
  Program log: Instruction: Initialize
  Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC consumed 593 of 200000 compute units
  Program 6CCLqLGeyExCFegJDjRDirWQRRSbM5XNq3yKvmaWS2ZC success

我们可以看到,这消耗了更多的计算单元,几乎是第一个例子的两倍。但这并不影响我们的交易费用。这是预期的,表明计算单元确实不会影响用户支付的交易费用。

无论消耗了多少计算单元,交易都收取了 5000 lamports 或 0.000005 SOL。

回到计算单元。那么,既然计算单元不影响交易费用,我们为什么要优化计算单元呢?

  • 首先,这只是目前的情况,未来 Solana 可能会决定将限制提高到更高,并且必须激励节点不将这些复杂交易与简单交易区别对待。这将意味着在计算交易费用时考虑消耗的计算单元。
  • 其次,如果网络活动竞争区块空间,较小的交易更有可能被包含在区块中。
  • 第三,这将使你的程序更容易与其他程序组合。如果另一个程序调用你的程序,交易不会获得额外的计算限制。如果交易使用了太多计算单元,其他程序可能不愿意与你的程序集成,因为这会为原始程序留下很少的计算单元。

较小的整数节省计算单元

使用的值类型越大,消耗的计算单元越多。在适用的情况下,最好使用较小的类型。让我们以代码示例和注释为例:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // 这消耗 600 CU(类型默认为 Vec<i32>)
    let mut a = Vec::new();
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);

    // 这消耗 618 CU
    let mut a: Vec<u64> = Vec::new();
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);

    // 这消耗 600 CU(与第一个相同,但类型已明确表示)
    let mut a: Vec<i32> = Vec::new();
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);

    // 这消耗 618 CU(与 u64 占用相同的空间)
    let mut a: Vec<i64> = Vec::new();
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);

    // 这消耗 459 CU
    let mut a: Vec<u8> = Vec::new();
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);
    a.push(1);

    Ok(())
}

注意随着整数类型的减小,计算单元成本的减少。这是预期的,因为较大的类型在内存中占用的空间比较小的类型大,无论表示的值如何。

使用 find_program_address 在链上生成程序派生账户(PDA)可能会消耗更多的计算单元,因为此方法会迭代调用 create_program_address,直到找到不在 ed25519 曲线上的 PDA。为了减少计算成本,尽可能在链外使用 find_program_address() 并将生成的 bump seed 传递给程序。更多内容将在后面的部分讨论,因为这超出了本节的范围。

这不是一个详尽的列表,但几点可以让你了解什么会使一个程序比另一个程序更计算密集。

什么是 eBPF?

Solana 的字节码主要源自 BPF。“eBPF” 简单来说就是“扩展的 BPF”。本节解释了 Linux 上下文中的 BPF。

正如你所料,Solana VM 不理解 Rust 或 C。用这些语言编写的程序被编译为 eBPF(扩展的 Berkeley Packet Filter)。

简而言之,eBPF 允许在内核中(在沙盒环境中)执行任意的 eBPF 字节码,当内核发出 eBPF 字节码订阅的事件时,例如:

  • 网络:打开/关闭套接字
  • 磁盘:写入/读取
  • 进程的创建
  • 线程的创建
  • CPU 指令调用
  • 支持高达 64 位(这就是为什么 Solana 的最大 uint 类型是 u64)

你可以将其视为内核的 JavaScript。JavaScript 在浏览器中发出事件时执行操作,eBPF 在内核中发出事件时执行类似的操作,例如当执行系统调用时。

这使我们能够为各种用例构建程序,例如(基于上面列出的事件):

  • 网络:分析路由等
  • 安全:根据某些规则过滤流量并报告任何不良/被阻止的流量
  • 跟踪和分析:从用户空间程序到内核指令收集详细的执行流程
  • 可观察性:报告和分析内核活动

程序仅在我们需要时执行(即当内核中发出事件时)。例如,假设你想在文件被写入时获取文件名和写入的数据,我们监听/注册/订阅 vfs_write() 系统调用事件。现在,每当该文件被写入时,我们就可以使用这些数据。

Solana 字节码格式(SBF)

Solana 字节码格式是 eBPF 的一个变体,具有某些变化,最突出的是移除了字节码验证器。字节码验证器存在于 eBPF 中,以确保所有可能的执行路径都是有限且安全执行的。

Solana 使用计算单元限制来处理这个问题。拥有一个限制计算资源消耗的计算单元计量器将安全检查移到运行时,并允许任意内存访问、间接跳转、循环和其他有趣的行为。

在后面的教程中,我们将深入探讨一个简单的程序及其字节码,调整它,了解不同的计算单元成本,并确切了解 Solana 字节码的工作原理以及如何分析它。

转载:https://learnblockchain.cn/article/11487

Solana基础 - Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者

本文详细比较了Solidity和Solana智能合约中的msg.sender和tx.origin概念,并提供了在Solana中如何实现类似功能的代码示例。文章还介绍了如何在Solana中处理多个签名者以及如何实现onlyOwner功能。

在Solidity中,msg.sender 是一个全局变量,表示调用或发起智能合约函数调用的地址。全局变量 tx.origin 是签署交易的钱包。

在Solana中,没有与 msg.sender 相当的概念。

tx.origin 有类似的概念,但你需要注意的是,Solana交易可以有多个签名者,因此我们可以将其视为具有“多个 tx.origins”。

要在Solana中获取 “tx.origin” 地址,你需要通过将签名者账户添加到函数上下文中并在调用函数时传递调用者的账户来进行设置。

让我们看看如何在Solana中访问交易签名者的地址的示例:

use anchor_lang::prelude::*;

declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");

#[program]
pub mod day14 {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let the_signer1: &mut Signer = &mut ctx.accounts.signer1;

        // 函数逻辑....

        msg!("签名者1: {:?}", *the_signer1.key);

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub signer1: Signer<'info>,
}

在上面的代码片段中,Signer<'info> 用于验证 Initialize<'info> 账户结构中的 signer1 账户已签署该交易。

initialize 函数中,signer1 账户从上下文中可变引用并被赋值给 the_signer1 变量。

然后最后,我们使用 msg! 宏记录 signer1 的公钥(地址),并传入 *the_signer1.key,这会解引用并访问通过 the_signer1 指向的实际值的 key 字段或方法。

接下来是为上述程序编写测试:

describe("Day14", () => {
  // 将客户端配置为使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day14 as Program<Day14>;

  it("由单个签名者签名", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods.initialize().accounts({
      signer1: program.provider.publicKey
    }).rpc();

    console.log("签名者1: ", program.provider.publicKey.toBase58());
  });
});

在测试中,我们将我们的钱包账户作为签名者传递给 signer1 账户,然后调用初始化函数。之后,我们将钱包账户记录到控制台,以验证其与程序中的一致性。

练习: 你注意到了在运行测试后 shell_1(命令终端)和 shell_3(日志终端)中的输出有什么吗?

多个签名者

在Solana中,我们也可以有多个签名者签署一个交易,你可以把它看作是将一堆签名捆绑在一起并发送到一个交易中。一个用例是在一个交易中进行多签交易。

要做到这一点,我们只需在程序的账户结构中添加更多的 Signer 结构,然后确保在调用函数时传递必要的账户:

use anchor_lang::prelude::*;

declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");

#[program]
pub mod day14 {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let the_signer1: &mut Signer = &mut ctx.accounts.signer1;
        let the_signer2: &mut Signer = &mut ctx.accounts.signer2;

        msg!("签名者1: {:?}", *the_signer1.key);
        msg!("签名者2: {:?}", *the_signer2.key);

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    pub signer1: Signer<'info>,
    pub signer2: Signer<'info>,
}

上面的示例与单个签名者示例基本相同,有一个显著的区别。在这种情况下,我们向 Initialize 结构添加了另一个签名者账户(signer2),并在 initialize 函数中记录了两个签名者的公钥。

调用 initialize 函数与多个签名者是不同的,下面的测试显示如何调用多个签名者的函数:

describe("Day14", () => {
  // 将客户端配置为使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day14 as Program<Day14>;

  // 生成一个签名者来调用我们的函数
  let myKeypair = anchor.web3.Keypair.generate();

  it("由多个签名者签名", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods
      .initialize()
      .accounts({
        signer1: program.provider.publicKey,
        signer2: myKeypair.publicKey,
      })
      .signers([myKeypair])
      .rpc();

    console.log("签名者1: ", program.provider.publicKey.toBase58());
    console.log("签名者2: ", myKeypair.publicKey.toBase58());
  });
});

那么上面的测试有什么不同呢?首先是 signers() 方法,它接收一个签名者数组作为参数来签署交易。但我们在数组中只有一个签名者,而不是两个。Anchor 会自动将提供者中的钱包账户作为签名者,因此我们不需要将其再次添加到签名者数组中。

生成随机地址进行测试

第二个变化是 myKeypair 变量,它存储了通过 anchor.web3 模块随机生成的 Keypair(一个公钥及其对应的私钥以访问账户)。在测试中,我们将 Keypair(存储在 myKeypair 变量中的)公钥分配给 signer2 账户,因此它作为参数传递给 .signers([myKeypair]) 方法。

多次运行测试,你会注意到 signer1 的公钥没有变化,但 signer2 的公钥发生了变化。这是因为分配给 signer1 账户的钱包账户(在测试中)来自提供者,它也是你本地计算机上的Solana钱包账户,而分配给 signer2 的账户则是每次运行 anchor test —skip-local-validator 都是随机生成的。

练习: 创建另一个函数(你可以随意命名)要求三个签名者(提供者钱包账户和两个随机生成的账户),并为其编写测试。

onlyOwner

这是在Solidity中常用的模式,限制函数的访问仅限于合约的所有者。使用 Anchor 的 #[access_control] 属性,我们也可以实现 only owner 模式,即限制我们Solana程序中函数的访问到一个 PubKey(所有者的地址)。

以下是如何在Solana中实现“onlyOwner”功能的示例:

use anchor_lang::prelude::*;

declare_id!("Hf96fZsgq9R6Y1AHfyGbhi9EAmaQw2oks8NqakS6XVt1");

// 注意:替换为你的钱包的公钥
const OWNER: &str = "8os8PKYmeVjU1mmwHZZNTEv5hpBXi5VvEKGzykduZAik";

#[program]
pub mod day14 {
    use super::*;

    #[access_control(check(&ctx))]
    pub fn initialize(ctx: Context<OnlyOwner>) -> Result<()> {
        // 函数逻辑...

        msg!("嗨,我是所有者。");
        Ok(())
    }
}

fn check(ctx: &Context<OnlyOwner>) -> Result<()> {
    // 检查签名者是否为所有者
    require_keys_eq!(
        ctx.accounts.signer_account.key(),
        OWNER.parse::<Pubkey>().unwrap(),
        OnlyOwnerError::NotOwner
    );

    Ok(())
}

#[derive(Accounts)]
pub struct OnlyOwner<'info> {
    signer_account: Signer<'info>,
}

// 自定义错误代码的枚举
#[error_code]
pub enum OnlyOwnerError {
    #[msg("只有所有者可以调用此函数!")]
    NotOwner,
}

在上面的代码中,OWNER 变量存储与我本地Solana钱包关联的公钥(地址)。在测试之前,请确保替换 OWNER 变量为你的钱包的公钥。你可以通过运行 solana address 命令轻松检索到你的公钥。

#[access_control] 属性在运行主要指令之前执行给定的访问控制方法。当调用 initialize 函数时,访问控制方法 (check) 会在 initialize 函数之前执行。check 方法接受引用上下文作为参数,然后检查交易的签名者是否等于 OWNER 变量的值。require_keys_eq! 宏确保两个公钥值相等,如果为真,则执行 initialize 函数,否则它会以 NotOwner 自定义错误进行回滚。

测试 onlyOwner 功能——正常情况

在下面的测试中,我们调用 initialize 函数并使用所有者的密钥对交易进行签名:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Day14 } from "../target/types/day14";

describe("day14", () => {
  // 将客户端配置为使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day14 as Program<Day14>;

  it("由所有者调用", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods
      .initialize()
      .accounts({
        signerAccount: program.provider.publicKey,
      })
      .rpc();

    console.log("交易哈希:", tx);
  });
});

我们调用 initialize 函数,并将钱包账户(本地Solana钱包账户)通过提供者传递给 signerAccount,以验证该钱包账户确实签署了交易。还要记住,Anchor 会自动使用提供者中的钱包账户秘密签署任何交易。

运行测试 anchor test --skip-local-validator,如果一切正确,测试应该通过:Anchor测试通过

测试签名者不是所有者——攻击情况

使用不是所有者的不同密钥对 initialize 函数进行调用和签署交易将抛出错误,因为该函数调用仅限于所有者:

describe("day14", () => {
  // 将客户端配置为使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day14 as Program<Day14>;

  let Keypair = anchor.web3.Keypair.generate();

  it("NOT由所有者调用", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods
      .initialize()
      .accounts({
        signerAccount: Keypair.publicKey,
      })
      .signers([Keypair])
      .rpc();

    console.log("交易哈希:", tx);
  });
});

这里我们生成了一个随机的密钥对,并用其来签署交易。让我们再次运行测试:anchor测试因签名者错误而失败正如预期的那样,我们得到了一个错误,因为签名者的公钥不等于所有者的公钥。

修改所有者

要在程序中更改所有者,必须将赋值给所有者的公钥存储在链上。但是,关于Solana中“存储”的讨论将在未来的教程中涵盖。

所有者可以重新部署字节码。

练习:升级上述程序以拥有新所有者。

转载:https://learnblockchain.cn/article/11372

Solana基础 - Solana 日志、“事件”与交易历史

本文介绍了 Solana 中的日志和事件机制,解释了如何通过事件在前端传递信息,以及如何查询 Solana 的交易历史。与以太坊不同,Solana 的事件不能用于历史查询,而是更适合实时监听。

Solana 程序可以发出事件,类似于 Ethereum 发出事件,不过我们会讨论其中的一些差异。

具体而言,Solana 中的事件旨在将信息传递给前端,而不是记录过去的交易。如需获取历史记录,可以通过地址查询 Solana 交易。

Solana 日志和事件

下面的程序有两个事件:MyEventMySecondEvent。与 Ethereum 事件有“参数”类似,Solana 事件在结构体中有字段:

use anchor_lang::prelude::*;

declare_id!("FmyZrMmPvRzmJCG3p5R1AnbkPqSmzdJrcYzgnQiGKuBq");

#[program]
pub mod emit {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        emit!(MyEvent { value: 42 });
        emit!(MySecondEvent { value: 3, message: "hello world".to_string() });
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

#[event]
pub struct MyEvent {
    pub value: u64,
}

#[event]
pub struct MySecondEvent {
    pub value: u64,
    pub message: String,
}

事件成为 Solana 程序的 IDL 的一部分,类似于事件是 Solidity 智能合约 ABI 的一部分。下面我们截图了上面程序的 IDL,并高亮显示了相关部分:Solana IDL 中的事件定义

在 Solana 中没有类似于 Ethereum 的“索引”和“非索引”信息(即使在上面的截图中有一个“索引”字段,它也没有用)。

与 Ethereum 不同,我们不能直接根据区块编号的范围查询过去的事件。我们只能在事件发生时进行监听。(稍后我们将看到 Solana 的审计过去交易的方法)。下面的代码展示了如何在 Solana 中监听事件:

import * as anchor from "@coral-xyz/anchor";
import { BorshCoder, EventParser, Program } from "@coral-xyz/anchor";
import { Emit } from "../target/types/emit";

describe("emit", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Emit as Program<Emit>;

  it("已初始化!", async () => {
    const listenerMyEvent = program.addEventListener('MyEvent', (event, slot) => {
      console.log(`槽 ${slot} 事件值 ${event.value}`);
    });

    const listenerMySecondEvent = program.addEventListener('MySecondEvent', (event, slot) => {
      console.log(`槽 ${slot} 事件值 ${event.value} 事件消息 ${event.message}`);
    });

    await program.methods.initialize().rpc();

    // 这行仅用于测试,确保事件监听器有时间来监听事件。
    await new Promise((resolve) => setTimeout(resolve, 5000));

    program.removeEventListener(listenerMyEvent);
    program.removeEventListener(listenerMySecondEvent);
  });
});

在 Solana 中不能像在 Ethereum 中那样扫描过去的日志,它们必须在交易发生时进行监听。测试中的 Solana 事件日志

日志底层工作原理

在 EVM 中,日志通过运行 log0log1log2 等操作码发出。在 Solana 中,日志是通过调用系统调用 sol_log_data 来运行的。作为参数,简单地是一个字节序列:

https://docs.rs/solana-program/latest/src/solana_program/log.rs.html#116-124

以下是 Solana 客户端中系统调用的函数:

/// 将某些切片打印为 base64。
pub fn sol_log_data(data: &[&[u8]]) {
    #[cfg(target_os = "solana")]
    unsafe {
        crate::syscalls::sol_log_data(data as *const _ as *const u8, data.len() as u64)
    };

    #[cfg(not(target_os = "solana"))]
    crate::program_stubs::sol_log_data(data);
}

我们用来创建事件的“结构体”结构是对字节序列的抽象。在后台,Anchor 将结构体转换为字节序列传递给此函数。Solana 系统调用仅接受字节序列,而不接受结构体。

Solana 日志不适用于历史查询

在 Ethereum 中,日志用于审计目的,但在 Solana 中,日志不能以这种方式使用,因为它们只能在发生时被查询。因此,它们更适合将信息传递给前端应用程序。Solana 函数不能像 Solidity 查看函数那样返回数据给前端,所以 Solana 日志是一种轻量级的实现方式。

然而,事件会保存在区块浏览器中。请查看这个交易的底部作为例子:
https://explorer.solana.com/tx/JgyHQPxL3cPLFtV4cx5i842ZgBx57R2fkNn2TZn1wsQZqVXKfijd43CEHo88C3ridK27Kw8KkMzfvDdqaS398SX

与 Ethereum 不同,Solana 交易可以根据地址查询

在 Ethereum 中,没有直接的方法查询发往智能合约或特定钱包的交易。

我们可以通过 eth_getTransactionCount 计算从一个地址发送的交易数量。我们可以使用交易哈希通过 eth_getTransactionByHash 获取特定交易。我们可以使用 eth_getBlockByNumbereth_getBlockByHash 获取特定区块中的交易。

但是,不可能按地址获取所有交易。这必须通过解析自钱包激活或智能合约部署以来的每个区块来间接完成。

为了审计智能合约中的交易,开发人员添加 智能合约事件 以查询感兴趣的交易。

获取 Solana 中的交易历史

另一方面,Solana 有一个 RPC 函数 getSignaturesForAddress,用于列出某个地址所做的所有交易。该地址可以是一个程序或一个钱包。

以下是一个列出某个地址的交易的脚本:

let web3 = require('@solana/web3.js');

const solanaConnection = new web3.Connection(web3.clusterApiUrl("mainnet-beta"));

const getTransactions = async(address,limit) => {
  const pubKey = new web3.PublicKey(address);
  let transactionList = await solanaConnection.getSignaturesForAddress(pubKey, {limit: limit});
  let signatureList = transactionList.map(transaction => transaction.signature);

  console.log(signatureList);

  for await (const sig of signatureList) {
    console.log(await solanaConnection.getParsedTransaction(sig, {maxSupportedTransactionVersion: 0}));
  }
}

let myAddress = "请在此输入地址";

getTransactions(myAddress, 3);

请注意,交易的实际内容是通过 getParsedTransaction RPC 方法获取的。

转载:https://learnblockchain.cn/article/11416

Solana基础 - Solana 系统变量详解

文章详细介绍了 Solana 中的系统变量(sysvars),包括如何通过 Anchor 框架访问这些变量,以及它们的功能和使用场景。

在 Solana 中,sysvar 是只读系统账户,它们使 Solana 程序能够访问区块链状态和网络信息。它们类似于 Ethereum 的全局变量,这些变量也使智能合约能够访问网络或区块链状态信息,但它们具有类似于 Ethereum 预编译的唯一公共地址。

在 Anchor 程序中,你可以通过两种方式访问 sysvars:要么使用 anchor 的 get 方法包装,或者将其视为你的 #[Derive(Accounts)] 中的账户,使用其公共地址。

并非所有 sysvars 都支持 get 方法,并且其中一些已被弃用(关于弃用的信息将在本指南中指定)。对于没有 get 方法的 sysvars,我们将使用其公共地址进行访问。

  • Clock: 用于与时间相关的操作,例如获取当前时间或 slot 编号。
  • EpochSchedule: 包含关于纪元调度的信息,包括特定 slot 的纪元。
  • Rent: 包含租金比率和信息,例如保持账户免租的最低余额要求。
  • Fees: 包含当前 slot 的费用计算器。费用计算器提供了有关在 Solana 交易中每个签名支付多少 lamports 的信息。
  • EpochRewards: EpochRewards sysvar 保留了 Solana 中的纪元奖励分配记录,包括区块奖励和质押奖励。
  • RecentBlockhashes: 包含活跃的最近区块哈希。
  • SlotHashes: 包含近期 slot 哈希的历史记录。
  • SlotHistory: 保留 Solana 中最近纪元内的可用 slot 数组,并在处理新 slot 时进行更新。
  • StakeHistory: 维护整个网络的质押激活和停用记录,按纪元进行记录,并在每个纪元开始时更新。
  • Instructions: 获取当前交易中序列化指令的访问权限。
  • LastRestartSlot: 包含最后一次重启的 slot 编号(Solana 上一次重启的时间)或为零(如果从未发生过)。如果 Solana 区块链崩溃并重启,则应用程序可以使用此信息来确定它是否应等待事情稳定。

区分 Solana 的 slots 和 blocks。

slot 是一个时间窗口(大约 400 毫秒),在该窗口中,指定的领导者可以生成一个区块。一个 slot 包含一个区块(与 Ethereum 上相同类型的区块,即交易列表)。然而,如果区块领导者未能在该 slot 产生区块,则该 slot 可能不包含区块。其关系如下图所示:

solana slots and blocks

尽管每个区块映射到恰好一个 slot,但区块哈希与 slot 哈希并不相同。当你在浏览器中单击一个 slot 编号时,则会打开带有不同哈希的区块详细信息,这一区别是显而易见的。

让我们看下图中来自 Solana 区块浏览器的一个例子:solana slot hashes

图片中高亮的绿色数字是 slot 编号 237240962 ,而高亮的黄色文本是 slot 哈希 DYFtWxEdLbos9E6SjZQCMq8z242Yv2bVoj6dzwskd5vZ。下面高亮的红色的区块哈希是 FzHwFHDAXJBc55rpjShznGCBnC7DsTCjxf3KKAk6hk9T

(其他区块详细信息已被裁剪):solana blockhash

我们可以通过它们独特的哈希来区分区块和 slot,即使它们具有相同的数字。

作为测试,点击浏览器中的任何 slot 编号 在这里,你会注意到将打开一个区块页面。此区块的哈希将与 slot 哈希不同。

使用 get 方法访问 Solana Sysvars

如前所述,并非所有 sysvars 都可以使用 Anchor 的 get 方法进行访问。诸如 Clock、EpochSchedule 和 Rent 之类的 sysvars 可以通过此方法访问。

虽然 Solana 文档将 Fees 和 EpochRewards 列为可以通过 get 方法访问的 sysvars,但在最新版本的 Anchor 中这些已被弃用。因此,无法通过 get 方法在 Anchor 中调用它们。

我们将使用 get 方法访问并记录所有当前支持的 sysvars 的内容。首先,我们创建一个新的 Anchor 项目:

anchor init sysvars
cd sysvars
anchor build

Clock sysvar

要利用 Clock sysvar,我们可以调用 Clock::get()(我们在之前的教程中做过类似的事情)方法,如下所示。

在我们项目的初始化函数中添加以下代码:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // 获取 Clock sysvar
    let clock = Clock::get()?;

    msg!(
        "clock: {:?}",
        // 获取 Clock sysvar 的所有详细信息
        clock
    );

    Ok(())
}

现在,运行对本地方的 Solana 节点的测试并检查日志:solana epoch

EpochSchedule sysvar

在 Solana 中,纪元是大约两天的时间段。SOL 只能在纪元开始时质押或解除质押。如果你在纪元结束之前质押(或解除质押)SOL,则该 SOL 被标记为“激活”或“停用”,同时等待纪元结束。

Solana 在其关于 委托 SOL 的描述中详细说明了这一点。

我们可以使用 get 方法访问 EpochSchedule sysvar,类似于 Clock sysvar。

更新初始化函数,添加以下代码:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // 获取 EpochSchedule sysvar
    let epoch_schedule = EpochSchedule::get()?;

    msg!(
        "epoch schedule: {:?}",
        // 获取 EpochSchedule sysvar 的所有详细信息
        epoch_schedule
    );

    Ok(())
}

再次运行测试,将生成以下日志:test output log

从日志中,我们可以观察到 EpochSchedule sysvar 包含以下字段:

  • slots_per_epoch 以黄色高亮显示,表示每个纪元的 slot 数量,这里是 432,000 个 slot。
  • leader_schedule_slot_offset 以红色高亮显示,决定了下一个纪元的领导者调度的时间(我们在第 11 天讨论过这个)。它也设置为 432,000。
  • warmup 以紫色高亮显示,是一个布尔值,指示 Solana 是否处于预热阶段。在此阶段,纪元开始较小,并逐渐增加大小。这有助于网络在重置后或早期阶段平稳启动。
  • first_normal_epoch 以橙色高亮显示,标识可以拥有其 slot 数的第一个纪元,first_normal_slot 以蓝色高亮显示,则是开始此纪元的 slot。在此情况下,两者均为 0(零)。

我们看到 first_normal_epochfirst_normal_slot 为 0 的原因是测试验证器尚未运行两天。如果我们在主网(写作时)运行此命令,我们期待看到 first_normal_epoch 为 576 和 first_normal_slot 为 248,832,000。

solana recent epoch

Rent sysvar

我们再次使用 get 方法访问 Rent sysvar。

我们更新初始化函数,添加以下代码:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // 之前的代码...

    // 获取 Rent sysvar
    let rent_var = Rent::get()?;
    msg!(
        "Rent {:?}",
        // 获取 Rent sysvar 的所有详细信息
        rent_var
    );

    Ok(())
}

运行测试后,我们得到这个日志:solana rent sysvar

Solana 的 Rent sysvar 具有三个关键字段:

  • lamports_per_byte_year
  • exemption_threshold
  • burn_percent

lamports_per_byte_year 以黄色高亮显示,表示每年每字节所需的 lamports 数,以获得免租资格。

exemption_threshold 以红色高亮显示,是一个用于计算免租资格所需最低余额的乘数。在此示例中,我们看到需要支付 3480×2=69603480×2=6960 lamports 每字节才能创建新账户。

其中 50% 的费用被烧毁(burn_percent 以紫色高亮显示),以管理 Solana 的通货膨胀。

“租金”的概念将在后面的教程中完整解释。

使用 sysvar 公共地址访问 Anchor 中的 Sysvars

对于不支持 get 方法的 sysvars,我们可以使用其公共地址进行访问。任何对此的例外将被指定。

StakeHistory sysvar

回想一下,我们之前提到过,该 sysvar 维护整个网络的质押激活和停用记录,按纪元进行记录。然而,由于我们正在运行一个本地验证器节点,因此此 sysvar 将返回空数据。

我们将通过其公共地址 SysvarStakeHistory1111111111111111111111111 访问此 sysvar。

首先,我们修改我们项目中的 Initialize 账户结构,如下所示:

#[derive(Accounts)]
pub struct Initialize<'info> {
    /// CHECK:
    pub stake_history: AccountInfo<'info>, // 为 StakeHistory sysvar 创建一个账户
}

我们要求读者现在将新的语法视为模板。/// CHECK:AccountInfo 将在以后的教程中进行解释。对于好奇的人,<'info> 标记是一个 Rust 生命周期

接下来,我们向 initialize 函数添加以下代码。

(对 sysvar 账户的引用将在我们的测试中作为事务的一部分传递。之前的示例中将它们构建到 Anchor 框架中)。

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // 之前的代码...

    // 访问 StakeHistory sysvar
    // 创建一个数组以存储 StakeHistory 账户
    let arr = [ctx.accounts.stake_history.clone()];

    // 为数组创建一个迭代器
    let accounts_iter = &mut arr.iter();

    // 从迭代器获取下一个账户信息(仍然是 StakeHistory)
    let sh_sysvar_info = next_account_info(accounts_iter)?;

    // 从账户信息创建一个 StakeHistory 实例
    let stake_history = StakeHistory::from_account_info(sh_sysvar_info)?;

    msg!("stake_history: {:?}", stake_history);

    Ok(())
}

我们不导入 StakeHistory sysvar,因为我们可以通过使用 super::*; import 进行访问。如果不是这种情况,我们将导入特定的 sysvar。

并更新测试:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Sysvars } from "../target/types/sysvars";

describe("sysvars", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Sysvars as Program<Sysvars>;

  // 创建 StakeHistory 公共密钥对象
  const StakeHistory_PublicKey = new anchor.web3.PublicKey(
    "SysvarStakeHistory1111111111111111111111111"
  );

  it("已初始化!", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods
      .initialize()
      .accounts({
        stakeHistory: StakeHistory_PublicKey,
      })
      .rpc();
    console.log("你的交易签名", tx);
  });
});

现在,我们再运行一次测试:solana stake history

正如前面提到的,它为我们的本地验证器返回空数据。

我们还可以通过将我们的 StakeHistory_PublicKey 变量替换为 anchor.web3.SYSVAR_STAKE_HISTORY_PUBKEY 从 Anchor TypeScript 客户端获得 StakeHistory sysvar 的公共密钥。

RecentBlockhashes sysvar

如何访问该 sysvar 在我们的 上一篇教程 中讨论过。请记住,它已被弃用,并且支持将被丢弃。

Fees sysvar

Fees sysvar 也已被弃用。

Instruction sysvar

此 sysvar 可用于访问当前交易的序列化指令以及该交易的一些元数据。我们将在下面演示这一点。

首先,我们更新导入内容:

#[program]
pub mod sysvars {
    use super::*;
    use anchor_lang::solana_program::sysvar::{instructions, fees::Fees, recent_blockhashes::RecentBlockhashes};
    // 其余代码
}

接下来,我们将 Instruction sysvar 账户添加到 Initialize 账户结构:

#[derive(Accounts)]
pub struct Initialize<'info> {
    /// CHECK:
    pub stake_history: AccountInfo<'info>, // 为 StakeHistory sysvar 创建一个账户
    /// CHECK:
    pub recent_blockhashes: AccountInfo<'info>,
    /// CHECK:
    pub instruction_sysvar: AccountInfo<'info>,
}

现在,修改 initialize 函数,使其接受一个 number: u32 参数,并添加以下代码到 initialize 函数中。

pub fn initialize(ctx: Context<Initialize>, number: u32) -> Result<()> {
    // 之前的代码...

    // 获取 Instruction sysvar
    let arr = [ctx.accounts.instruction_sysvar.clone()];

    let account_info_iter = &mut arr.iter();

    let instructions_sysvar_account = next_account_info(account_info_iter)?;

    // 从指令 sysvar 账户加载指令详细信息
    let instruction_details =
        instructions::load_instruction_at_checked(0, instructions_sysvar_account)?;

    msg!(
        "此交易的指令详细信息: {:?}",
        instruction_details
    );
    msg!("数字是: {}", number);

    Ok(())
}

与先前的 sysvar 不同,在此示例中,我们使用 load_instruction_at_checked() 方法来检索 sysvar。该方法要求指令数据索引(在此情况下为 0)和指令 sysvar 账户作为参数。

更新测试:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Sysvars } from "../target/types/sysvars";

describe("sysvars", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Sysvars as Program<Sysvars>;

  // 创建 StakeHistory 公共密钥对象
  const StakeHistory_PublicKey = new anchor.web3.PublicKey(
    "SysvarStakeHistory1111111111111111111111111"
  );

  it("已初始化!", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods
      .initialize(3) // 调用 initialize 函数,参数为数字 `3`
      .accounts({
        stakeHistory: StakeHistory_PublicKey, // 将 StakeHistory sysvar 的公共密钥传递到指令所需的账户列表中
        recentBlockhashes: anchor.web3.SYSVAR_RECENT_BLOCKHASHES_PUBKEY, // 将 RecentBlockhashes sysvar 的公共密钥传递到指令所需的账户列表中
        instructionSysvar: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, // 将 Instruction sysvar 的公共密钥传递到指令所需的账户列表中
      })
      .rpc();
    console.log("你的交易签名", tx);
  });
});

并运行测试:solana sysvar instructions

如果我们仔细检查日志,可以看到程序 ID、sysvar 指令的公共密钥、序列化数据和其他元数据。

我们还可以在序列化指令数据和我们自己的程序日志中看到数字 3,以黄色箭头高亮显示。高亮的红色序列化数据是 Anchor 注入的一个鉴别器(我们可以忽略它)。

练习: 访问 LastRestartSlot sysvar

SysvarLastRestartS1ot1111111111111111111111,使用上述方法。请注意,Anchor 没有此 sysvar 的地址,因此你需要创建一个 PublicKey 对象。

当前版本 Anchor 中无法访问的 Solana Sysvars。

在当前版本的 Anchor 中,无法访问某些 sysvars。这些 sysvars 包括 EpochRewards、SlotHistory 和 SlotHashes。当尝试访问这些 sysvars 时,会导致错误。

转载:https://learnblockchain.cn/article/11418