Cross Program Invocation (CPI)

Το Cross Program Invocation (CPI) αναφέρεται στην περίπτωση όπου ένα πρόγραμμα επικαλείται τις εντολές ενός άλλου προγράμματος. Αυτό επιτρέπει τη συνθεσιμότητα των προγραμμάτων Solana.

Μπορείτε να σκεφτείτε τις εντολές ως τελικά σημεία API που ένα πρόγραμμα εκθέτει στο δίκτυο και ένα CPI ως ένα API που εσωτερικά καλεί ένα άλλο API.

Cross Program InvocationCross Program Invocation

Βασικά σημεία

  • Τα Cross Program Invocations επιτρέπουν στις εντολές προγραμμάτων Solana να καλούν απευθείας εντολές σε άλλο πρόγραμμα.
  • Τα προνόμια υπογραφής από ένα πρόγραμμα καλούντος επεκτείνονται στο πρόγραμμα που καλείται.
  • Κατά την πραγματοποίηση ενός Cross Program Invocation, τα προγράμματα μπορούν να υπογράφουν εκ μέρους των PDAs που προέρχονται από το δικό τους αναγνωριστικό προγράμματος.
  • Το πρόγραμμα που καλείται μπορεί να κάνει περαιτέρω CPIs σε άλλα προγράμματα, μέχρι βάθος 4.

Τι είναι το CPI;

Το Cross Program Invocation (CPI) είναι όταν ένα πρόγραμμα επικαλείται τις εντολές ενός άλλου προγράμματος.

Η σύνταξη μιας εντολής προγράμματος με CPI ακολουθεί το ίδιο μοτίβο με τη δημιουργία μιας εντολής για προσθήκη σε μια συναλλαγή. Στο παρασκήνιο, κάθε εντολή CPI πρέπει να καθορίζει:

  • Διεύθυνση προγράμματος: Καθορίζει το πρόγραμμα που θα κληθεί
  • Λογαριασμοί: Παραθέτει κάθε λογαριασμό από τον οποίο η εντολή διαβάζει ή στον οποίο γράφει, συμπεριλαμβανομένων άλλων προγραμμάτων
  • Δεδομένα εντολής: Καθορίζει ποια εντολή θα κληθεί στο πρόγραμμα, συν οποιαδήποτε δεδομένα χρειάζεται η εντολή (ορίσματα συνάρτησης)

Όταν ένα πρόγραμμα κάνει ένα Cross Program Invocation (CPI) σε ένα άλλο πρόγραμμα:

  • Τα προνόμια υπογραφής από την αρχική συναλλαγή επεκτείνονται στο πρόγραμμα που καλείται (π.χ. A->B)
  • Το πρόγραμμα που καλείται μπορεί να κάνει περαιτέρω CPIs σε άλλα προγράμματα, μέχρι βάθος 4 (π.χ. B->C, C->D)
  • Τα προγράμματα μπορούν να "υπογράψουν" εκ μέρους των PDAs που προέρχονται από το αναγνωριστικό του προγράμματος

Το περιβάλλον εκτέλεσης του προγράμματος Solana ορίζει μια max_instruction_stack_depth σταθερά MAX_INSTRUCTION_STACK_DEPTH με τιμή 5. Αυτό αντιπροσωπεύει το μέγιστο ύψος της στοίβας κλήσεων εντολών προγράμματος. Το ύψος της στοίβας ξεκινά από 1 για την αρχική συναλλαγή και αυξάνεται κατά 1 κάθε φορά που ένα πρόγραμμα καλεί μια άλλη εντολή. Αυτή η ρύθμιση περιορίζει το βάθος κλήσεων για CPIs σε 4.

Κατά την επεξεργασία μιας συναλλαγής, τα προνόμια λογαριασμού επεκτείνονται από το ένα πρόγραμμα στο άλλο. Ιδού τι σημαίνει αυτό:

Ας υποθέσουμε ότι το Πρόγραμμα Α λαμβάνει μια εντολή με:

  • Έναν λογαριασμό που υπέγραψε τη συναλλαγή
  • Έναν λογαριασμό στον οποίο μπορεί να γίνει εγγραφή (μεταβλητός)

Όταν το Πρόγραμμα Α κάνει ένα CPI στο Πρόγραμμα Β:

  • Το Πρόγραμμα Β μπορεί να χρησιμοποιήσει αυτούς τους ίδιους λογαριασμούς με τα αρχικά τους δικαιώματα
  • Το Πρόγραμμα Β μπορεί να υπογράψει με τον λογαριασμό υπογραφής
  • Το Πρόγραμμα Β μπορεί να γράψει στον εγγράψιμο λογαριασμό
  • Το Πρόγραμμα Β μπορεί ακόμη και να μεταβιβάσει αυτά τα ίδια δικαιώματα αν κάνει τα δικά του CPIs

Cross Program Invocation

Η συνάρτηση invoke χειρίζεται CPIs που δεν απαιτούν υπογράφοντες PDA. Η συνάρτηση καλεί τη invoke_signed συνάρτηση με έναν κενό πίνακα signers_seeds, υποδεικνύοντας ότι δεν απαιτούνται PDAs για υπογραφή.

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

Τα παρακάτω παραδείγματα δείχνουν πώς να κάνετε ένα CPI χρησιμοποιώντας το Anchor Framework και το Native Rust. Τα παραδείγματα προγραμμάτων περιλαμβάνουν μια μοναδική εντολή που μεταφέρει SOL από έναν λογαριασμό σε άλλον χρησιμοποιώντας ένα CPI.

Anchor Framework

Τα παρακάτω παραδείγματα παρουσιάζουν τρεις τρόπους για τη δημιουργία Cross Program Invocations (CPIs) σε ένα πρόγραμμα Anchor, το καθένα σε διαφορετικό επίπεδο αφαίρεσης. Όλα τα παραδείγματα λειτουργούν με τον ίδιο τρόπο. Ο κύριος σκοπός είναι να δείξουν τις λεπτομέρειες υλοποίησης ενός CPI.

  • Παράδειγμα 1: Χρησιμοποιεί το CpiContext του Anchor και βοηθητική συνάρτηση για την κατασκευή της εντολής CPI.
  • Παράδειγμα 2: Χρησιμοποιεί τη συνάρτηση system_instruction::transfer από το solana_program crate για την κατασκευή της εντολής CPI. Το παράδειγμα 1 αφαιρεί αυτήν την υλοποίηση.
  • Παράδειγμα 3: Κατασκευάζει την εντολή CPI χειροκίνητα. Αυτή η προσέγγιση είναι χρήσιμη όταν δεν υπάρχει crate για να βοηθήσει στην κατασκευή της εντολής.
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

Το παρακάτω παράδειγμα δείχνει πώς να κάνετε ένα CPI από ένα πρόγραμμα γραμμένο σε Native Rust. Το πρόγραμμα περιλαμβάνει μια μόνο εντολή που μεταφέρει SOL από έναν λογαριασμό σε άλλον χρησιμοποιώντας ένα CPI. Το αρχείο δοκιμής χρησιμοποιεί το LiteSVM για τη δοκιμή του προγράμματος.

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 με υπογράφοντες PDA

Η συνάρτηση invoke_signed χειρίζεται CPIs που απαιτούν υπογράφοντες PDA. Η συνάρτηση παίρνει τα seeds για την παραγωγή υπογραφόντων PDAs ως signer_seeds.

Μπορείτε να ανατρέξετε στη σελίδα Program Derived Address για λεπτομέρειες σχετικά με το πώς να παράγετε 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)
}

Κατά την επεξεργασία μιας εντολής που περιλαμβάνει ένα CPI, το Solana runtime εσωτερικά καλεί τη συνάρτηση create_program_address χρησιμοποιώντας το signers_seeds και το program_id του προγράμματος που καλεί. Όταν επαληθευτεί ένα έγκυρο PDA, η διεύθυνση προστίθεται ως έγκυρος υπογράφων.

Τα παρακάτω παραδείγματα δείχνουν πώς να κάνετε ένα CPI με υπογράφοντες PDA χρησιμοποιώντας το Anchor Framework και το Native Rust. Τα παραδείγματα προγραμμάτων περιλαμβάνουν μια μόνο εντολή που μεταφέρει SOL από ένα PDA σε τον λογαριασμό παραλήπτη χρησιμοποιώντας ένα CPI υπογεγραμμένο από το PDA.

Anchor Framework

Τα παρακάτω παραδείγματα περιλαμβάνουν τρεις προσεγγίσεις για την υλοποίηση Cross Program Invocations (CPIs) σε ένα πρόγραμμα Anchor, καθεμία σε διαφορετικό επίπεδο αφαίρεσης. Όλα τα παραδείγματα είναι λειτουργικά ισοδύναμα. Ο κύριος σκοπός είναι να απεικονίσουν τις λεπτομέρειες υλοποίησης ενός CPI.

  • Παράδειγμα 1: Χρησιμοποιεί το CpiContext του Anchor και βοηθητική συνάρτηση για την κατασκευή της εντολής CPI.
  • Παράδειγμα 2: Χρησιμοποιεί τη συνάρτηση system_instruction::transfer από το solana_program crate για την κατασκευή της εντολής CPI. Το Παράδειγμα 1 είναι μια αφαίρεση αυτής της υλοποίησης.
  • Παράδειγμα 3: Κατασκευάζει την εντολή CPI χειροκίνητα. Αυτή η προσέγγιση είναι χρήσιμη όταν δεν υπάρχει διαθέσιμο crate για να βοηθήσει στην κατασκευή της εντολής που θέλετε να καλέσετε.
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

Το παρακάτω παράδειγμα δείχνει πώς να κάνετε ένα CPI με υπογράφοντες PDA από ένα πρόγραμμα γραμμένο σε Native Rust. Το πρόγραμμα περιλαμβάνει μια μόνο εντολή που μεταφέρει SOL από ένα PDA στον λογαριασμό παραλήπτη χρησιμοποιώντας ένα CPI υπογεγραμμένο από το PDA. Το αρχείο δοκιμής χρησιμοποιεί το LiteSVM για τη δοκιμή του προγράμματος.

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?

Πίνακας Περιεχομένων

Επεξεργασία Σελίδας