Cross Program Invocation (CPI)
Una Cross Program Invocation (CPI) si riferisce a quando un programma invoca le istruzioni di un altro programma. Questo permette la componibilità dei programmi Solana.
Puoi pensare alle istruzioni come endpoint API che un programma espone alla rete e a una CPI come un'API che internamente invoca un'altra API.
Cross Program Invocation
Punti chiave
- Le Cross Program Invocations permettono alle istruzioni dei programmi Solana di invocare direttamente istruzioni su un altro programma.
- I privilegi di firma dal programma chiamante si estendono al programma chiamato.
- Quando si effettua una Cross Program Invocation, i programmi possono firmare per conto di PDA derivati dal proprio ID programma.
- Il programma chiamato può effettuare ulteriori CPI ad altri programmi, fino a una profondità di 4.
Che cos'è una CPI?
Una Cross Program Invocation (CPI) avviene quando un programma invoca le istruzioni di un altro programma.
Scrivere un'istruzione di programma con una CPI segue lo stesso schema della costruzione di un'istruzione da aggiungere a una transazione. Dietro le quinte, ogni istruzione CPI deve specificare:
- Indirizzo del programma: Specifica il programma da invocare
- Account: Elenca ogni account da cui l'istruzione legge o su cui scrive, inclusi altri programmi
- Instruction data: Specifica quale istruzione invocare sul programma, più qualsiasi dato di cui l'istruzione ha bisogno (argomenti della funzione)
Quando un programma effettua una Cross Program Invocation (CPI) a un altro programma:
- I privilegi del firmatario dalla transazione iniziale si estendono al programma chiamato (es. A->B)
- Il programma chiamato può effettuare ulteriori CPI ad altri programmi, fino a una profondità di 4 (es. B->C, C->D)
- I programmi possono "firmare" per conto dei PDA derivati dal proprio ID programma
Il runtime del programma Solana imposta una
max_instruction_stack_depth
costante
MAX_INSTRUCTION_STACK_DEPTH
di 5. Questo rappresenta l'altezza massima dello stack di invocazione delle
istruzioni del programma. L'altezza dello stack inizia a 1 per la transazione
iniziale e aumenta di 1 ogni volta che un programma invoca un'altra
istruzione. Questa impostazione limita la profondità di invocazione per i CPI
a 4.
Quando una transazione viene elaborata, i privilegi dell'account si estendono da un programma all'altro. Ecco cosa significa:
Supponiamo che il Programma A riceva un'istruzione con:
- Un account che ha firmato la transazione
- Un account che può essere modificato (mutabile)
Quando il Programma A effettua un CPI al Programma B:
- Il Programma B può utilizzare questi stessi account con i loro permessi originali
- Il Programma B può firmare con l'account firmatario
- Il Programma B può scrivere nell'account modificabile
- Il Programma B può persino trasmettere questi stessi permessi se effettua i propri CPI
Cross Program Invocation
La funzione
invoke
gestisce i CPI che non richiedono firmatari PDA. La funzione chiama la funzione
invoke_signed
con un array signers_seeds
vuoto, indicando che non sono
richiesti PDA per la firma.
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {invoke_signed(instruction, account_infos, &[])}
I seguenti esempi mostrano come effettuare un CPI utilizzando Anchor Framework e Rust nativo. Gli esempi di programma includono una singola istruzione che trasferisce SOL da un account a un altro utilizzando un CPI.
Anchor Framework
I seguenti esempi presentano tre modi per creare Cross Program Invocations (CPI) in un programma Anchor, ciascuno a un diverso livello di astrazione. Tutti gli esempi funzionano allo stesso modo. Lo scopo principale è mostrare i dettagli di implementazione di una CPI.
- Esempio 1: Utilizza
CpiContext
di Anchor e la funzione helper per costruire l'istruzione CPI. - Esempio 2: Utilizza la funzione
system_instruction::transfer
dal cratesolana_program
per costruire l'istruzione CPI. L'esempio 1 astrae questa implementazione. - Esempio 3: Costruisce l'istruzione CPI manualmente. Questo approccio è utile quando non esiste un crate per aiutare a costruire l'istruzione.
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 nativo
Il seguente esempio mostra come effettuare una CPI da un programma scritto in Rust nativo. Il programma include una singola istruzione che trasferisce SOL da un account a un altro utilizzando una CPI. Il file di test utilizza LiteSVM per testare il programma.
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 con firmatari PDA
La funzione
invoke_signed
gestisce le CPI che richiedono firmatari PDA. La funzione prende i seed per
derivare i PDA firmatari come signer_seeds
.
Puoi fare riferimento alla pagina Program Derived Address per dettagli su come derivare i PDA.
pub fn invoke_signed(instruction: &Instruction,account_infos: &[AccountInfo],signers_seeds: &[&[&[u8]]],) -> ProgramResult {// --snip--invoke_signed_unchecked(instruction, account_infos, signers_seeds)}
Durante l'elaborazione di un'istruzione che include una CPI, il runtime di
Solana internamente chiama
create_program_address
utilizzando signers_seeds
e program_id
del programma chiamante. Quando un
PDA valido viene verificato, l'indirizzo viene
aggiunto come firmatario valido.
I seguenti esempi dimostrano come effettuare una CPI con firmatari PDA utilizzando Anchor Framework e Rust nativo. I programmi di esempio includono una singola istruzione che trasferisce SOL da un PDA a un account destinatario utilizzando una CPI firmata dal PDA.
Anchor Framework
I seguenti esempi includono tre approcci per implementare le Cross Program Invocations (CPI) in un programma Anchor, ciascuno a un diverso livello di astrazione. Tutti gli esempi sono funzionalmente equivalenti. Lo scopo principale è illustrare i dettagli di implementazione di una CPI.
- Esempio 1: Utilizza
CpiContext
di Anchor e la funzione helper per costruire l' istruzione CPI. - Esempio 2: Utilizza la funzione
system_instruction::transfer
dal cratesolana_program
per costruire l'istruzione CPI. L'esempio 1 è un' astrazione di questa implementazione. - Esempio 3: Costruisce l'istruzione CPI manualmente. Questo approccio è utile quando non è disponibile un crate per aiutare a costruire l'istruzione che si desidera invocare.
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 nativo
Il seguente esempio mostra come effettuare una CPI con firmatari PDA da un programma scritto in Rust nativo. Il programma include una singola istruzione che trasferisce SOL da un PDA all'account destinatario utilizzando una CPI firmata dal PDA. Il file di test utilizza LiteSVM per testare il programma.
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?