Cross Program Invocation (CPI)
Cross Program Invocation (CPI) означает ситуацию, когда одна программа вызывает инструкции другой программы. Это обеспечивает композиционность программ Solana.
Вы можете представлять инструкции как конечные точки API, которые программа предоставляет сети, а CPI - как один API, внутренне вызывающий другой API.
Cross 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 для подписи.
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 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
обрабатывает CPIs, требующие 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 и нативный 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 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?