本文详细介绍了Solana区块链上的跨程序调用(CPI),重点讲解了invoke和invoke_signed两个核心函数及其在原生Rust中的使用。文章还阐述了Anchor框架如何抽象这些函数,并通过一个包含目标程序和调用程序的实际案例,展示了如何在程序间传递数据。
跨程序调用 (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 函数。
我们将涵盖:
invoke 和 invoke_signed让我们首先了解 invoke 和 invoke_signed CPI 函数。
Solana 有两个用于进行跨程序调用的核心函数:
invoke:用于不需要 PDA 签名的 CPI 调用(使用原始交易签名者)invoke_signed:用于需要 PDA 签名的 CPI 调用(当程序需要代表其控制的 PDA 进行签名时)让我们详细了解这些函数:
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 提供你的程序读取或写入的实际账户数据和状态。
invoke_signed 函数invoke_signed 函数使用账户和指令数据调用另一个程序,就像 invoke 一样,但用于你的程序必须代表 PDA 进行签名的情况。它的工作原理如下:
invoke_signed 时,你必须提供用于派生 PDA 的种子Copypub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
signers_seeds: &[&[&[u8]]],
) -> ProgramResult
此函数与 invoke 具有相同的参数,但多了一个参数:
signers_seeds:需要签署 CPI 指令的 PDA 的派生种子。运行时使用这些种子重新派生 PDA 并验证它属于你的程序。现在我们了解了 Solana 提供的原生 CPI 函数,接下来让我们看看 Anchor 是如何使用它们的。
Anchor 通过提供两种封装 invoke 和 invoke_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::transfer 或 token::transfer)并传入上下文。
在幕后,Anchor 对所有 CPI 调用都使用 invoke_signed。这是因为:
invoke 完全相同这种统一的方法意味着 Anchor 只需要一个代码路径来处理所有 CPI 操作。
invoke 函数以这种方式工作,因为它的实现使用了 invoke_signed 函数,但为 PDA 签名者种子传递了一个空字节切片。
Copypub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {
invoke_signed(instruction, account_infos, &[])
}
在 Solana 的 BPF 运行时内部,invoke 和 invoke_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_seeds 的 invoke_signed。如前所述,当未提供种子时(常规 CPI),空的 signer_seeds 会使 invoke_signed 的行为与 invoke 完全相同。当提供种子时(PDA 签名),它允许程序代表其 PDA 进行签名。
我们现在了解了 Anchor 如何抽象跨程序调用。接下来,我们将学习如何手动构建 CPI 指令。
要在原生 Rust Solana 程序中构建和执行 CPI 调用,我们需要构建一条指令,然后调用相应的 CPI 函数。
为此,我们需要:
program_id,在 CPI 期间程序需要读取或写入的账户列表,以及要发送的指令数据。invoke 或 invoke_signed)。正如本教程开头所述,我们将创建两个协同工作的独立 Solana 程序:
invoke 函数对目标程序进行 CPI 调用。通过实现这两个程序,我们可以从两个角度观察完整的 CPI 过程,并了解数据如何在程序之间流动。
在我们创建程序之前,让我们设置项目结构:
Copymkdir solana-cpi-example
cd solana-cpi-example
我们将保持目标程序简单,它只为调用程序返回一个值。
首先,我们创建目标程序目录并进行初始化(在 solana-cpi-example 内部):
Copymkdir tar... 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!