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


Solana基础 - Solana时钟及其他“区块”变量


文章详细介绍了 Solidity 中的块变量在 Solana 中的类比实现,包括 block.timestamp、block.number、block.coinbase 等,并提供了具体的代码示例和说明。
今天我们将覆盖 Solidity 中所有区块变量的类比。并不是所有的变量都有一一对应的类比。在 Solidity 中,我们有以下常用的区块变量:

  • block.timestamp
  • block.number
  • blockhash()

以及一些较少使用的变量:

  • block.coinbase
  • block.basefee
  • block.chainid
  • block.difficulty / block.prevrandao

我们假设你已经知道它们的作用,但如果你需要复习,可以查看 Solidity 全局变量文档

Solana 中的 block.timestamp

通过利用 Clock sysvar 中的 unix_timestamp 字段,我们可以访问 Solana 的区块时间戳。

首先,我们初始化一个新的 Anchor 项目:

anchor init sysvar

将初始化函数替换为以下内容:

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    let clock: Clock = Clock::get()?;
    msg!(
        "区块时间戳: {}",
        // 获取 block.timestamp
        clock.unix_timestamp,
    );
    Ok(())
}

Anchor 的预导入模块包含 Clock 结构,默认情况下会自动导入:

use anchor_lang::prelude::*;

有些令人困惑的是,unix_timestamp 返回的类型是 i64,而不是 u64,这意味着它支持负数,尽管时间不能为负数。然而,时间间隔可以是负数。

获取星期几

现在让我们创建一个程序,使用 Clock sysvar 中的 unix_timestamp 告诉我们当前是星期几。

chrono crate 提供了 Rust 中的日期和时间操作功能。

在程序目录 ./sysvar/Cargo.toml 中将 chrono crate 添加为依赖项:

[dependencies]
chrono = "0.4.31"

sysvar 模块中导入 chrono crate:

// ...其他代码

#[program]
pub mod sysvar {
    use super::*;
    use chrono::*;  // 新增这一行

    // ...
}

现在,我们在程序下方添加这个函数:

pub fn get_day_of_the_week(
    _ctx: Context<Initialize>) -> Result<()> {

    let clock = Clock::get()?;
    let time_stamp = clock.unix_timestamp; // 当前时间戳

    let date_time = chrono::NaiveDateTime::from_timestamp_opt(time_stamp, 0).unwrap();
    let day_of_the_week = date_time.weekday();

    msg!("星期几是: {}", day_of_the_week);

    Ok(())
}

我们将从 Clock sysvar 获得的当前 unix 时间戳作为参数传递给 from_timestamp_opt 函数,返回一个包含日期和时间的 NaiveDateTime 结构。然后,我们调用星期几方法以根据传递的时间戳获取当前星期几。

并更新我们的测试:

it("获取星期几", async () => {
    const tx = await program.methods.getDayOfTheWeek().rpc();
    console.log("你的交易签名", tx);
});

再次运行测试并获取以下日志:

在插槽 36 中执行交易:
  签名: 5HVAjmo85Yi3yeQX5t6fNorU1da4H1zvgcJN7BaiPGnRwQhjbKd5YHsVE8bppU9Bg2toF4iVBvhbwkAtMo4NJm7V
  状态: Ok
  日志信息:
    程序 H52ppiSyiZyYVn1Yr9DgeUKeChktUiPwDfuuo932Uqxy 调用 [1]
    程序日志: 指令: GetDayOfTheWeek
    程序日志: 星期几是: Wed
    程序 H52ppiSyiZyYVn1Yr9DgeUKeChktUiPwDfuuo932Uqxy 消耗了 1597 个计算单位(总计 200000 个计算单位)

注意日志中的“星期几是: Wed”。

Solana 中的 block.number

Solana 有一个“插槽编号”的概念,和“区块编号”很类似,但并不相同。关于这二者的区别将在以下教程中讨论,因此我们暂时推迟关于如何获取“区块编号”的完整讨论。

block.coinbase

在以太坊中,“块 coinbase”代表已成功挖掘一个区块的矿工的地址,而 Solana 使用一种基于领导者的共识机制,它结合了历史证明(PoH)和权益证明(PoS),取消了挖矿的概念。相反,一名 区块或插槽领导者 被任命来验证交易并在某些时间间隔内提出区块,依据一种被称为 领导者计划 的系统。该计划确定了在特定时间内的区块生产者。

然而,目前没有特定的方法可以在 Solana 程序中访问区块领导者的地址。

blockhash

我们包括这一节以保持完整性,但这将很快被弃用。

对于不感兴趣的读者,可以跳过这一部分而没有后果。

Solana 有一个 RecentBlockhashes sysvar,它保存活动的最近区块哈希及其相关的费用计算器。然而,这个 sysvar 已被 弃用,并将在未来的 Solana 版本中不再支持。RecentBlockhashes sysvar 没有像 Clock sysvar 那样提供获取方法。然而,缺少此方法的 sysvar 可以使用 sysvar_name::from_account_info 访问。

我们还将介绍一些新语法,这将在稍后说明。现在,请将其视为模板:

#[derive(Accounts)]
pub struct Initialize<'info> {
    /// CHECK: 只读
    pub recent_blockhashes: AccountInfo<'info>,
}

以下是我们如何在 Solana 获取最新的区块哈希:

use anchor_lang::{prelude::*, solana_program::sysvar::recent_blockhashes::RecentBlockhashes};

// 替换程序 ID
declare_id!("H52ppiSyiZyYVn1Yr9DgeUKeChktUiPwDfuuo932Uqxy");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        // 最近区块哈希
        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!("最近区块哈希是: {:?}", data.blockhash);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    /// CHECK: 只读
    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", () => {
  // 配置客户端以使用本地集群。
  anchor.setProvider(anchor.AnchorProvider.env());

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

  it("已初始化!", async () => {
    // 在这里添加你的测试。
    const tx = await program.methods
      .initialize()
      .accounts({
        recentBlockhashes: anchor.web3.SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
      })
      .rpc();

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

我们运行测试并获得以下日志:

日志信息:
  程序 H52ppiSyiZyYVn1Yr9DgeUKeChktUiPwDfuuo932Uqxy 调用 [1]
  程序日志: 指令: Initialize
  程序日志: 最近区块哈希是: erVQHJdJ11oL4igkQwDnv7oPZoxt88j7u8DCwHkVFnC
  程序 H52ppiSyiZyYVn1Yr9DgeUKeChktUiPwDfuuo932Uqxy 消耗了 46181 个计算单位(总计 200000 个计算单位)
  程序 H52ppiSyiZyYVn1Yr9DgeUKeChktUiPwDfuuo932Uqxy 成功

我们可以看到最新的区块哈希。请注意,由于我们正在部署到本地节点,因此我们获得的区块哈希是本地节点的,而不是 Solana 主网络的。

在时间结构方面,Solana 在固定时间线中运作,划分为插槽,每个插槽是分配给领导者提出区块的时间部分。这些插槽进一步组织为纪元,在这些预定义期间内,领导者计划保持不变。

block.gaslimit

Solana 每区块计算单位 限制为 48000000。每个交易默认限制为 200,000 计算单位,尽管可以提高到 1.4 million 计算单位(我们将在后续教程中讨论这一点,但你可以 在这里查看示例)。

从 Rust 程序中无法访问此限制。

block.basefee

在以太坊中,basefee 是动态的,根据 EIP-1559 的规定;它是前一个区块利用率的函数。而在 Solana 中,交易的基本价格是静态的,因此不需要这样的变量。

block.difficulty

区块难度是与工作量证明(PoW)区块链相关的概念。而 Solana 基于历史证明(PoH)与权益证明(PoS)共识机制运作,这不涉及区块难度的概念。

block.chainid

Solana 没有链 ID,因为它不是 EVM 兼容的区块链。block.chainid 是 Solidity 智能合约知道它们在测试网、L2、主网或其他 EVM 兼容链上的方式。

Solana 为 Devnet、Testnet 和 Mainnet 运行独立集群,但程序没有机制来知道它们所处的哪个集群。但你可以在部署时使用 Rust cfg 功能根据各个集群调整程序代码。以下是 根据集群更改程序 ID 的示例

转载:lockchain.cn/article/11405


Solana基础 - Rust 和 Solana 中的可见性与“继承”


本文详细讲解了如何将Solidity中的函数可见性和合约继承概念化到Solana中,并提供了Rust语言中实现这些概念的代码示例。

今天我们将学习如何在 Solana 中构思 Solidity 的函数可见性和合约继承。Solidity 中有四种函数可见性级别,它们是:

  • public – 从合约内部和外部均可访问。
  • external – 从合约外部仅可访问。
  • internal – 从合约内部和继承合约中可访问。
  • private – 仅在合约内部可访问。

让我们在 Solana 中实现相同的功能,好吗?

公共函数

自第一天起到现在我们定义的所有函数都是公共函数:

pub fn my_public_function(ctx: Context<Initialize>) -> Result<()> {
    // 函数逻辑...

    Ok(())
}

在函数声明前添加 pub 关键字,从而使函数变为公共。

你不能移除标记为 #[program] 的模块 (mod) 内部的函数的 pub 关键字。这将导致无法编译。

不用太担心 external 和 public 之间的区别

在 Solana 程序中,调用自己公共函数通常是不方便的。如果 Solana 程序中存在 pub 函数,从实际角度来看,你可以将其视作为在 Solidity 中的外部函数。

如果你想在同一个 Solana 程序中调用公共函数,包裹这个公共函数并调用该内部实现函数要更容易。

私有和内部函数

虽然你不能在使用 #[program] 宏的模块中声明没有 pub 的函数,但你可以在文件内部声明函数。考虑以下代码:

use anchor_lang::prelude::*;

declare_id!("F26bvRaY1ut3TD1NhrXMsKHpssxF2PAUQ7SjZtnrLkaM");

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

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        // -------- 调用一个“私有”函数 --------
        let u = get_a_num();
        msg!("{}", u);
        Ok(())
    }
}

// ------- 我们在此声明了一个非 pub 函数 -------
fn get_a_num() -> u64 {
    2
}

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

这将按预期运行并记录。

如果你想构建简单的 Solana 程序,这些是你真正需要了解的有关公共和内部函数的所有内容。但如果你希望比仅仅在文件中声明一堆函数更好地组织代码,你可以继续深入。Rust,因此 Solana,没有像 Solidity 那样的“类”,因为 Rust 不是面向对象的。因此,“私有”和“内部”之间的区别在 Rust 中没有直接类比。

Rust 使用模块来组织代码。模块内外函数的可见性在Rust 文档的可见性和隐私部分中有很好的讨论,但我们将添加自己针对 Solana 的见解。

内部函数

通过在程序模块内定义函数并确保其在其自己的模块及导入或使用它的其他模块内可访问来实现。这是怎么做的:

use anchor_lang::prelude::*;

declare_id!("53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX");

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

    pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
        // 从其父模块内部调用 internal_function
        some_internal_function::internal_function();

        Ok(())
    }

    pub mod some_internal_function {
        pub fn internal_function() {
            // 内部函数逻辑...
        }
    }
}

mod do_something {
    // 导入 func_visibility 模块
    use crate::func_visibility;

    pub fn some_func_here() {
        // 从外部其父模块调用 internal_function
        func_visibility::some_internal_function::internal_function();

        // 做其他事情...
    }
}

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

构建程序后,如果导航到 ./target/idl/func_visibility.json 文件,你将看到定义在 some_internal_function 模块中的函数没有被包含在构建程序中。这表明 some_internal_function 函数是内部的,仅能在程序内部及任何导入或使用它的程序中访问。

从上面的例子中,我们能够从其“父”模块(func_visibility)内部访问 internal_function 函数,也能从 func_visibility 模块外的单独模块中访问(do_something)。

私有函数

在特定模块内定义函数并确保它们不会暴露在该作用域之外就是实现私有可见性的一种方式:

use anchor_lang::prelude::*;

declare_id!("53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX");

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

    pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
        // 从其父模块内部调用 private_function
        some_function_function::private_function();

        Ok(())
    }

    pub mod some_function_function {
        pub(in crate::func_visibility) fn private_function() {
            // 私有函数逻辑...
        }
    }
}

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

pub(in crate::func_visibility) 关键字表明 private_function 函数仅在 func_visibility 模块中可见。

我们能够成功在初始化函数中调用 private_function,因为初始化函数处于 func_visibility 模块内。让我们尝试从模块外部调用 private_function

use anchor_lang::prelude::*;

declare_id!("53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX");

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

    pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
        // 从其父模块内部调用 private_function
        some_private_function::private_function();

        Ok(())
    }

    pub mod some_private_function {
        pub(in crate::func_visibility) fn private_function() {
            // 私有函数逻辑...
        }
    }
}

mod do_something {
    // 导入 func_visibility 模块
    use crate::func_visibility;

    pub fn some_func_here() {
        // 从外部其父模块调用 private_function
        func_visibility::some_private_function::private_function()

        // 做一些事情...
    }
}

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

构建程序。发生了什么?我们得到了一个错误:

error[E0624]: associated functionprivate_function is private

这表明 private_function 不是公开可访问的,无法从其可见的模块外部调用。查看Rust 文档中的可见性和隐私以了解更多关于 pub 可见性关键字的信息。

合约继承

将 Solidity 合约继承直接翻译为 Solana 是不可能的,因为 Rust 没有类。

然而,Rust 中的解决方法涉及创建定义特定功能的单独模块,然后在我们的主程序中使用这些模块,从而实现类似于 Solidity 的合约继承的效果。

从另一个文件获取模块

随着程序变大,我们通常不希望将所有内容放入一个文件中。以下是如何将逻辑组织到多个文件中的示例。

src 文件夹下创建一个叫 calculate.rs 的文件,并将提供的代码复制到其中。

pub fn add(x: u64, y: u64) -> u64 {
    // 返回 x 和 y 的和
    x + y
}

这个 add 函数返回 xy 的和。

以及将其放入 lib.rs 中。

use anchor_lang::prelude::*;

// 导入 `calculate` 模块或库
pub mod calculate;

declare_id!("53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX");

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

    pub fn add_two_numbers(_ctx: Context<Initialize>, x: u64, y: u64) -> Result<()> {
        // 调用 calculate.rs 中的 `add` 函数
        let result = calculate::add(x, y);

        msg!("{} + {} = {}", x, y, result);
        Ok(())
    }
}

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

在上面的程序中,我们导入了之前创建的 calculate 模块,并声明了一个名为 add_two_numbers 的函数,该函数添加两个数字并记录结果。add_two_numbers 函数调用 calculate 模块中的 add 函数,将 xy 作为参数传递,然后将返回值存储在 result 变量中。msg! 宏记录了两个相加的数字和结果。

模块不必是单独的文件

以下示例在 lib.rs 中声明一个模块,而不是在 calculate.rs 中。

use anchor_lang::prelude::*;

declare_id!("53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX");

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

    pub fn add_two_numbers(_ctx: Context<Initialize>, x: u64, y: u64) -> Result<()> {
        // 调用 calculate.rs 中的 `add` 函数
        let result = calculate::add(x, y);

        msg!("{} + {} = {}", x, y, result);

        Ok(())
    }
}

mod calculate {
    pub fn add(x: u64, y: u64) -> u64 {
        // 返回 x 和 y 的总和
        x + y
    }
}

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

这个程序与前一个示例的功能相同,唯一的区别是 add 函数出现在 lib.rs 文件和计算模块内。此外,向函数添加 pub 关键字非常重要,因为这使函数对外公开可访问。以下代码将无法编译:

use anchor_lang::prelude::*;

declare_id!("53hgft52DHUKMPHGu1kusuwxFGk2T8qngwSw2SyGRNrX");

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

    pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
        // 调用私有类函数
        let result2 = do_something::some_func_here();

        msg!("The result is {}", result2);

        Ok(())
    }
}

mod do_something {
    // 私有类函数。它存在于代码中,但并不是每个人都可以调用它
    fn some_func_here() -> u64 {
        // 做一些事情...

        return 20;
    }
}

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

总结

在 Solidity 中,我们非常重视函数可见性,因为它非常关键。以下是我们在 Rust 中的思考方式:

  • 公共/外部函数:这些是可以在程序内部和外部访问的函数。在 Solana 中,所有声明的函数默认都是公共的。#[program] 块中的所有内容必须声明为 pub。
  • 内部函数:这些是可在程序内部及继承该程序的程序中访问的函数。在嵌套的 pub mod 块内的函数不会包含在构建程序中,但仍然可以在父模块内或外访问。
  • 私有函数:这些是不能公开访问的函数,无法从其所在模块外部调用。在 Rust/Solana 中实现私有可见性的方法是定义一个位于特定模块内的函数,使用 pub(in crate::<module>) 关键字,使得函数仅在定义它的模块内可见。

Solidity 通过类实现合约继承,而 Rust,Solana 使用的语言,并没有这个特性。尽管如此,你仍然可以使用 Rust 模块来组织代码。

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


Solana基础 - Rust 结构体与属性式和自定义派生宏


文章详细介绍了 Rust 语言中的 attribute-like 和 custom derive 宏的使用方法,通过具体代码示例展示了如何通过宏在编译时修改结构体,并解释了宏的工作原理和实现方式。
Rust 中的类似属性和自定义派生宏用于在编译时以某种方式修改一段 Rust 代码,通常是为了增加功能。

要理解 Rust 中的类似属性和自定义派生宏,我们首先需要简要介绍 Rust 中的实现结构。

结构的实现:impl

以下结构应该是相当容易理解的。当我们创建对特定结构进行操作的函数时,事情就变得有趣了。我们这样做的方法是使用 impl

struct Person {
    name: String,
    age: u8,
}

关联函数和方法是在 impl 块中为结构实现的。

关联函数可以与 Solidity 中为与结构交互创建库的场景进行比较。当我们定义 using lib for MyStruct 时,它允许我们使用语法 myStruct.associatedFunction()。这使得该函数可以通过 Self 关键字访问 myStruct

我们建议使用 Rust Playground,但对于更复杂的示例,你可能需要设置你的 IDE。

让我们看一个下面的示例:

struct Person {
    age: u8,
    name: String,
}

// 为 `Person` 结构实现方法 `new()`,允许初始化一个 `Person` 实例
impl Person {
    // 使用提供的 `name` 和 `age` 创建一个新的 `Person`
    fn new(name: String, age: u8) -> Self {
        Person { name, age }
    }

    fn can_drink(&self) -> bool {
        if self.age >= 21 as u8 {
            return true;
        }
        return false;
    }

    fn age_in_one_year(&self) -> u8 {
        return &self.age + 1;
    }
} 

fn main() {
    // 用法:创建一个带有名字和年龄的新 `Person` 实例
    let person = Person::new(String::from("Jesserc"), 19);

    // 使用一些实现函数
    println!("{:?}", person.can_drink()); // false
    println!("{:?}", person.age_in_one_year()); // 20
    println!("{:?}", person.name);
}

用法:

// 用法:创建一个带有名字和年龄的新 `Person` 实例
let person = Person::new(String::from("Jesserc"), 19);

// 使用一些实现函数
person.can_drink(); // false
person.age_in_one_year(); // 20

Rust Traits

Rust traits 是在不同的 impl 之间实现共享行为的一种方式。可以将它们视为 Solidity 中的接口或抽象合约——任何使用该接口的合约必须实现某些函数。

例如,假设我们有一个需要定义汽车和船的结构的场景。我们想附加一个方法,允许我们以每小时公里数检索它们的速度。在 Rust 中,我们可以通过使用单个 trait 并在两个结构之间共享该方法来实现这个目标。

如下所示:

// traits 用 `trait` 关键字定义,后跟其名称
trait Speed {
    fn get_speed_kph(&self) -> f64;
}

// 汽车结构
struct Car {
    speed_mph: f64,
}

// 船结构
struct Boat {
    speed_knots: f64,
}

// 使用 `impl` 关键字为类型实现 traits,如下所示
impl Speed for Car {
    fn get_speed_kph(&self) -> f64 {
        // 将英里每小时转换为公里每小时
        self.speed_mph * 1.60934
    }
}

// 我们也为 `Boat` 实现 `Speed` trait
impl Speed for Boat {
    fn get_speed_kph(&self) -> f64 {
        // 将节转换为公里每小时
        self.speed_knots * 1.852
    }
}

fn main() {
    // 初始化一个 `Car` 和 `Boat` 类型
    let car = Car { speed_mph: 60.0 };
    let boat = Boat { speed_knots: 30.0 };

    // 获取并打印以公里每小时为单位的速度
    let car_speed_kph = car.get_speed_kph();
    let boat_speed_kph = boat.get_speed_kph();

    println!("Car Speed: {} km/h", car_speed_kph); // 96.5604 km/h
    println!("Boat Speed: {} km/h", boat_speed_kph); // 55.56 km/h
}

宏如何修改结构

在我们关于类似函数的宏的教程中,我们看到了宏如何扩展代码,比如 println!(...)msg!(...) 在大型 Rust 代码中的用法。在 Solana 的上下文中,我们关心的另一种宏是 attribute-like 宏和 derive 宏。我们可以在 anchor 创建的启动程序中看到这三种宏(函数式、类似属性和派生):

Rust attribute and custom-derive macros

为了直观了解类似属性的宏做了什么,我们将创建两个宏:一个将字段添加到结构中,另一个将其删除。

示例 1:类似属性的宏,插入字段

为了更好地理解 Rust 的属性和宏如何工作,我们将创建一个 attribute-like macro,其功能为:

  1. 处理一个不包含 foobar 字段的结构,字段类型为 i32
  2. 将这些字段插入到结构中
  3. 创建一个包含称为 double_foo 的函数的 impl,该函数返回 foo 字段所持的整数值的两倍。

设置

首先我们创建一个新的 Rust 项目:

cargo new macro-demo --lib 
cd macro-demo
touch src/main.rs

在 Cargo.toml 文件中添加以下内容:

[lib]
proc-macro = true

[dependencies]
syn = {version="1.0.57",features=["full","fold"]}
quote = "1.0.8"

创建主程序

将以下代码粘贴到 src/main.rs 文件中。请确保阅读注释:

// src/main.rs
// 导入 macro_demo crate 并用 `*` 通配符引入所有项
// (实际上是此 crate 中的所有内容,包括我们在 `src/lib.rs` 中的宏)
use macro_demo::*;

// 将我们在 `src/lib.rs` 中创建的 `foo_bar_attribute` 过程属性宏应用于 `struct MyStruct`
// 该过程宏将生成一个带有指定字段和方法的新结构定义
#[foo_bar_attribute]
struct MyStruct {
    baz: i32,
}

fn main() {
    // 使用 `default()` 方法创建 `MyStruct` 的新实例
    // 此方法由宏生成的 `Default` trait 实现提供
    let demo = MyStruct::default();

    // 将 `demo` 的内容打印到控制台
    // 宏生成的 `Debug` trait 实现允许使用 `println!` 进行格式化输出
    println!("struct is {:?}", demo);

    // 在 `demo` 上调用 `double_foo()` 方法
    // 此方法由宏生成,返回 `foo` 字段值的两倍
    let double_foo = demo.double_foo();

    // 将调用 `double_foo` 的结果打印到控制台
    println!("double foo: {}", double_foo);
}

一些观察:

  • 结构 MyStruct 中没有字段 foo
  • double_foo 函数没有在上面的代码中定义,假设它存在。

现在让我们创建将修改 MyStruct 的类似属性的宏。

将 src/lib.rs 中的代码替换为以下代码(请确保阅读注释):

// src/lib.rs
// 导入必要的外部库
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemStruct};

// 声明一个使用 `proc_macro_attribute` 指令的过程属性宏
// 这使得宏可以作为属性使用
#[proc_macro_attribute]
// `foo_bar_attribute` 函数接受两个参数:
// _metadata:提供给宏的参数(如果有)
// _input:宏所应用的 TokenStream
pub fn foo_bar_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {
    // 将输入 TokenStream 解析为表示结构的 AST 节点
    let input = parse_macro_input!(_input as ItemStruct);
    let struct_name = &input.ident; // 获取结构的名称

    // 使用 quote! 宏构建输出 TokenStream
    // quote! 宏允许将 Rust 代码写成字符串的方式,但可以插入值
    TokenStream::from(quote! {
        // 为 #struct_name 派生 Debug trait,以启用使用 `println()` 的格式化输出
        #[derive(Debug)]
        // 定义具有两个字段:foo 和 bar 的新结构 #struct_name
        struct #struct_name {
            foo: i32,
            bar: i32,
        }

        // 为 #struct_name 实现 Default trait
        // 这提供了一个 default() 方法,用于创建 #struct_name 的新实例
        impl Default for #struct_name {
            // 默认方法返回一个新的 #struct_name 实例,其中 foo 设置为 10,bar 设置为 20
            fn default() -> Self {
                #struct_name { foo: 10, bar: 20}
            }
        }

        impl #struct_name {
            // 为 #struct_name 定义一个方法 double_foo
            // 此方法返回 foo 的双倍值
            fn double_foo(&self) -> i32 {
                self.foo * 2
            }
        }
    })
}

现在,为了测试我们的宏,我们使用 cargo run src/main.rs 运行代码。

我们会得到以下输出:

struct is MyStruct { foo: 10, bar: 20 }
double foo: 20

示例 2:类似属性的宏,删除字段

关于类似属性的宏,可以认为它们在修改结构时具有无限的能力。让我们重复上面的示例,但这次类似属性的宏将删除结构中的所有字段。

将 src/lib.rs 替换为以下内容:

// src/lib.rs
// 导入必要的外部库
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemStruct};

#[proc_macro_attribute]
pub fn destroy_attribute(_metadata: TokenStream, _input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(_input as ItemStruct);
    let struct_name = &input.ident; // 获取结构的名称

    TokenStream::from(quote! {
        // 返回一个具有相同名称的空结构
        #[derive(Debug)]
        struct #struct_name {
        }
    })
}

将 src/main.rs 替换为以下内容:

use macro_demo::*;

#[destroy_attribute]
struct MyStruct {
    baz: i32,
    qux: i32,
}

fn main() {
    let demo = MyStruct { baz: 3, qux: 4 };

    println!("struct is {:?}", demo);
}

当你尝试使用 cargo run src/main.rs 编译时,会收到以下错误消息:

Error: struct MyStruct has no field named baz

这可能看起来很奇怪,因为结构显然有这些字段。然而,类似属性的宏删除了它们!

#[derive(…)]

#[derive(…)] 宏的功能远没有类似属性宏强大。对于我们的目的而言,派生宏 增强 了结构,而不是改变它。(这不是一个精确的定义,但现在足够了)。

派生宏可以,除了其他外,向结构附加一个 impl

例如,如果我们尝试做以下操作:

struct Foo {
    bar: i32,
}

pub fn main() {
    let foo = Foo { bar: 3 };
    println!("{:?}", foo);
}

编译时将不会通过,因为结构不可“打印”。

为了使它们可打印,需要有一个 impl,其中有一个函数 fmt,返回结构的字符串表示。

如果我们这样做:

#[derive(Debug)]
struct Foo {
    bar: i32,
}

pub fn main() {
    let foo = Foo { bar: 3 };
    println!("{:?}", foo);
}

我们期望它打印:

Foo { bar: 3 }

派生属性以某种方式“增强”了 Foo,使得 println! 可以为其创建字符串表示。

总结

impl 是一组对结构进行操作的函数。它们通过使用与结构相同的名称“附加”到结构上。trait 强制工具的 impl 实现某些函数。在我们的示例中,我们通过语法 impl Speed for Car 将 trait Speed 附加到 impl Car。

类似属性的宏接收一个结构,可以完全重写它。

派生宏增强了结构,使其具有附加功能。

宏允许 Anchor 隐藏复杂性

让我们再看看 Anchor 在 anchor init 时创建的程序:

Rust attribute and custom-derive macros

属性 #[program] 在幕后修改模块。例如,它实现了一个路由器,自动将传入的区块链指令定向到模块内的适当函数。

结构 Initialize {} 被增强了额外的功能,以便在 Solana 框架中使用。

总结

宏是一个非常大的主题。我们在这里的目的是让你了解当你看到 #[program]#[derive(Accounts)] 时发生了什么。不要因它感到陌生而沮丧。你不需要能够编写宏才能编写 Solana 程序

不过,了解它们的作用希望能让你看到的程序变得不那么神秘。
转载:https://learnblockchain.cn/article/11373


Solana基础 - Rust 函数式过程宏


本教程解释了函数与函数式宏之间的区别。例如,为什么 msg! 后面有一个感叹号?本教程将解释这种语法。作为一种强类型语言,Rust 不能接受函数的任意数量的参数。例如,Python 的 print 函数可以接受任意数量的参数:

print(1)
print(1, 2)
print(1, 2, 3)

! 表示“函数”是一个函数式宏。Rust 的函数式宏通过 ! 符号来标识,例如 Solana 中的 println!(...)msg!(...)。在 Rust 中,打印内容的常规函数(非函数式宏)是 std::io::stdout().write,它只接受一个字节字符串作为参数。如果你想运行以下代码,如果你不想设置开发环境,Rust Playground 是一个方便的工具。我们使用以下示例(取自这里):

use std::io::Write;

fn main() {
    std::io::stdout().write(b"Hello, world!
").unwrap();
}

请注意,write 是一个函数,而不是宏,因为它没有 !。如果你尝试像上面在 Python 中那样做,代码将无法编译,因为 write 只接受一个参数:

// 这段代码无法编译
use std::io::Write;

fn main() {
    std::io::stdout().write(b"1
").unwrap();
    std::io::stdout().write(b"1", b"2
").unwrap();
    std::io::stdout().write(b"1", b"2", b"3
").unwrap();
}

因此,如果你想打印任意数量的参数,你需要为每种参数数量编写一个自定义的 print 函数来处理每种情况 —— 这是非常低效的!以下是这样代码的样子(非常不推荐!):

use std::io::Write;

// 打印一个参数
fn print1(arg1: &[u8]) -> () {
    std::io::stdout().write(arg1).unwrap();
}

// 打印两个参数
fn print2(arg1: &[u8], arg2: &[u8]) -> () {
    let combined_vec = [arg1, b" ", arg2].concat();
    let combined_slice = combined_vec.as_slice();
    std::io::stdout().write(combined_slice).unwrap();
}

// 打印三个参数
fn print3(arg1: &[u8], arg2: &[u8], arg3: &[u8]) -> () {
    let combined_vec = [arg1, b" ", arg2, b" ", arg3].concat();
    let combined_slice = combined_vec.as_slice();
    std::io::stdout().write(combined_slice).unwrap();
}

fn main() {
    print1(b"1
");
    print2(b"1", b"2
");
    print3(b"1", b"2", b"3
");
}

如果我们观察 print1print2print3 函数中的模式,它只是将参数插入到向量中并在它们之间添加一个空格,然后将向量转换回字节字符串(准确说是字节切片)。如果我们能像 println! 这样编写一段代码,并自动将其扩展为一个 print 函数,接受我们所需的任意数量的参数,那岂不是很好?这就是 Rust 宏的作用。Rust 宏将 Rust 代码作为输入,并以编程方式将其扩展为更多的 Rust 代码。 这帮助我们避免了必须为代码所需的每种 print 语句编写 print 函数的无聊工作。

宏的扩展

要查看 Rust 编译器如何扩展 println! 宏的示例,请查看 cargo expand github 仓库。结果相当冗长,所以我们不在这里展示。

将宏视为黑盒子是可以的

当宏由库提供时,它们非常方便,但手动编写宏非常繁琐,因为它需要字面解析 Rust 代码。

Rust 中不同类型的宏

我们给出的 println! 示例是一个函数式宏。Rust 还有其他类型的宏,但我们关心的另外两种是自定义派生宏属性式宏。让我们看看 anchor 创建的新程序:不同类型的宏我们将在接下来的教程中解释它们的工作原理。

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