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 InvocationCross 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.

Invoke Function
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 crate solana_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 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 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.

Invoke Signed
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 crate solana_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 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?

Indice

Modifica Pagina