Cross Program Invocation

Cross Program Invocation (CPI) происходит, когда одна программа Solana напрямую вызывает инструкции другой программы. Это позволяет создавать композицию программ. Если представить инструкцию Solana как конечную точку API, которую программа предоставляет сети, то CPI — это как если бы одна конечная точка вызывала другую внутри системы.

При выполнении CPI программа может подписывать от имени PDA, производного от её идентификатора программы. Эти привилегии подписанта передаются от вызывающей программы к вызываемой программе.

Пример вызова между программамиПример вызова между программами

При выполнении CPI привилегии аккаунтов передаются от одной программы к другой. Например, Программа A получает инструкцию с аккаунтом подписанта и аккаунтом с правами записи. Затем Программа A выполняет CPI к Программе B. Теперь Программа B может использовать те же аккаунты, что и Программа A, с их исходными разрешениями. (Это означает, что Программа B может подписывать с аккаунтом подписанта и записывать в аккаунт с правами записи.) Если Программа B выполняет свои собственные CPI, она может передать эти же разрешения дальше, до глубины 4.

Максимальная высота стека вызовов инструкций программы называется max_instruction_stack_depth и задаётся константой MAX_INSTRUCTION_STACK_DEPTH, равной 5.

Высота стека начинается с 1 для начальной транзакции и увеличивается на 1 каждый раз, когда программа вызывает другую инструкцию, ограничивая глубину вызовов для CPI до 4.

CPI с подписантами PDA

Когда для CPI требуется PDA-подписант, используется функция invoke_signed. Она принимает seed подписанта, используемый для вывода подписанта PDA. Среда выполнения Solana внутренне вызывает create_program_address с использованием signers_seeds и program_id вызывающей программы. Когда PDA проверен, он добавляется как валидный подписант.

Invoke signed
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo],
signers_seeds: &[&[&[u8]]],
) -> ProgramResult {
// --snip--
invoke_signed_unchecked(instruction, account_infos, signers_seeds)
}

Примеры ниже демонстрируют создание CPI с PDA-подписантами, используя Anchor и нативный Rust. Каждый пример включает одну инструкцию для перевода SOL с PDA на аккаунт получателя, используя CPI, подписанный PDA.

Anchor

Следующие примеры показывают три подхода к реализации CPI в программе Anchor. Примеры функционально эквивалентны, но каждый демонстрирует разный уровень абстракции.

  • Пример 1: Использует CpiContext Anchor и вспомогательную функцию.
  • Пример 2: Использует функцию system_instruction::transfer из крейта solana_program. (Пример 1 является абстракцией этой реализации.)
  • Пример 3: Создает инструкцию CPI вручную. Этот подход полезен, когда нет доступного крейта для помощи в создании инструкции, которую вы хотите вызвать.
use anchor_lang::prelude::*;
use anchor_lang::system_program::{transfer, Transfer};
declare_id!("BrcdB9sV7z9DvF9rDHG263HUxXgJM3iCQdF36TcxbFEn");
#[program]
pub mod cpi {
use super::*;
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> {
let from_pubkey = ctx.accounts.pda_account.to_account_info();
let to_pubkey = ctx.accounts.recipient.to_account_info();
let program_id = ctx.accounts.system_program.to_account_info();
let seed = to_pubkey.key();
let bump_seed = ctx.bumps.pda_account;
let signer_seeds: &[&[&[u8]]] = &[&[b"pda", seed.as_ref(), &[bump_seed]]];
let cpi_context = CpiContext::new(
program_id,
Transfer {
from: from_pubkey,
to: to_pubkey,
},
)
.with_signer(signer_seeds);
transfer(cpi_context, amount)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct SolTransfer<'info> {
#[account(
mut,
seeds = [b"pda", recipient.key().as_ref()],
bump,
)]
pda_account: SystemAccount<'info>,
#[account(mut)]
recipient: SystemAccount<'info>,
system_program: Program<'info, System>,
}

Rust

Пример ниже демонстрирует создание CPI с PDA-подписантами из программы, написанной на нативном Rust. Он включает одну инструкцию, которая переводит SOL с PDA-аккаунта на другой. CPI подписан PDA-аккаунтом. (Тестовый файл использует LiteSVM для тестирования программы.)

use borsh::BorshDeserialize;
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
};
// Declare program entrypoint
entrypoint!(process_instruction);
// Define program instructions
#[derive(BorshDeserialize)]
enum ProgramInstruction {
SolTransfer { amount: u64 },
}
impl ProgramInstruction {
fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
Self::try_from_slice(input).map_err(|_| ProgramError::InvalidInstructionData)
}
}
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Deserialize instruction data
let instruction = ProgramInstruction::unpack(instruction_data)?;
// Process instruction
match instruction {
ProgramInstruction::SolTransfer { amount } => {
// Parse accounts
let [pda_account_info, recipient_info, system_program_info] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Derive PDA and verify it matches the account provided by client
let recipient_pubkey = recipient_info.key;
let seeds = &[b"pda", recipient_pubkey.as_ref()];
let (expected_pda, bump_seed) = Pubkey::find_program_address(seeds, program_id);
if expected_pda != *pda_account_info.key {
return Err(ProgramError::InvalidArgument);
}
// Create the transfer instruction
let transfer_ix = system_instruction::transfer(
pda_account_info.key,
recipient_info.key,
amount,
);
// Create signer seeds for PDA
let signer_seeds: &[&[&[u8]]] = &[&[b"pda", recipient_pubkey.as_ref(), &[bump_seed]]];
// Invoke the transfer instruction with PDA as signer
invoke_signed(
&transfer_ix,
&[
pda_account_info.clone(),
recipient_info.clone(),
system_program_info.clone(),
],
signer_seeds,
)?;
Ok(())
}
}
}

CPI без PDA-подписантов

Когда для CPI не требуются PDA-подписанты, используется функция invoke. Функция invoke вызывает функцию invoke_signed с пустым массивом signers_seeds. Пустой массив подписантов указывает, что PDA не требуются для подписания.

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

Примеры ниже демонстрируют создание CPI с использованием Anchor и Native Rust. Они включают одну инструкцию, которая переводит SOL с одного аккаунта на другой.

Anchor

Следующие примеры показывают три подхода к реализации CPI в программе Anchor. Примеры функционально эквивалентны, но каждый демонстрирует разный уровень абстракции.

  • Пример 1: Использует CpiContext и вспомогательную функцию Anchor.
  • Пример 2: Использует функцию system_instruction::transfer из крейта solana_program.
  • Пример 3: Создает инструкцию CPI вручную. Этот подход полезен, когда нет доступного крейта для создания нужной инструкции.
use anchor_lang::prelude::*;
use anchor_lang::system_program::{transfer, Transfer};
declare_id!("9AvUNHjxscdkiKQ8tUn12QCMXtcnbR9BVGq3ULNzFMRi");
#[program]
pub mod cpi {
use super::*;
pub fn sol_transfer(ctx: Context<SolTransfer>, amount: u64) -> Result<()> {
let from_pubkey = ctx.accounts.sender.to_account_info();
let to_pubkey = ctx.accounts.recipient.to_account_info();
let program_id = ctx.accounts.system_program.to_account_info();
let cpi_context = CpiContext::new(
program_id,
Transfer {
from: from_pubkey,
to: to_pubkey,
},
);
transfer(cpi_context, amount)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct SolTransfer<'info> {
#[account(mut)]
sender: Signer<'info>,
#[account(mut)]
recipient: SystemAccount<'info>,
system_program: Program<'info, System>,
}

Rust

Следующий пример показывает, как создать CPI в программе, написанной на Native Rust. Он включает одну инструкцию, которая переводит SOL с одного аккаунта на другой. (Тестовый файл использует LiteSVM для тестирования программы.)

use borsh::BorshDeserialize;
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
};
// Declare program entrypoint
entrypoint!(process_instruction);
// Define program instructions
#[derive(BorshDeserialize)]
enum ProgramInstruction {
SolTransfer { amount: u64 },
}
impl ProgramInstruction {
fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
Self::try_from_slice(input).map_err(|_| ProgramError::InvalidInstructionData)
}
}
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Deserialize instruction data
let instruction = ProgramInstruction::unpack(instruction_data)?;
// Process instruction
match instruction {
ProgramInstruction::SolTransfer { amount } => {
// Parse accounts
let [sender_info, recipient_info, system_program_info] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Verify the sender is a signer
if !sender_info.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Create and invoke the transfer instruction
let transfer_ix = system_instruction::transfer(
sender_info.key,
recipient_info.key,
amount,
);
invoke(
&transfer_ix,
&[
sender_info.clone(),
recipient_info.clone(),
system_program_info.clone(),
],
)?;
Ok(())
}
}
}

Is this page helpful?

Содержание

Редактировать страницу