Cross Program Invocation (CPI)

Cross Program Invocation (CPI) mengacu pada saat satu program memanggil instruksi program lain. Ini memungkinkan komposabilitas program Solana.

Anda dapat menganggap instruksi sebagai endpoint API yang diekspos program ke jaringan dan CPI sebagai satu API yang secara internal memanggil API lain.

Cross Program InvocationCross Program Invocation

Poin Penting

  • Cross Program Invocations memungkinkan instruksi program Solana secara langsung memanggil instruksi pada program lain.
  • Hak penandatangan dari program pemanggil diperluas ke program yang dipanggil.
  • Saat melakukan Cross Program Invocation, program dapat menandatangani atas nama PDA yang diturunkan dari ID program mereka sendiri.
  • Program yang dipanggil dapat membuat CPI lebih lanjut ke program lain, hingga kedalaman 4.

Apa itu CPI?

Cross Program Invocation (CPI) adalah ketika satu program memanggil instruksi program lain.

Menulis instruksi program dengan CPI mengikuti pola yang sama seperti membangun instruksi untuk ditambahkan ke transaksi. Di balik layar, setiap instruksi CPI harus menentukan:

  • Alamat program: Menentukan program yang akan dipanggil
  • Akun: Mencantumkan setiap akun yang dibaca atau ditulis oleh instruksi, termasuk program lain
  • Instruction data: Menentukan instruksi mana yang akan dipanggil pada program, ditambah data apa pun yang dibutuhkan instruksi (argumen fungsi)

Ketika program melakukan Cross Program Invocation (CPI) ke program lain:

  • Hak istimewa penandatangan dari transaksi awal diperluas ke program yang dipanggil (contoh: A->B)
  • Program yang dipanggil dapat membuat CPIs lebih lanjut ke program lain, hingga kedalaman 4 (contoh: B->C, C->D)
  • Program dapat "menandatangani" atas nama PDAs yang diturunkan dari ID programnya

Runtime program Solana menetapkan konstanta max_instruction_stack_depth MAX_INSTRUCTION_STACK_DEPTH sebesar 5. Ini mewakili ketinggian maksimum tumpukan pemanggilan instruksi program. Ketinggian tumpukan dimulai pada 1 untuk transaksi awal dan bertambah 1 setiap kali program memanggil instruksi lain. Pengaturan ini membatasi kedalaman pemanggilan untuk CPIs hingga 4.

Ketika transaksi diproses, hak istimewa akun diperluas dari satu program ke program lainnya. Berikut adalah artinya:

Misalkan Program A menerima instruksi dengan:

  • Akun yang menandatangani transaksi
  • Akun yang dapat ditulis (mutable)

Ketika Program A membuat CPI ke Program B:

  • Program B dapat menggunakan akun-akun yang sama dengan izin aslinya
  • Program B dapat menandatangani dengan akun penandatangan
  • Program B dapat menulis ke akun yang dapat ditulis
  • Program B bahkan dapat meneruskan izin yang sama jika membuat CPIs sendiri

Cross Program Invocation

Fungsi invoke menangani CPIs yang tidak memerlukan penandatangan PDA. Fungsi ini memanggil fungsi invoke_signed dengan array signers_seeds kosong, yang menunjukkan tidak ada PDA yang diperlukan untuk penandatanganan.

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

Contoh berikut menunjukkan cara membuat CPI menggunakan Anchor Framework dan Native Rust. Program contoh mencakup satu instruksi yang mentransfer SOL dari satu akun ke akun lain menggunakan CPI.

Anchor Framework

Contoh berikut menampilkan tiga cara untuk membuat Cross Program Invocations (CPIs) dalam program Anchor, masing-masing pada tingkat abstraksi yang berbeda. Semua contoh bekerja dengan cara yang sama. Tujuan utamanya adalah untuk menunjukkan detail implementasi dari CPI.

  • Contoh 1: Menggunakan CpiContext Anchor dan fungsi pembantu untuk membangun instruksi CPI.
  • Contoh 2: Menggunakan fungsi system_instruction::transfer dari crate solana_program. Contoh 1 mengabstraksi implementasi ini.
  • Contoh 3: Membangun instruksi CPI secara manual. Pendekatan ini berguna ketika tidak ada crate yang tersedia untuk membantu membangun instruksi.
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

Contoh berikut menunjukkan cara membuat CPI dari program yang ditulis dalam Native Rust. Program ini mencakup satu instruksi yang mentransfer SOL dari satu akun ke akun lain menggunakan CPI. File pengujian menggunakan LiteSVM untuk menguji program.

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 dengan PDA Signers

Fungsi invoke_signed menangani CPIs yang memerlukan PDA signers. Fungsi ini mengambil seed untuk menurunkan signer PDAs sebagai signer_seeds.

Anda dapat merujuk ke halaman Program Derived Address untuk detail tentang cara menurunkan 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)
}

Ketika memproses instruksi yang mencakup CPI, runtime Solana secara internal memanggil create_program_address menggunakan signers_seeds dan program_id dari program yang memanggil. Ketika PDA valid diverifikasi, alamat tersebut ditambahkan sebagai signer yang valid.

Contoh berikut mendemonstrasikan cara membuat CPI dengan PDA signers menggunakan Anchor Framework dan Native Rust. Program contoh mencakup satu instruksi yang mentransfer SOL dari PDA ke akun penerima menggunakan CPI yang ditandatangani oleh PDA.

Anchor Framework

Contoh berikut mencakup tiga pendekatan untuk mengimplementasikan Cross Program Invocations (CPIs) dalam program Anchor, masing-masing pada tingkat abstraksi yang berbeda. Semua contoh secara fungsional setara. Tujuan utamanya adalah untuk mengilustrasikan detail implementasi dari CPI.

  • Contoh 1: Menggunakan CpiContext Anchor dan fungsi pembantu untuk membangun instruksi CPI.
  • Contoh 2: Menggunakan fungsi system_instruction::transfer dari crate solana_program untuk membangun instruksi CPI. Contoh 1 adalah abstraksi dari implementasi ini.
  • Contoh 3: Membangun instruksi CPI secara manual. Pendekatan ini berguna ketika tidak ada crate yang tersedia untuk membantu membangun instruksi yang ingin Anda panggil.
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

Contoh berikut menunjukkan cara melakukan CPI dengan penandatangan PDA dari program yang ditulis dalam Native Rust. Program ini mencakup satu instruksi yang mentransfer SOL dari PDA ke akun penerima menggunakan CPI yang ditandatangani oleh PDA. File pengujian menggunakan LiteSVM untuk menguji program.

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?

Daftar Isi

Edit Halaman