Solana 60 天课程

2025年02月27日更新 89 人订阅
原价: ¥ 66 限时优惠
专栏简介 开始 Solana - 安装与故障排除 Solana 和 Rust 中的算术与基本类型 Solana Anchor 程序 IDL Solana中的Require、Revert和自定义错误 Solana程序是可升级的,并且没有构造函数 Solidity开发者的Rust基础 Rust不寻常的语法 Rust 函数式过程宏 Rust 结构体与属性式和自定义派生宏 Rust 和 Solana 中的可见性与“继承” Solana时钟及其他“区块”变量 Solana 系统变量详解 Solana 日志、“事件”与交易历史 Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者 Solana 计算单元与交易费用介绍 在 Solana 和 Anchor 中初始化账户 Solana 计数器教程:在账户中读写数据 使用 Solana web3 js 和 Anchor 读取账户数据 在Solana中创建“映射”和“嵌套映射” Solana中的存储成本、最大存储容量和账户调整 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因 在 Solana 上实现 SOL 转账及构建支付分配器 使用不同签名者修改账户 PDA(程序派生地址)与 Solana 中的密钥对账户 理解 Solana 中的账户所有权:从PDA中转移SOL Anchor 中的 Init if needed 与重初始化攻击 Solana 中的多重调用:批量交易与交易大小限制 Solana 中的所有者与权限 在Solana中删除和关闭账户与程序 在 Anchor 中:不同类型的账户 在链上读取另一个锚点程序账户数据 在 Anchor 中的跨程序调用(CPI) SPL Token 的运作方式 使用 Anchor 和 Web3.js 转移 SPL Token Solana 教程 - 如何实现 Token 出售 基础银行教程 Metaplex Token 元数据工作原理 使用Metaplex实施代币元数据 使用 LiteSVM 进行时间旅行测试 Solana Token-2022 标准规范 生息代币第一部分 计息代币第二部分 Solana 指令自省 Solana 中的 Ed25519 签名验证 Solana - Switchboard 预言机使用 原生Solana:程序入口与执行 原生 Solana :读取账户数据 原生 Solana :Borsh 序列化 原生 Solana:使用 invoke 和 invoke signed 进行跨程序调用 原生 Solana :创建存储账户 (一) 原生 Solana:创建存储账户 二 原生 Solana: 函数分发 原生 Solana:关键安全检查 Rust 程序到 SBF 编译 sBPF 虚拟机和指令集介绍 跟踪 sBPF 指令执行和计算成本 Solana 程序执行与输入序列化 指令处理器和运行时设置 sBPF 内存布局和寄存器约定 使用 sBPF 汇编读取 Solana 指令输入 Solana 系统调用:sBPF 汇编中的日志记录

原生 Solana:使用 invoke 和 invoke signed 进行跨程序调用

本文详细介绍了Solana区块链上的跨程序调用(CPI),重点讲解了invokeinvoke_signed两个核心函数及其在原生Rust中的使用。文章还阐述了Anchor框架如何抽象这些函数,并通过一个包含目标程序和调用程序的实际案例,展示了如何在程序间传递数据。

原生 Solana:使用 invoke 和 invoke_signed 进行跨程序调用

跨程序调用 (CPI) 是程序在 Solana 区块链上调用其他程序的方式。在本教程中,我们将学习如何在原生 Rust 中进行 CPI 调用。

在之前的 Anchor 教程中,我们已经使用过 CPI,例如通过 SPL Token 程序转移 SOL 或铸造代币。在 Anchor 中,CPI 调用看起来是这样的:

Copylet cpi_ctx = CpiContext::new(
    ctx.accounts.system_program.to_account_info(),
    system_program::Transfer {
        from: ctx.accounts.from.to_account_info(),
        to: ctx.accounts.to.to_account_info(),
    }
);
system_program::transfer(cpi_ctx, amount)?;

上面的代码创建了一个 CPI 上下文,其中包含系统程序和转账所需的账户,然后调用 system_program::transfer 来执行 CPI。

本教程解释了在 Anchor 中进行 CPI 调用时幕后发生的事情,然后展示了如何直接使用 Solana 的原生 CPI 函数。

我们将涵盖:

  • Solana 中两个核心 CPI 函数:invokeinvoke_signed
  • Anchor 如何抽象这些核心函数
  • 如何在原生 Rust 中手动构建 CPI 指令,通过一个实际示例,我们将构建两个程序:一个返回 42 的目标程序和一个通过 CPI 调用它的调用程序

让我们首先了解 invokeinvoke_signed CPI 函数。

了解 Solana 的核心 CPI 函数

Solana 有两个用于进行跨程序调用的核心函数:

  1. invoke:用于不需要 PDA 签名的 CPI 调用(使用原始交易签名者)
  2. invoke_signed:用于需要 PDA 签名的 CPI 调用(当程序需要代表其控制的 PDA 进行签名时)

让我们详细了解这些函数:

1. invoke 函数

invoke 函数使用账户和指令数据调用另一个程序。当你的程序需要使用原始交易签名者进行授权来调用另一个程序时,你会使用它。

Copypub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>],
) -> ProgramResult

参数如下:

  • instruction:一个 Instruction 结构体,包含:
    • program_id:目标程序的公钥
    • accounts:一个 AccountMeta 结构体向量。每个结构体包含三个字段:pubkey(账户的公钥),is_signer(此账户是否必须签署交易),以及 is_writable(程序是否可以修改此账户)
    • data:一个包含指令数据的字节数组。通常包括一个鉴别器(用于识别要执行的指令),后跟指令预期的任何参数。确切的布局由目标程序定义
  • account_infos:一个 AccountInfo 结构体切片。这必须包括指令 accounts 字段中引用的所有账户,以及目标程序的账户。运行时使用这些来在执行期间访问实际账户数据

指令中的 AccountMeta 告诉 Solana 你需要哪些账户以及它们将如何使用。AccountInfo 提供你的程序读取或写入的实际账户数据和状态。

2. invoke_signed 函数

invoke_signed 函数使用账户和指令数据调用另一个程序,就像 invoke 一样,但用于你的程序必须代表 PDA 进行签名的情况。它的工作原理如下:

  1. 使用 invoke_signed 时,你必须提供用于派生 PDA 的种子
  2. 运行时使用这些种子来验证你的程序派生了 PDA(即,PDA 属于你的程序)
  3. 这允许你的程序代表 PDA 进行签名,因为 PDA 没有私钥,不能直接签名
Copypub fn invoke_signed(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>],
    signers_seeds: &[&[&[u8]]],
) -> ProgramResult

此函数与 invoke 具有相同的参数,但多了一个参数:

  • signers_seeds:需要签署 CPI 指令的 PDA 的派生种子。运行时使用这些种子重新派生 PDA 并验证它属于你的程序。

现在我们了解了 Solana 提供的原生 CPI 函数,接下来让我们看看 Anchor 是如何使用它们的。

Anchor 如何抽象 Solana 的 CPI 函数

Anchor 通过提供两种封装 invokeinvoke_signed 函数的方法,降低了构建 CPI 调用的复杂性:

1. 常规 CPI 调用(使用原始交易签名者):

当被调用程序所需的账户已经是原始交易中的签名者时,Anchor 使用 CpiContext::new() 来封装原生 invoke 函数。以下是通过系统程序转移 SOL 的示例:

Copylet cpi_ctx = CpiContext::new(
    ctx.accounts.system_program.to_account_info(),
    system_program::Transfer {
        from: ctx.accounts.from.to_account_info(),
        to: ctx.accounts.to.to_account_info(),
    }
);
system_program::transfer(cpi_ctx, amount)?;

2. PDA 签名 CPI 调用(当你的程序需要代表 PDA 签名时):

当你的程序控制的 PDA 必须签署被调用的指令时,Anchor 使用 CpiContext::new_with_signer() 来封装原生 invoke_signed 函数。第三个参数 (&[&seeds]) 提供了用于派生和签署 PDA 的种子:

Copylet seeds = &[\
    b"seed-prefix",\
    payer.key.as_ref(),\
    &[bump],\
];

let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.token_program.to_account_info(),
    token::Transfer {
        from: ctx.accounts.from.to_account_info(),
        to: ctx.accounts.to.to_account_info(),
        authority: ctx.accounts.authority.to_account_info(),
    },
    &[&seeds],
);
token::transfer(cpi_ctx, amount)?;

通过这两种方法构建指令上下文后,你调用相应的 Anchor CPI 辅助函数(例如 system_program::transfertoken::transfer)并传入上下文。

在幕后,Anchor 对所有 CPI 调用都使用 invoke_signed。这是因为:

  • 如果没有签名者种子,它的工作方式与 invoke 完全相同
  • 有了签名者种子,它就可以实现 PDA 签名

这种统一的方法意味着 Anchor 只需要一个代码路径来处理所有 CPI 操作。

invoke 函数以这种方式工作,因为它的实现使用了 invoke_signed 函数,但为 PDA 签名者种子传递了一个空字节切片。

Copypub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {
    invoke_signed(instruction, account_infos, &[])
}

在 Solana 的 BPF 运行时内部,invokeinvoke_signed 都调用 sol_invoke_signed_rust 系统调用。此系统调用通过暂停调用者执行、调用目标程序、管理调用堆栈以及在提供签名者种子时验证 PDA 派生签名来执行实际的跨程序调用。还有一个 C 语言 ABI 变体 sol_invoke_signed_c,它为用 C 编程语言编写的程序公开了相同的行为。

要了解 Anchor 如何使用 invoke_signed,让我们检查 Anchor 源代码中的 system_program::transfer 函数。请注意,它使用 system_instruction::transfer 构建了一个指令,然后使用 ctx.signer_seeds 调用 invoke_signed

Copypub fn transfer<'info>(
    ctx: CpiContext<'_, '_, '_, 'info, Transfer<'info>>,
    lamports: u64,
) -> Result<()> {
    let ix = crate::solana_program::system_instruction::transfer(
        ctx.accounts.from.key,
        ctx.accounts.to.key,
        lamports,
    );
    crate::solana_program::program::invoke_signed(
        &ix,
        &[ctx.accounts.from, ctx.accounts.to],
        ctx.signer_seeds,
    )
    .map_err(Into::into)
}

在 Anchor SPL token crate 中的 SPL token transfer 函数中也可以看到相同的模式:

Copypub fn transfer<'info>(
    ctx: CpiContext<'_, '_, '_, 'info, Transfer<'info>>,
    amount: u64,
) -> Result<()> {
    let ix = spl_token::instruction::transfer(
        &spl_token::ID,
        ctx.accounts.from.key,
        ctx.accounts.to.key,
        ctx.accounts.authority.key,
        &[],
        amount,
    )?;
    anchor_lang::solana_program::program::invoke_signed(
        &ix,
        &[ctx.accounts.from, ctx.accounts.to, ctx.accounts.authority],
        ctx.signer_seeds,
    )
    .map_err(Into::into)
}

请注意,在这两个示例中,代码始终使用带有 ctx.signer_seedsinvoke_signed。如前所述,当未提供种子时(常规 CPI),空的 signer_seeds 会使 invoke_signed 的行为与 invoke 完全相同。当提供种子时(PDA 签名),它允许程序代表其 PDA 进行签名。

我们现在了解了 Anchor 如何抽象跨程序调用。接下来,我们将学习如何手动构建 CPI 指令。

在原生 Rust 中手动构建 CPI 指令

要在原生 Rust Solana 程序中构建和执行 CPI 调用,我们需要构建一条指令,然后调用相应的 CPI 函数。

为此,我们需要:

  1. 创建一条指令,其中包含我们要调用的程序的 program_id,在 CPI 期间程序需要读取或写入的账户列表,以及要发送的指令数据。
  2. 然后我们使用指令和账户调用相应的 CPI 函数(invokeinvoke_signed)。

正如本教程开头所述,我们将创建两个协同工作的独立 Solana 程序:

  1. 目标程序:被调用时返回数字 42(我们稍后会看到如何实现)。
  2. 调用程序:使用 invoke 函数对目标程序进行 CPI 调用。

通过实现这两个程序,我们可以从两个角度观察完整的 CPI 过程,并了解数据如何在程序之间流动。

在我们创建程序之前,让我们设置项目结构:

Copymkdir solana-cpi-example
cd solana-cpi-example

创建目标程序

我们将保持目标程序简单,它只为调用程序返回一个值。

首先,我们创建目标程序目录并进行初始化(在 solana-cpi-example 内部):


Copymkdir tar...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论