Cross Program Invocation (CPI)

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

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

Cross Program InvocationCross Program Invocation

Ключевые моменты

  • Cross Program Invocation позволяет инструкциям программы Solana напрямую вызывать инструкции другой программы.
  • Привилегии подписанта от вызывающей программы распространяются на вызываемую программу.
  • При выполнении Cross Program Invocation программы могут подписываться от имени PDA, полученных из их собственного идентификатора программы.
  • Вызываемая программа может делать дальнейшие CPI к другим программам, до глубины 4.

Что такое CPI?

Cross Program Invocation (CPI) - это когда одна программа вызывает инструкции другой программы.

Написание инструкции программы с CPI следует тому же шаблону, что и создание инструкции для добавления в транзакцию. Под капотом каждая инструкция CPI должна указывать:

  • Адрес программы: Указывает программу для вызова
  • Аккаунты: Перечисляет каждый аккаунт, из которого инструкция читает или в который записывает, включая другие программы
  • Instruction data: Указывает, какую инструкцию вызвать в программе, плюс любые данные, которые нужны инструкции (аргументы функции)

Когда программа выполняет Cross Program Invocation (CPI) к другой программе:

  • Привилегии подписывающего из исходной транзакции распространяются на вызываемую программу (например, A->B)
  • Вызываемая программа может делать дальнейшие CPIs к другим программам, до глубины 4 (например, B->C, C->D)
  • Программы могут "подписывать" от имени PDA, полученных из идентификатора программы

Среда выполнения программ Solana устанавливает max_instruction_stack_depth константу MAX_INSTRUCTION_STACK_DEPTH равную 5. Это представляет максимальную высоту стека вызовов инструкций программы. Высота стека начинается с 1 для исходной транзакции и увеличивается на 1 каждый раз, когда программа вызывает другую инструкцию. Эта настройка ограничивает глубину вызовов для CPIs до 4.

Когда транзакция обрабатывается, привилегии аккаунта распространяются от одной программы к другой. Вот что это означает:

Допустим, Программа A получает инструкцию с:

  • Аккаунтом, который подписал транзакцию
  • Аккаунтом, в который можно записывать (изменяемый)

Когда Программа A делает CPI к Программе B:

  • Программа B получает возможность использовать эти же аккаунты с их исходными разрешениями
  • Программа B может подписывать с помощью аккаунта-подписанта
  • Программа B может записывать в изменяемый аккаунт
  • Программа B может даже передавать эти же разрешения дальше, если делает свои собственные CPIs

Cross Program Invocation

Функция invoke обрабатывает CPIs, которые не требуют подписантов PDA. Функция вызывает invoke_signed с пустым массивом signers_seeds, что указывает на отсутствие необходимости в PDA для подписи.

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

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

Фреймворк Anchor

Следующие примеры представляют три способа создания Cross Program Invocations (CPIs) в программе Anchor, каждый на разном уровне абстракции. Все примеры работают одинаково. Основная цель — показать детали реализации CPI.

  • Пример 1: Использует CpiContext Anchor и вспомогательную функцию для создания инструкции CPI.
  • Пример 2: Использует функцию system_instruction::transfer из крейта solana_program для создания инструкции CPI. Пример 1 абстрагирует эту реализацию.
  • Пример 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 из программы, написанной на нативном Rust. Программа включает одну инструкцию, которая переводит SOL с одного аккаунта на другой с помощью CPI. Тестовый файл использует 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(())
}
}
}

Cross Program Invocations с PDA-подписантами

Функция invoke_signed обрабатывает CPIs, требующие PDA-подписантов. Функция принимает seed для вычисления PDA-подписантов как signer_seeds.

Вы можете обратиться к странице Program Derived Address для получения подробной информации о том, как вычислять 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, среда выполнения Solana внутренне вызывает create_program_address используя signers_seeds и program_id вызывающей программы. Когда действительный PDA проверен, адрес добавляется как действительный подписант.

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

Фреймворк Anchor

Следующие примеры включают три подхода к реализации межпрограммных вызовов (Cross Program Invocations, CPIs) в программе Anchor, каждый на разном уровне абстракции. Все примеры функционально эквивалентны. Основная цель — проиллюстрировать детали реализации CPI.

  • Пример 1: Использует CpiContext от Anchor и вспомогательную функцию для создания инструкции CPI.
  • Пример 2: Использует функцию system_instruction::transfer из крейта solana_program для создания инструкции CPI. Пример 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(())
}
}
}

Is this page helpful?

Содержание

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