今天我们将介绍 Solidity 中所有 block 变量在 Solana 中的对应物。并非所有变量都有完全的一一对应关系。在 Solidity 中,我们有以下常用的 block 变量:
- block.timestamp
- block.number
- blockhash()
以及一些较少为人知的变量:
- block.coinbase
- block.basefee
- block.chainid
- block.difficulty / block.prevrandao
我们假设你已经知道它们的作用,但如果你需要复习一下,可以在 Solidity global variables doc 中找到它们的解释。
Solana 中的 block.timestamp
通过利用 Clock sysvar 中的 unix_timestamp 字段,我们可以访问 Solana 的区块时间戳。
首先我们初始化一个新的 Anchor 项目:
anchor init sysvar
将 initialize 函数替换为以下内容:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let clock: Clock = Clock::get()?;
msg!(
"Block timestamp: {}",
// Get block.timestamp
clock.unix_timestamp,
);
Ok(())
}
Anchor 的 prelude 模块包含 Clock 结构体,该结构体默认会自动导入:
use anchor_lang::prelude::*;
有些令人困惑的是,unix_timestamp 返回的类型是 i64,而不是 u64,这意味着它支持负数,尽管时间不能为负。然而,时间差是可以为负的。
获取星期几
现在让我们创建一个程序,使用 Clock sysvar 中的 unix_timestamp 来告诉我们当前是星期几。
chrono crate 为 Rust 中的日期和时间操作提供了功能。
在程序目录 ./sysvar/Cargo.toml 下的 Cargo.toml 文件中,将 chrono crate 添加为依赖项:
[dependencies]
chrono = "0.4.31"
在 sysvar 模块中导入 chrono crate:
// ...other code
#[program]
pub mod sysvar {
use super::*;
use chrono::*; // new line here
// ...
}
现在,我们在程序中添加以下函数:
pub fn get_day_of_the_week(
_ctx: Context<Initialize>) -> Result<()> {
let clock = Clock::get()?;
let time_stamp = clock.unix_timestamp; // current timestamp
let date_time = chrono::NaiveDateTime::from_timestamp_opt(time_stamp, 0).unwrap();
let day_of_the_week = date_time.weekday();
msg!("Week day is: {}", day_of_the_week);
Ok(())
}
我们将从 Clock sysvar 获取的当前 unix 时间戳作为参数传递给 from_timestamp_opt 函数,该函数返回一个包含日期和时间的 NaiveDateTime 结构体。然后,我们调用 weekday 方法,根据传递的时间戳获取当前的星期几。
并更新我们的测试:
it("Get day of the week", async () => {
const tx = await program.methods.getDayOfTheWeek().rpc();
console.log("Your transaction signature", tx);
});
再次运行测试并获得以下日志:
Transaction executed in slot 36:
Signature: 5HVAjmo85Yi3yeQX5t6fNorU1da4H1zvgcJN7BaiPGnRwQhjbKd5YHsVE8bppU9Bg2toF4iVBvhbwkAtMo4NJm7V
Status: Ok
Log Messages:
Program H52ppiSyiZyYVn1Yr9DgeUKeChktUiPwDfuuo932Uqxy invoke [1]
Program log: Instruction: GetDayOfTheWeek
Program log: Week day is: Wed
Program H52ppiSyiZyYVn1Yr9DgeUKeChktUiPwDfuuo932Uqxy consumed 1597 of 200000 compute units
请注意 “Week day is: Wed” 的日志。
Solana 中的 block.number
Solana 有一个 “slot number” 的概念,它与 “block number” 非常相关,但并不完全相同。它们之间的区别将在后续教程中介绍,因此我们暂时将其推迟,直到那时再全面讨论如何获取 “block number”。
block.coinbase
在 Ethereum 中,“Block Coinbase” 代表在工作量证明 (PoW) 中成功挖出区块的矿工地址。另一方面,Solana 使用基于 leader 的共识机制,这是历史证明 (PoH) 和权益证明 (PoS) 的结合,从而消除了挖矿的概念。相反,在被称为 leader schedule 的系统下,会指定一个 block or slot leader 在特定时间间隔内验证交易并提出区块。此时间表决定了特定时间点的区块生产者是谁。
然而,目前在 Solana 程序中并没有具体的方法来访问 block leader 的地址。
blockhash
我们包含这一节是为了内容的完整性,但它很快就会被弃用。不感兴趣的读者可以跳过此部分,不会有任何影响。
Solana 有一个 RecentBlockhashes sysvar,它保存着活跃的近期区块哈希及其相关的费用计算器。然而,这个 sysvar 已经被 deprecated(弃用),并且在未来的 Solana 版本中不再受到支持。与 Clock sysvar 不同,RecentBlockhashes sysvar 没有提供 get 方法。不过,缺乏此方法的 sysvar 可以通过 sysvar_name::from_account_info 来进行访问。
我们还将介绍一些新语法,这些语法将在以后进行解释。现在,请将它视作样板代码:
#[derive(Accounts)]
pub struct Initialize<'info> {
/// CHECK: readonly
pub recent_blockhashes: AccountInfo<'info>,
}
以下是我们如何在 Solana 中获取最新的区块哈希:
use anchor_lang::{prelude::*, solana_program::sysvar::recent_blockhashes::RecentBlockhashes};
// replace program id
declare_id!("H52ppiSyiZyYVn1Yr9DgeUKeChktUiPwDfuuo932Uqxy");
#[program]
pub mod sysvar {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// RECENT BLOCK HASHES
let arr = [ctx.accounts.recent_blockhashes.clone()];
let accounts_iter = &mut arr.iter();
let sh_sysvar_info = next_account_info(accounts_iter)?;
let recent_blockhashes = RecentBlockhashes::from_account_info(sh_sysvar_info)?;
let data = recent_blockhashes.last().unwrap();
msg!("The recent block hash is: {:?}", data.blockhash);
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
/// CHECK: readonly
pub recent_blockhashes: AccountInfo<'info>,
}
测试文件:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Sysvar } from "../target/types/sysvar";
describe("sysvar", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Sysvar as Program<Sysvar>;
it("Is initialized!", async () => {
// Add your test here.
const tx = await program.methods
.initialize()
.accounts({
recentBlockhashes: anchor.web3.SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
})
.rpc();
console.log("Transaction hash:", tx);
});
});
我们运行测试并获得以下日志:
Log Messages:
Program H52ppiSyiZyYVn1Yr9DgeUKeChktUiPwDfuuo932Uqxy invoke [1]
Program log: Instruction: Initialize
Program log: The recent block hash is: erVQHJdJ11oL4igkQwDnv7oPZoxt88j7u8DCwHkVFnC
Program H52ppiSyiZyYVn1Yr9DgeUKeChktUiPwDfuuo932Uqxy consumed 46181 of 200000 compute units
Program H52ppiSyiZyYVn1Yr9DgeUKeChktUiPwDfuuo932Uqxy success
我们可以看到最新的区块哈希。请注意,因为我们是部署到本地节点,所以获取到的区块哈希也是本地节点的,而不是 Solana 主网的。
在时间结构方面,Solana 运行在一个固定时间轴上,该时间轴被划分为多个 slot,每个 slot 是分配给 leader 用以提出区块的时间段。这些 slot 进一步被组织成 epoch,即 leader schedule 保持不变的预定时间段。
block.gaslimit
Solana 每个区块的计算单元 (compute unit) 上限为 4800 万。每笔交易默认限制为 200,000 个计算单元,尽管可以提高到 140 万个计算单元(我们将在后续教程中讨论这一点,不过你可以 在这里查看示例)。
我们无法在 Rust 程序中访问此限制。
block.basefee
在 Ethereum 中,根据 EIP-1559 协议,basefee 是动态的;它是基于前一个区块利用率的函数。在 Solana 中,交易的基础价格是静态的,所以不需要这样的变量。
block.difficulty
区块难度 (Block difficulty) 是一个与工作量证明 (PoW) 区块链相关的概念。另一方面,Solana 运行在历史证明 (PoH) 结合权益证明 (PoS) 的共识机制上,这不涉及区块难度的概念。
block.chainid
Solana 没有 chain id,因为它不是一个兼容 EVM 的区块链。block.chainid 是 Solidity 智能合约用来了解自己究竟处于 testnet、L2、mainnet 还是其他 EVM 兼容链的方法。
Solana 为 Devnet、Testnet 和 Mainnet 运行独立的集群,但程序没有机制可以知道自己身处哪一个集群中。但是,你可以使用 Rust cfg 功能在部署时以编程方式调整代码,从而根据部署到的集群加载不同的功能。这是一个 根据集群更改 program id 的示例。
了解更多
本教程是我们免费 Solana course 的一部分。
原载于 2024 年 2 月 18 日