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

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

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

Inhaltsverzeichnis

Seite bearbeiten