Cross Program Invocation

Une Cross Program Invocation (CPI) se produit lorsqu'un programme Solana invoque directement les instructions d'un autre programme. Cela permet la composabilité des programmes. Si vous considérez une instruction Solana comme un point d'API qu'un programme expose au réseau, un CPI est comme un point d'accès invoquant internement un autre.

Lors d'un CPI, un programme peut signer au nom d'un PDA dérivé de son ID de programme. Ces privilèges de signature s'étendent du programme appelant au programme appelé.

Exemple d'invocation inter-programmesExemple d'invocation inter-programmes

Lors d'un CPI, les privilèges des comptes s'étendent d'un programme à un autre. Supposons que le Programme A reçoive une instruction avec un compte signataire et un compte inscriptible. Le Programme A effectue ensuite un CPI vers le Programme B. Maintenant, le Programme B peut utiliser les mêmes comptes que le Programme A, avec leurs permissions d'origine. (Ce qui signifie que le Programme B peut signer avec le compte signataire et peut écrire dans le compte inscriptible.) Si le Programme B effectue ses propres CPI, il peut transmettre ces mêmes permissions, jusqu'à une profondeur de 4.

La hauteur maximale de l'invocation d'instruction de programme est appelée max_instruction_stack_depth et est définie sur la constante MAX_INSTRUCTION_STACK_DEPTH de 5.

La hauteur de la pile commence à 1 pour la transaction initiale et augmente de 1 chaque fois qu'un programme invoque une autre instruction, limitant la profondeur d'invocation pour les CPI à 4.

CPI avec signataires PDA

Lorsqu'un CPI nécessite un signataire PDA, la fonction invoke_signed est utilisée. Elle prend les seeds de signataire utilisées pour dériver les PDAs signataires. Le runtime Solana appelle en interne create_program_address en utilisant les signers_seeds et le program_id du programme appelant. Lorsqu'un PDA est vérifié, il est ajouté comme signataire valide.

Invoke signed
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo],
signers_seeds: &[&[&[u8]]],
) -> ProgramResult {
// --snip--
invoke_signed_unchecked(instruction, account_infos, signers_seeds)
}

Les exemples ci-dessous effectuent un CPI avec des signataires PDA en utilisant Anchor et Rust natif. Chaque exemple inclut une seule instruction pour transférer des SOL d'un PDA vers un compte destinataire, en utilisant un CPI signé par le PDA.

Anchor

Les exemples suivants montrent trois approches pour implémenter des CPIs dans un programme Anchor. Les exemples sont fonctionnellement équivalents, mais chacun démontre un niveau d'abstraction différent.

  • Exemple 1 : Utilise CpiContext d'Anchor et une fonction auxiliaire.
  • Exemple 2 : Utilise la fonction system_instruction::transfer du crate solana_program. (L'exemple 1 est une abstraction de cette implémentation.)
  • Exemple 3 : Construit l'instruction CPI manuellement. Cette approche est utile lorsqu'aucun crate n'est disponible pour aider à construire l'instruction que vous souhaitez invoquer.
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

L'exemple ci-dessous effectue un CPI avec des signataires PDA à partir d'un programme écrit en Rust natif. Il inclut une seule instruction qui transfère des SOL d'un compte PDA vers un autre. Le CPI est signé par le compte PDA. (Le fichier de test utilise LiteSVM pour tester le programme.)

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(())
}
}
}

CPIs sans signataires PDA

Lorsqu'un CPI ne nécessite pas de signataires PDA, la fonction invoke est utilisée. La fonction invoke appelle la fonction invoke_signed avec un tableau signers_seeds vide. Le tableau de signataires vide indique qu'aucun PDA n'est requis pour la signature.

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

Les exemples ci-dessous montrent comment effectuer un CPI en utilisant Anchor et Rust natif. Ils incluent une seule instruction qui transfère du SOL d'un compte à un autre.

Anchor

Les exemples suivants présentent trois approches pour implémenter des CPIs dans un programme Anchor. Ces exemples sont fonctionnellement équivalents, mais chacun démontre un niveau d'abstraction différent.

  • Exemple 1 : Utilise CpiContext d'Anchor et une fonction auxiliaire.
  • Exemple 2 : Utilise la fonction system_instruction::transfer du crate solana_program.
  • Exemple 3 : Construit l'instruction CPI manuellement. Cette approche est utile lorsqu'aucun crate n'existe pour aider à construire l'instruction que vous souhaitez invoquer.
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

L'exemple suivant montre comment effectuer un CPI à partir d'un programme écrit en Rust natif. Il inclut une seule instruction qui transfère du SOL d'un compte à un autre. (Le fichier de test utilise LiteSVM pour tester le programme.)

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(())
}
}
}

Is this page helpful?

Table des matières

Modifier la page

Géré par

© 2025 Fondation Solana.
Tous droits réservés.
Restez connecté