Cross Program Invocation (CPI)

A Cross Program Invocation (CPI) refers to when one program invokes the instructions of another program. This allows for the composability of Solana programs.

You can think of instructions as API endpoints that a program exposes to the network and a CPI as one API internally invoking another API.

Cross Program InvocationCross Program Invocation

Key Points

  • Cross Program Invocations enable Solana program instructions to directly invoke instructions on another program.
  • Signer privileges from a caller program extend to the callee program.
  • When making a Cross Program Invocation, programs can sign on behalf of PDAs derived from their own program ID.
  • The callee program can make further CPIs to other programs, up to a depth of 4.

What is a CPI?

A Cross Program Invocation (CPI) is when one program invokes the instructions of another program.

Writing a program instruction with a CPI follows the same pattern as building an instruction to add to a transaction. Under the hood, each CPI instruction must specify:

  • Program address: Specifies the program to invoke
  • Accounts: Lists every account the instruction reads from or writes to, including other programs
  • Instruction Data: Specifies which instruction to invoke on the program, plus any data the instruction needs (function arguments)

When a program makes a Cross Program Invocation (CPI) to another program:

  • The signer privileges from the initial transaction extend to the callee program (ex. A->B)
  • The callee program can make further CPIs to other programs, up to a depth of 4 (ex. B->C, C->D)
  • The programs can "sign" on behalf of the PDAs derived from its program ID

The Solana program runtime sets a max_instruction_stack_depth constant MAX_INSTRUCTION_STACK_DEPTH of 5. This represents the max height of the program instruction invocation stack. The stack height begins at 1 for the initial transaction and increases by 1 each time a program invokes another instruction. This setting limits invocation depth for CPIs to 4.

When a transaction is processed, account privileges extend from one program to another. Here's what that means:

Let's say Program A receives an instruction with:

  • An account that signed the transaction
  • An account that can be written to (mutable)

When Program A makes a CPI to Program B:

  • Program B gets to use these same accounts with their original permissions
  • Program B can sign with the signer account
  • Program B can write to the writable account
  • Program B can even pass these same permissions forward if it makes its own CPIs

Cross Program Invocations

The invoke function handles CPIs that don't require PDA signers. The function calls the invoke_signed function with an empty signers_seeds array, indicating no PDAs required for signing.

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

The following examples show how to make a CPI using the Anchor Framework and Native Rust. The example programs include a single instruction that transfers SOL from one account to another using a CPI.

Anchor Framework

The following examples present three ways to create Cross Program Invocations (CPIs) in an Anchor program, each at a different level of abstraction. All examples work the same way. The main purpose is to show the implementation details of a CPI.

  • Example 1: Uses Anchor's CpiContext and helper function to construct the CPI instruction.
  • Example 2: Uses the system_instruction::transfer function from the solana_program crate to construct the CPI instruction. Example 1 abstracts this implementation.
  • Example 3: Constructs the CPI instruction manually. This approach is useful when no crate exists to help build the instruction.
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

The following example shows how to make a CPI from a program written in Native Rust. The program includes a single instruction that transfers SOL from one account to another using a CPI. The test file uses the LiteSVM to test the 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 with PDA Signers

The invoke_signed function handles CPIs that require PDA signers. The function takes the seeds for deriving signer PDAs as signer_seeds.

You can reference the Program Derived Address page for details on how to derive 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)
}

When processing an instruction that includes a CPI, the Solana runtime internally calls create_program_address using the signers_seeds and the program_id of the calling program. When a valid PDA verified, the address is added as a valid signer.

The following examples demonstrate how to make a CPI with PDA signers using the Anchor Framework and Native Rust. The example programs include a single instruction that transfers SOL from a PDA to the recipient account using a CPI signed by the PDA.

Anchor Framework

The following examples include three approaches to implementing Cross Program Invocations (CPIs) in an Anchor program, each at a different level of abstraction. All examples are functionally equivalent. The main purpose is to illustrate the implementation details of a CPI.

  • Example 1: Uses Anchor's CpiContext and helper function to construct the CPI instruction.
  • Example 2: Uses the system_instruction::transfer function from solana_program crate to construct the CPI instruction. Example 1 is an abstraction of this implementation.
  • Example 3: Constructs the CPI instruction manually. This approach is useful when there is no crate available to help build the instruction you want to invoke.
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

The following example shows how to make a CPI with PDA signers from a program written in Native Rust. The program includes a single instruction that transfers SOL from a PDA to the recipient account using a CPI signed by the PDA. The test file uses the LiteSVM to test the 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?

목차

페이지 편집