Cross Program Invocation (CPI)
Cross Program Invocation (CPI) означає ситуацію, коли одна програма викликає інструкції іншої програми. Це забезпечує композиційність програм Solana.
Інструкції можна розглядати як кінцеві точки API, які програма надає мережі, а CPI — як один API, що внутрішньо викликає інший API.
Cross Program Invocation
Ключові моменти
- Cross Program Invocations дозволяють інструкціям програми Solana безпосередньо викликати інструкції іншої програми.
- Привілеї підписувача від програми-викликача поширюються на програму-отримувача.
- Під час виконання Cross Program Invocation програми можуть підписуватися від імені PDA, похідних від їхнього власного ідентифікатора програми.
- Програма-отримувач може робити подальші CPI до інших програм, до глибини 4.
Що таке CPI?
Cross Program Invocation (CPI) — це коли одна програма викликає інструкції іншої програми.
Написання інструкції програми з CPI слідує тому ж шаблону, що й створення інструкції для додавання до транзакції. Під капотом кожна інструкція CPI повинна вказувати:
- Адреса програми: Визначає програму для виклику
- Облікові записи: Перераховує кожен обліковий запис, з якого інструкція читає або в який записує, включаючи інші програми
- Дані інструкції: Визначає, яку інструкцію викликати в програмі, плюс будь-які дані, які потрібні інструкції (аргументи функції)
Коли програма робить Cross Program Invocation (CPI) до іншої програми:
- Привілеї підписувача з початкової транзакції поширюються на програму-отримувача (наприклад, A->B)
- Програма-отримувач може здійснювати подальші Cross Program Invocation до інших програм, до глибини 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
обробляє 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 та нативний 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?