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 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ý.
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ừ cratesolana_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 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 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.
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_seeds
và program_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ừ cratesolana_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 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?