Cross Program Invocation (CPI)
Eine Cross Program Invocation (CPI) bezieht sich auf den Fall, wenn ein Programm die Anweisungen eines anderen Programms aufruft. Dies ermöglicht die Kompositionsfähigkeit von Solana- Programmen.
Man kann sich Anweisungen als API-Endpunkte vorstellen, die ein Programm dem Netzwerk zur Verfügung stellt, und eine CPI als eine API, die intern eine andere API aufruft.
Cross Program Invocation
Kernpunkte
- Cross Program Invocations ermöglichen es Solana-Programmanweisungen, direkt Anweisungen auf einem anderen Programm aufzurufen.
- Signer-Privilegien vom aufrufenden Programm erstrecken sich auf das aufgerufene Programm.
- Bei der Durchführung einer Cross Program Invocation können Programme im Namen von PDAs signieren, die von ihrer eigenen Programm-ID abgeleitet sind.
- Das aufgerufene Programm kann weitere CPIs zu anderen Programmen durchführen, bis zu einer Tiefe von 4.
Was ist eine CPI?
Eine Cross Program Invocation (CPI) liegt vor, wenn ein Programm die Anweisungen eines anderen Programms aufruft.
Das Schreiben einer Programmanweisung mit einer CPI folgt dem gleichen Muster wie das Erstellen einer Anweisung, die einer Transaktion hinzugefügt wird. Im Hintergrund muss jede CPI-Anweisung Folgendes angeben:
- Programm-Adresse: Gibt das aufzurufende Programm an
- Konten: Listet jedes Konto auf, von dem die Anweisung liest oder in das sie schreibt, einschließlich anderer Programme
- instruction data: Gibt an, welche Anweisung im Programm aufgerufen werden soll, plus alle Daten, die die Anweisung benötigt (Funktionsargumente)
Wenn ein Programm eine Cross Program Invocation (CPI) zu einem anderen Programm durchführt:
- Die Signer-Privilegien aus der ursprünglichen Transaktion erstrecken sich auf das aufgerufene Programm (z.B. A->B)
- Das aufgerufene Programm kann weitere CPIs an andere Programme vornehmen, bis zu einer Tiefe von 4 (z.B. B->C, C->D)
- Die Programme können "im Namen" der PDAs, die von ihrer Programm-ID abgeleitet sind, "signieren"
Die Solana-Programm-Laufzeitumgebung setzt eine
max_instruction_stack_depth
Konstante
MAX_INSTRUCTION_STACK_DEPTH
von 5. Dies repräsentiert die maximale Höhe des
Programm-Anweisungs-Aufrufstacks. Die Stack-Höhe beginnt bei 1 für die
initiale Transaktion und erhöht sich um 1 jedes Mal, wenn ein Programm eine
andere Anweisung aufruft. Diese Einstellung begrenzt die Aufruftiefe für CPIs
auf 4.
Wenn eine Transaktion verarbeitet wird, erstrecken sich Konten-Privilegien von einem Programm zum anderen. Hier ist, was das bedeutet:
Nehmen wir an, Programm A erhält eine Anweisung mit:
- Einem Konto, das die Transaktion signiert hat
- Einem Konto, das beschrieben werden kann (veränderbar)
Wenn Programm A einen CPI an Programm B macht:
- Programm B kann diese gleichen Konten mit ihren ursprünglichen Berechtigungen verwenden
- Programm B kann mit dem Signer-Konto signieren
- Programm B kann in das beschreibbare Konto schreiben
- Programm B kann diese Berechtigungen sogar weitergeben, wenn es eigene CPIs durchführt
Cross Program Invocation
Die
invoke
Funktion behandelt CPIs, die keine PDA-Signer erfordern. Die Funktion ruft die
invoke_signed
Funktion mit einem leeren signers_seeds
Array auf, was darauf
hinweist, dass keine PDAs für die Signierung erforderlich sind.
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {invoke_signed(instruction, account_infos, &[])}
Die folgenden Beispiele zeigen, wie man einen CPI mit dem Anchor Framework und nativem Rust durchführt. Die Beispielprogramme enthalten eine einzelne Anweisung, die SOL von einem Konto zu einem anderen mittels eines CPI überträgt.
Anchor Framework
Die folgenden Beispiele zeigen drei Möglichkeiten, Cross Program Invocations (CPIs) in einem Anchor-Programm zu erstellen, jeweils mit unterschiedlichem Abstraktionsgrad. Alle Beispiele funktionieren auf die gleiche Weise. Der Hauptzweck besteht darin, die Implementierungsdetails eines CPI zu zeigen.
- Beispiel 1: Verwendet Anchor's
CpiContext
und Hilfsfunktion, um die CPI-Anweisung zu erstellen. - Beispiel 2: Verwendet die
system_instruction::transfer
Funktion aus demsolana_program
Crate, um die CPI-Anweisung zu erstellen. Beispiel 1 abstrahiert diese Implementierung. - Beispiel 3: Erstellt die CPI-Anweisung manuell. Dieser Ansatz ist nützlich, wenn kein Crate existiert, um die Anweisung zu erstellen.
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
Das folgende Beispiel zeigt, wie man einen CPI aus einem in Native Rust geschriebenen Programm durchführt. Das Programm enthält eine einzelne Anweisung, die SOL von einem Konto zu einem anderen mittels CPI überträgt. Die Testdatei verwendet LiteSVM, um das Programm zu testen.
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 mit PDA Signern
Die
invoke_signed
Funktion behandelt CPIs, die PDA Signer erfordern. Die Funktion nimmt die seeds
für die Ableitung von Signer PDAs als signer_seeds
.
Auf der Seite Program Derived Address finden Sie Details zur Ableitung von PDAs.
pub fn invoke_signed(instruction: &Instruction,account_infos: &[AccountInfo],signers_seeds: &[&[&[u8]]],) -> ProgramResult {// --snip--invoke_signed_unchecked(instruction, account_infos, signers_seeds)}
Bei der Verarbeitung einer Anweisung, die einen CPI enthält, ruft die
Solana-Laufzeit intern
create_program_address
auf und verwendet dabei die signers_seeds
und die program_id
des aufrufenden
Programms. Wenn eine gültige PDA verifiziert wird, wird die Adresse
als gültiger Signer hinzugefügt.
Die folgenden Beispiele zeigen, wie man einen CPI mit PDA Signern unter Verwendung des Anchor Framework und Native Rust durchführt. Die Beispielprogramme enthalten eine einzelne Anweisung, die SOL von einer PDA zum Empfängerkonto mittels eines von der PDA signierten CPI überträgt.
Anchor Framework
Die folgenden Beispiele zeigen drei Ansätze zur Implementierung von Cross Program Invocations (CPIs) in einem Anchor-Programm, jeweils mit unterschiedlichem Abstraktionsgrad. Alle Beispiele sind funktional gleichwertig. Der Hauptzweck besteht darin, die Implementierungsdetails einer CPI zu veranschaulichen.
- Beispiel 1: Verwendet Anchor's
CpiContext
und Hilfsfunktion, um die CPI-Anweisungen zu erstellen. - Beispiel 2: Verwendet die
system_instruction::transfer
Funktion aus demsolana_program
Crate, um die CPI-Anweisungen zu erstellen. Beispiel 1 ist eine Abstraktion dieser Implementierung. - Beispiel 3: Erstellt die CPI-Anweisungen manuell. Dieser Ansatz ist nützlich, wenn kein Crate verfügbar ist, um die Anweisung zu erstellen, die Sie aufrufen möchten.
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
Das folgende Beispiel zeigt, wie man eine CPI mit PDA-Signern aus einem Programm erstellt, das in Native Rust geschrieben wurde. Das Programm enthält eine einzelne Anweisung, die SOL von einem PDA zum Empfängerkonto überträgt, indem es eine vom PDA signierte CPI verwendet. Die Test-Datei verwendet LiteSVM, um das Programm zu testen.
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?