Cross Program Invocation (CPI)

Cross Program Invocation (CPI) odnosi się do sytuacji, gdy jeden program wywołuje instrukcje innego programu. Umożliwia to kompozycyjność programów Solana.

Możesz myśleć o instrukcjach jako o punktach końcowych API, które program udostępnia sieci, a o CPI jako o jednym API wewnętrznie wywołującym inne API.

Cross Program InvocationCross Program Invocation

Kluczowe punkty

  • Cross Program Invocations umożliwiają instrukcjom programów Solana bezpośrednie wywoływanie instrukcji innego programu.
  • Uprawnienia podpisującego z programu wywołującego są rozszerzane na program wywoływany.
  • Podczas wykonywania Cross Program Invocation programy mogą podpisywać w imieniu PDA pochodzących z ich własnego ID programu.
  • Program wywoływany może wykonywać dalsze CPI do innych programów, do głębokości 4.

Czym jest CPI?

Cross Program Invocation (CPI) to sytuacja, gdy jeden program wywołuje instrukcje innego programu.

Pisanie instrukcji programu z CPI podąża za tym samym wzorcem, co budowanie instrukcji do dodania do transakcji. Pod spodem każda instrukcja CPI musi określać:

  • Adres programu: Określa program do wywołania
  • Konta: Wymienia każde konto, z którego instrukcja odczytuje lub do którego zapisuje, w tym inne programy
  • Dane instrukcji: Określa, którą instrukcję wywołać w programie, oraz wszelkie dane potrzebne instrukcji (argumenty funkcji)

Gdy program wykonuje Cross Program Invocation (CPI) do innego programu:

  • Uprawnienia podpisującego z początkowej transakcji są rozszerzane na wywoływany program (np. A->B)
  • Wywoływany program może wykonywać dalsze CPI do innych programów, do głębokości 4 (np. B->C, C->D)
  • Programy mogą "podpisywać" w imieniu PDA pochodzących od ich ID programu

Środowisko uruchomieniowe programów Solana ustawia stałą max_instruction_stack_depth MAX_INSTRUCTION_STACK_DEPTH o wartości 5. Reprezentuje to maksymalną wysokość stosu wywołań instrukcji programu. Wysokość stosu zaczyna się od 1 dla początkowej transakcji i zwiększa się o 1 za każdym razem, gdy program wywołuje inną instrukcję. To ustawienie ogranicza głębokość wywołań dla CPI do 4.

Podczas przetwarzania transakcji uprawnienia kont są rozszerzane z jednego programu na inny. Oto, co to oznacza:

Załóżmy, że Program A otrzymuje instrukcję z:

  • Kontem, które podpisało transakcję
  • Kontem, które można modyfikować (zmienne)

Gdy Program A wykonuje CPI do Programu B:

  • Program B może używać tych samych kont z ich oryginalnymi uprawnieniami
  • Program B może podpisywać się kontem podpisującym
  • Program B może modyfikować konto z możliwością zapisu
  • Program B może nawet przekazać te same uprawnienia dalej, jeśli wykonuje własne CPI

Cross Program Invocations

Funkcja invoke obsługuje CPI, które nie wymagają podpisujących PDA. Funkcja wywołuje funkcję invoke_signed z pustą tablicą signers_seeds, co oznacza brak wymaganych PDA do podpisywania.

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

Poniższe przykłady pokazują, jak wykonać CPI za pomocą Anchor Framework i Native Rust. Przykładowe programy zawierają pojedynczą instrukcję, która przesyła SOL z jednego konta na drugie za pomocą CPI.

Anchor Framework

Poniższe przykłady przedstawiają trzy sposoby tworzenia Cross Program Invocations (CPIs) w programie Anchor, każdy na innym poziomie abstrakcji. Wszystkie przykłady działają w ten sam sposób. Głównym celem jest pokazanie szczegółów implementacji CPI.

  • Przykład 1: Wykorzystuje CpiContext i funkcję pomocniczą Anchor do skonstruowania instrukcji CPI.
  • Przykład 2: Wykorzystuje funkcję system_instruction::transfer z solana_program crate do skonstruowania instrukcji CPI. Przykład 1 abstrahuje tę implementację.
  • Przykład 3: Ręcznie konstruuje instrukcję CPI. To podejście jest przydatne, gdy nie istnieje crate, który pomaga w budowie instrukcji.
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>,
}

Native Rust

Poniższy przykład pokazuje, jak wykonać CPI z programu napisanego w Native Rust. Program zawiera pojedynczą instrukcję, która przenosi SOL z jednego konta na drugie za pomocą CPI. Plik testowy wykorzystuje LiteSVM do testowania programu.

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 z podpisującymi PDA

Funkcja invoke_signed obsługuje CPIs wymagające podpisujących PDA. Funkcja przyjmuje seedy do derivacji podpisujących PDA jako signer_seeds.

Możesz odnieść się do strony Program Derived Address, aby uzyskać szczegóły na temat derivacji 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)
}

Podczas przetwarzania instrukcji zawierającej CPI, runtime Solana wewnętrznie wywołuje create_program_address używając signers_seeds i program_id programu wywołującego. Gdy zweryfikowany zostanie prawidłowy PDA, adres jest dodawany jako prawidłowy podpisujący.

Poniższe przykłady pokazują, jak wykonać CPI z podpisującymi PDA przy użyciu Anchor Framework i Native Rust. Programy przykładowe zawierają pojedynczą instrukcję, która przenosi SOL z PDA na konto odbiorcy za pomocą CPI podpisanego przez PDA.

Anchor Framework

Poniższe przykłady obejmują trzy podejścia do implementacji Cross Program Invocations (CPIs) w programie Anchor, każde na innym poziomie abstrakcji. Wszystkie przykłady są funkcjonalnie równoważne. Głównym celem jest zilustrowanie szczegółów implementacji CPI.

  • Przykład 1: Wykorzystuje CpiContext i funkcję pomocniczą Anchor do skonstruowania instrukcji CPI.
  • Przykład 2: Wykorzystuje funkcję system_instruction::transfer z biblioteki solana_program do skonstruowania instrukcji CPI. Przykład 1 jest abstrakcją tej implementacji.
  • Przykład 3: Ręcznie konstruuje instrukcję CPI. To podejście jest przydatne, gdy nie ma dostępnej biblioteki do budowy instrukcji, którą chcesz wywołać.
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>,
}

Native Rust

Poniższy przykład pokazuje, jak wykonać CPI z podpisami PDA w programie napisanym w Native Rust. Program zawiera pojedynczą instrukcję, która przenosi SOL z PDA na konto odbiorcy za pomocą CPI podpisanego przez PDA. Plik testowy wykorzystuje LiteSVM do testowania programu.

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?

Spis treści

Edytuj stronę