Cross Program Invocation (CPI)

Cross Program Invocation (CPI) là khi một chương trình gọi các lệnh của một chương trình khác. Điều này cho phép khả năng kết hợp của các chương trình Solana.

Bạn có thể coi các lệnh như các điểm cuối API mà một chương trình hiển thị cho mạng và CPI như một API nội bộ gọi một API khác.

Cross Program InvocationCross Program Invocation

Điểm chính

  • Cross Program Invocations cho phép các lệnh chương trình Solana gọi trực tiếp các lệnh trên một chương trình khác.
  • Đặc quyền người ký từ chương trình gọi mở rộng đến chương trình được gọi.
  • Khi thực hiện Cross Program Invocation, các chương trình có thể ký thay mặt cho PDAs được tạo ra từ ID chương trình của chính họ.
  • Chương trình được gọi có thể thực hiện thêm các CPI đến các chương trình khác, với độ sâu tối đa là 4.

CPI là gì?

Cross Program Invocation (CPI) là khi một chương trình gọi các lệnh của một chương trình khác.

Viết lệnh chương trình với CPI tuân theo cùng một mẫu như xây dựng một lệnh để thêm vào một giao dịch. Bên dưới, mỗi lệnh CPI phải chỉ định:

  • Địa chỉ chương trình: Chỉ định chương trình cần gọi
  • Tài khoản: Liệt kê mọi tài khoản mà lệnh đọc từ hoặc ghi vào, bao gồm cả các chương trình khác
  • Instruction data: Chỉ định lệnh nào cần gọi trên chương trình, cộng với bất kỳ dữ liệu nào mà lệnh cần (đối số hàm)

Khi một chương trình thực hiện Cross Program Invocation (CPI) đến một chương trình khác:

  • Các đặc quyền của người ký từ giao dịch ban đầu được mở rộng đến chương trình được gọi (ví dụ: A->B)
  • Chương trình được gọi có thể thực hiện thêm các Cross Program Invocation đến các chương trình khác, với độ sâu tối đa là 4 (ví dụ: B->C, C->D)
  • Các chương trình có thể "ký" thay mặt cho PDAs được tạo ra từ ID chương trình của nó

Môi trường chạy chương trình Solana thiết lập một max_instruction_stack_depth hằng số MAX_INSTRUCTION_STACK_DEPTH là 5. Đây là chiều cao tối đa của ngăn xếp gọi lệnh chương trình. Chiều cao ngăn xếp bắt đầu từ 1 cho giao dịch ban đầu và tăng thêm 1 mỗi khi một chương trình gọi một lệnh khác. Cài đặt này giới hạn độ sâu gọi cho CPIs là 4.

Khi một giao dịch được xử lý, các đặc quyền tài khoản được mở rộng từ chương trình này sang chương trình khác. Dưới đây là ý nghĩa của điều này:

Giả sử Chương trình A nhận được một lệnh với:

  • Một tài khoản đã ký giao dịch
  • Một tài khoản có thể được ghi vào (có thể thay đổi)

Khi Chương trình A thực hiện một Cross Program Invocation đến Chương trình B:

  • Chương trình B được sử dụng các tài khoản này với quyền ban đầu của chúng
  • Chương trình B có thể ký với tài khoản người ký
  • Chương trình B có thể ghi vào tài khoản có thể ghi
  • Chương trình B thậm chí có thể chuyển tiếp các quyền này nếu nó thực hiện các CPIs của riêng mình

Cross Program Invocations

Hàm invoke xử lý các CPIs không yêu cầu người ký PDA. Hàm này gọi hàm invoke_signed với một mảng signers_seeds trống, cho biết không có PDAs nào cần thiết để ký.

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

Các ví dụ sau đây cho thấy cách thực hiện một CPI sử dụng Anchor Framework và Native Rust. Các chương trình ví dụ bao gồm một lệnh duy nhất chuyển SOL từ một tài khoản sang tài khoản khác sử dụng CPI.

Anchor Framework

Các ví dụ sau đây trình bày ba cách để tạo Cross Program Invocations (CPIs) trong một chương trình Anchor, mỗi cách ở một mức độ trừu tượng khác nhau. Tất cả các ví dụ đều hoạt động giống nhau. Mục đích chính là để hiển thị chi tiết thực hiện của một CPI.

  • Ví dụ 1: Sử dụng CpiContext của Anchor và hàm trợ giúp để xây dựng lệnh CPI.
  • Ví dụ 2: Sử dụng hàm system_instruction::transfer từ crate solana_program để xây dựng lệnh CPI. Ví dụ 1 trừu tượng hóa cách thực hiện này.
  • Ví dụ 3: Xây dựng lệnh CPI một cách thủ công. Cách tiếp cận này hữu ích khi không có crate nào tồn tại để giúp xây dựng lệnh.
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

Ví dụ sau đây cho thấy cách thực hiện CPI từ một chương trình được viết bằng Native Rust. Chương trình bao gồm một lệnh duy nhất chuyển SOL từ một tài khoản sang tài khoản khác sử dụng CPI. File kiểm thử sử dụng LiteSVM để kiểm tra chương trình.

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 với PDA Signers

Hàm invoke_signed xử lý các CPI yêu cầu PDA signers. Hàm này nhận các seed để tạo ra các signer PDA dưới dạng signer_seeds.

Bạn có thể tham khảo trang Program Derived Address để biết chi tiết về cách tạo ra 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)
}

Khi xử lý một lệnh bao gồm CPI, Solana runtime thực hiện gọi nội bộ create_program_address sử dụng signers_seedsprogram_id của chương trình gọi. Khi một PDA hợp lệ được xác minh, địa chỉ đó sẽ được thêm vào như một signer hợp lệ.

Các ví dụ sau đây minh họa cách thực hiện CPI với PDA signers sử dụng Anchor Framework và Native Rust. Các chương trình ví dụ bao gồm một lệnh duy nhất chuyển SOL từ một PDA đến tài khoản người nhận sử dụng CPI được ký bởi PDA.

Anchor Framework

Các ví dụ sau đây bao gồm ba cách tiếp cận để triển khai Cross Program Invocations (CPIs) trong một chương trình Anchor, mỗi cách ở một mức độ trừu tượng khác nhau. Tất cả các ví dụ đều tương đương về mặt chức năng. Mục đích chính là minh họa chi tiết việc triển khai một CPI.

  • Ví dụ 1: Sử dụng CpiContext của Anchor và hàm trợ giúp để xây dựng lệnh CPI.
  • Ví dụ 2: Sử dụng hàm system_instruction::transfer từ crate solana_program để xây dựng lệnh CPI. Ví dụ 1 là một trừu tượng hóa của cách triển khai này.
  • Ví dụ 3: Xây dựng lệnh CPI thủ công. Cách tiếp cận này hữu ích khi không có crate nào có sẵn để giúp xây dựng lệnh bạn muốn gọi.
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

Ví dụ sau đây cho thấy cách thực hiện CPI với người ký PDA từ một chương trình được viết bằng Native Rust. Chương trình bao gồm một lệnh duy nhất chuyển SOL từ một PDA đến tài khoản người nhận bằng cách sử dụng CPI được ký bởi PDA. File test sử dụng LiteSVM để kiểm tra chương trình.

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?

Mục lục

Chỉnh sửa trang