Cross Program Invocation (CPI)
Cross Program Invocation (CPI) относится к случаю, когда одна программа вызывает инструкции другой программы. Это позволяет создавать композиции программ Solana.
Вы можете представить инструкции как API-эндпоинты, которые программа предоставляет сети, а CPI — как внутренний вызов одного API другим API.
Cross Program Invocation
Основные моменты
- Cross Program Invocation позволяет инструкциям программы Solana напрямую вызывать инструкции другой программы.
- Привилегии подписанта от вызывающей программы распространяются на вызываемую программу.
- При выполнении Cross Program Invocation программы могут подписывать от имени PDA, производных от их собственного ID программы.
- Вызываемая программа может выполнять дальнейшие CPIs для других программ, до глубины 4.
Что такое CPI?
Cross Program Invocation (CPI) — это процесс, когда одна программа вызывает инструкции другой программы.
Написание инструкции программы с использованием CPI следует той же схеме, что и создание инструкции для добавления в транзакцию. На уровне реализации каждая инструкция CPI должна указывать:
- Адрес программы: Указывает программу для вызова
- Аккаунты: Перечисляет все аккаунты, из которых инструкция читает или в которые записывает, включая другие программы
- Instruction Data: Указывает, какую инструкцию вызвать в программе, а также любые данные, необходимые для инструкции (аргументы функции)
Когда программа выполняет Cross Program Invocation (CPI) для другой программы:
- Привилегии подписанта из исходной транзакции распространяются на вызываемую программу (например, A->B)
- Вызываемая программа может выполнять дальнейшие CPI к другим программам, до глубины 4 (например, B->C, C->D)
- Программы могут "подписывать" от имени PDA, производных от их идентификатора программы
Среда выполнения программ Solana устанавливает
max_instruction_stack_depth
константу
MAX_INSTRUCTION_STACK_DEPTH
равную 5. Это представляет максимальную высоту стека вызовов инструкций
программы. Высота стека начинается с 1 для исходной транзакции и увеличивается
на 1 каждый раз, когда программа вызывает другую инструкцию. Этот параметр
ограничивает глубину вызовов для CPI до 4.
Когда транзакция обрабатывается, привилегии аккаунтов распространяются от одной программы к другой. Вот что это означает:
Предположим, Программа A получает инструкцию с:
- Аккаунтом, который подписал транзакцию
- Аккаунтом, который можно изменять (изменяемый)
Когда Программа A выполняет CPI к Программе B:
- Программа B получает возможность использовать те же аккаунты с их исходными разрешениями
- Программа B может подписывать с помощью аккаунта подписанта
- Программа B может записывать в изменяемый аккаунт
- Программа B может даже передать эти же разрешения дальше, если она выполняет свои собственные CPI
Cross Program Invocations
Функция
invoke
обрабатывает CPI, которые не требуют подписантов PDA. Функция вызывает
invoke_signed
с пустым массивом signers_seeds
, указывая, что PDA не
требуются для подписания.
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {invoke_signed(instruction, account_infos, &[])}
Следующие примеры показывают, как выполнить CPI с использованием Anchor Framework и Native Rust. Примерные программы включают одну инструкцию, которая переводит SOL с одного аккаунта на другой с использованием CPI.
Фреймворк Anchor
Следующие примеры демонстрируют три способа создания Cross Program Invocations (CPI) в программе 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 entrypointentrypoint!(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 datalet instruction = ProgramInstruction::unpack(instruction_data)?;// Process instructionmatch instruction {ProgramInstruction::SolTransfer { amount } => {// Parse accountslet [sender_info, recipient_info, system_program_info] = accounts else {return Err(ProgramError::NotEnoughAccountKeys);};// Verify the sender is a signerif !sender_info.is_signer {return Err(ProgramError::MissingRequiredSignature);}// Create and invoke the transfer instructionlet 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
обрабатывает CPI, которые требуют PDA-подписантов. Функция принимает seed для
вывода PDA-подписантов как signer_seeds
.
Вы можете обратиться к странице Program Derived Address для получения подробностей о том, как выводить PDA.
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 Framework и нативный Rust. Примерные программы включают одну инструкцию, которая переводит SOL с PDA на аккаунт получателя с использованием CPI, подписанного PDA.
Фреймворк Anchor
Следующие примеры включают три подхода к реализации Cross Program Invocations (CPI) в программе 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 entrypointentrypoint!(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 datalet instruction = ProgramInstruction::unpack(instruction_data)?;// Process instructionmatch instruction {ProgramInstruction::SolTransfer { amount } => {// Parse accountslet [pda_account_info, recipient_info, system_program_info] = accounts else {return Err(ProgramError::NotEnoughAccountKeys);};// Derive PDA and verify it matches the account provided by clientlet 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 instructionlet transfer_ix = system_instruction::transfer(pda_account_info.key,recipient_info.key,amount,);// Create signer seeds for PDAlet signer_seeds: &[&[&[u8]]] = &[&[b"pda", recipient_pubkey.as_ref(), &[bump_seed]]];// Invoke the transfer instruction with PDA as signerinvoke_signed(&transfer_ix,&[pda_account_info.clone(),recipient_info.clone(),system_program_info.clone(),],signer_seeds,)?;Ok(())}}}
Is this page helpful?