文章详细介绍了 Solana 中的系统变量(sysvars),包括如何通过 Anchor 框架访问这些变量,以及它们的功能和使用场景。
在 Solana 中,sysvar 是只读系统账户,它们使 Solana 程序能够访问区块链状态和网络信息。它们类似于 Ethereum 的全局变量,这些变量也使智能合约能够访问网络或区块链状态信息,但它们具有类似于 Ethereum 预编译的唯一公共地址。
在 Anchor 程序中,你可以通过两种方式访问 sysvars:要么使用 anchor 的 get 方法包装,或者将其视为你的 #[Derive(Accounts)]
中的账户,使用其公共地址。
并非所有 sysvars 都支持 get
方法,并且其中一些已被弃用(关于弃用的信息将在本指南中指定)。对于没有 get
方法的 sysvars,我们将使用其公共地址进行访问。
slot 是一个时间窗口(大约 400 毫秒),在该窗口中,指定的领导者可以生成一个区块。一个 slot 包含一个区块(与 Ethereum 上相同类型的区块,即交易列表)。然而,如果区块领导者未能在该 slot 产生区块,则该 slot 可能不包含区块。其关系如下图所示:
尽管每个区块映射到恰好一个 slot,但区块哈希与 slot 哈希并不相同。当你在浏览器中单击一个 slot 编号时,则会打开带有不同哈希的区块详细信息,这一区别是显而易见的。
让我们看下图中来自 Solana 区块浏览器的一个例子:
图片中高亮的绿色数字是 slot 编号 237240962 ,而高亮的黄色文本是 slot 哈希 DYFtWxEdLbos9E6SjZQCMq8z242Yv2bVoj6dzwskd5vZ。下面高亮的红色的区块哈希是 FzHwFHDAXJBc55rpjShznGCBnC7DsTCjxf3KKAk6hk9T。
(其他区块详细信息已被裁剪):
我们可以通过它们独特的哈希来区分区块和 slot,即使它们具有相同的数字。
作为测试,点击浏览器中的任何 slot 编号 在这里,你会注意到将打开一个区块页面。此区块的哈希将与 slot 哈希不同。
如前所述,并非所有 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::get()
(我们在之前的教程中做过类似的事情)方法,如下所示。
在我们项目的初始化函数中添加以下代码:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// 获取 Clock sysvar
let clock = Clock::get()?;
msg!(
"clock: {:?}",
// 获取 Clock sysvar 的所有详细信息
clock
);
Ok(())
}
现在,运行对本地方的 Solana 节点的测试并检查日志:
在 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(())
}
再次运行测试,将生成以下日志:
从日志中,我们可以观察到 EpochSchedule sysvar 包含以下字段:
我们看到 first_normal_epoch
和 first_normal_slot
为 0 的原因是测试验证器尚未运行两天。如果我们在主网(写作时)运行此命令,我们期待看到 first_normal_epoch
为 576 和 first_normal_slot
为 248,832,000。
我们再次使用 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 具有三个关键字段:
lamports_per_byte_year 以黄色高亮显示,表示每年每字节所需的 lamports 数,以获得免租资格。
exemption_threshold 以红色高亮显示,是一个用于计算免租资格所需最低余额的乘数。在此示例中,我们看到需要支付 $3480 \times 2 = 6960$ lamports 每字节才能创建新账户。
其中 50% 的费用被烧毁(burn_percent 以紫色高亮显示),以管理 Solana 的通货膨胀。
“租金”的概念将在后面的教程中完整解释。
对于不支持 get
方法的 sysvars,我们可以使用其公共地址进行访问。任何对此的例外将被指定。
回想一下,我们之前提到过,该 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);
});
});
现在,我们再运行一次测试:
正如前面提到的,它为我们的本地验证器返回空数据。
我们还可以通过将我们的 StakeHistory_PublicKey
变量替换为 anchor.web3.SYSVAR_STAKE_HISTORY_PUBKEY
从 Anchor TypeScript 客户端获得 StakeHistory sysvar 的公共密钥。
如何访问该 sysvar 在我们的 上一篇教程 中讨论过。请记住,它已被弃用,并且支持将被丢弃。
Fees 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);
});
});
并运行测试:
如果我们仔细检查日志,可以看到程序 ID、sysvar 指令的公共密钥、序列化数据和其他元数据。
我们还可以在序列化指令数据和我们自己的程序日志中看到数字 3
,以黄色箭头高亮显示。高亮的红色序列化数据是 Anchor 注入的一个鉴别器(我们可以忽略它)。
练习: 访问 LastRestartSlot sysvar
SysvarLastRestartS1ot1111111111111111111111,使用上述方法。请注意,Anchor 没有此 sysvar 的地址,因此你需要创建一个 PublicKey 对象。
在当前版本的 Anchor 中,无法访问某些 sysvars。这些 sysvars 包括 EpochRewards、SlotHistory 和 SlotHashes。当尝试访问这些 sysvars 时,会导致错误。
请参见我们的 Solana 课程 获取更多 Solana 教程;本教程是该课程的一部分。
最初发布日期:2024 年 2 月 19 日
- 原文链接: rareskills.io/post/solan...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!