Solana 系统变量详解

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

Solana Sysvars

在 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 \times 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 时,会导致错误。

了解更多

请参见我们的 Solana 课程 获取更多 Solana 教程;本教程是该课程的一部分。

最初发布日期:2024 年 2 月 19 日

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

0 条评论

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