Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:01:25 +08:00
commit d733741f8a
37 changed files with 26647 additions and 0 deletions

View File

@@ -0,0 +1,824 @@
# Cross-Program Invocation (CPI)
This reference provides comprehensive coverage of Cross-Program Invocation (CPI) for native Rust Solana program development, including invoke patterns, account privilege propagation, and security considerations.
## Table of Contents
1. [What is CPI](#what-is-cpi)
2. [CPI Fundamentals](#cpi-fundamentals)
3. [invoke vs invoke_signed](#invoke-vs-invoke_signed)
4. [Account Privilege Propagation](#account-privilege-propagation)
5. [Common CPI Patterns](#common-cpi-patterns)
6. [CPI Limits and Constraints](#cpi-limits-and-constraints)
7. [Security Considerations](#security-considerations)
8. [Best Practices](#best-practices)
---
## What is CPI
**Cross-Program Invocation (CPI) is when one Solana program directly calls instructions on another program.**
### Conceptual Model
If you think of a Solana instruction as an API endpoint, a CPI is like one API endpoint internally calling another.
```
User Transaction
┌────────────────────┐
│ Your Program │
│ │
│ ┌──────────────┐ │
│ │ Instruction │ │
│ │ Handler │ │
│ └──────┬───────┘ │
│ │ CPI │
└──────────┼─────────┘
┌────────────────────┐
│ System Program │
│ create_account │
└────────────────────┘
```
### Why CPI is Essential
**Composability**: Programs can leverage functionality from other programs without reimplementing it.
**Common Use Cases:**
- Create accounts (System Program CPI)
- Transfer tokens (Token Program CPI)
- Interact with DeFi protocols
- Call custom program logic
- Complex multi-step operations
### CPI vs Direct Instruction
| Aspect | Direct Instruction | CPI |
|--------|-------------------|-----|
| Who initiates | User wallet | Another program |
| Signer source | User's private key | Program or PDA |
| Call depth | 1 (top-level) | 2-5 (nested) |
| Use case | Entry point | Program-to-program |
---
## CPI Fundamentals
### The Two CPI Functions
Solana provides two functions for making CPIs:
```rust
use solana_program::program::{invoke, invoke_signed};
// 1. invoke: For regular account signers
pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo],
) -> ProgramResult
// 2. invoke_signed: For PDA signers
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo],
signers_seeds: &[&[&[u8]]],
) -> ProgramResult
```
### Required Imports
```rust
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
instruction::{AccountMeta, Instruction},
program::{invoke, invoke_signed},
pubkey::Pubkey,
};
```
### Instruction Structure
Before making a CPI, you must construct an `Instruction`:
```rust
pub struct Instruction {
/// Program ID of the program being invoked
pub program_id: Pubkey,
/// Accounts required by the instruction
pub accounts: Vec<AccountMeta>,
/// Serialized instruction data
pub data: Vec<u8>,
}
pub struct AccountMeta {
/// Account public key
pub pubkey: Pubkey,
/// Is this account a signer?
pub is_signer: bool,
/// Is this account writable?
pub is_writable: bool,
}
```
---
## invoke vs invoke_signed
### invoke: Regular Signers
Use `invoke` when all required signers are regular accounts (not PDAs).
**Example: User transfers SOL**
```rust
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
};
pub fn user_transfer_sol(
_program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let sender = next_account_info(account_info_iter)?;
let recipient = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// Verify sender signed the transaction
if !sender.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Create transfer instruction
let transfer_ix = system_instruction::transfer(
sender.key,
recipient.key,
amount,
);
// Execute CPI (sender already signed the transaction)
invoke(
&transfer_ix,
&[
sender.clone(),
recipient.clone(),
system_program.clone(),
],
)?;
Ok(())
}
```
**Key Points:**
- `sender.is_signer` must be true (verified at transaction level)
- No `signers_seeds` needed
- `invoke` internally calls `invoke_signed` with empty seeds
### invoke_signed: PDA Signers
Use `invoke_signed` when a PDA needs to sign the instruction.
**Example: PDA transfers SOL**
```rust
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
};
pub fn pda_transfer_sol(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let pda_account = next_account_info(account_info_iter)?;
let recipient = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// Derive PDA and verify
let (pda, bump_seed) = Pubkey::find_program_address(
&[b"vault", recipient.key.as_ref()],
program_id,
);
if pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
// Create transfer instruction
let transfer_ix = system_instruction::transfer(
pda_account.key, // From PDA (needs signing!)
recipient.key,
amount,
);
// PDA signing seeds (must match derivation)
let signer_seeds: &[&[&[u8]]] = &[&[
b"vault",
recipient.key.as_ref(),
&[bump_seed], // Critical: bump must be included
]];
// Execute CPI with PDA signature
invoke_signed(
&transfer_ix,
&[
pda_account.clone(),
recipient.clone(),
system_program.clone(),
],
signer_seeds, // Runtime verifies and grants signing authority
)?;
Ok(())
}
```
**How Runtime Handles PDA Signing:**
1. Runtime receives `signers_seeds`
2. Calls `create_program_address(signers_seeds, calling_program_id)`
3. Verifies derived PDA matches an account in the instruction
4. Grants signing authority for that account
5. Executes the CPI
**Critical:** Seeds must exactly match the PDA derivation, including the bump.
---
## Account Privilege Propagation
### Privilege Extension
When making a CPI, account privileges **extend** from the caller to the callee.
```
User Transaction
│ (provides: signer=true, writable=true)
┌─────────────────────┐
│ Program A │
│ Receives accounts: │
│ - user (signer) │──┐ Privileges
│ - vault (writable) │ │ propagate
└─────────────────────┘ │
┌─────────────────────┐
│ Program B (via CPI)│
│ Can use: │
│ - user (signer) │
│ - vault (writable) │
└─────────────────────┘
```
### Propagation Rules
**Rule 1:** If an account is a signer in Program A, it remains a signer in Program B (via CPI)
**Rule 2:** If an account is writable in Program A, it remains writable in Program B (via CPI)
**Rule 3:** Programs can add PDA signers via `invoke_signed`
**Rule 4:** Programs cannot escalate privileges (can't make non-signer a signer without PDA derivation)
### Example: Privilege Propagation Chain
```rust
// User calls Program A
// Accounts: [user (signer, writable), vault (writable), data_account]
// Program A → CPI to Program B
invoke(
&instruction_for_program_b,
&[user.clone(), vault.clone()], // Both retain privileges
)?;
// Program B → CPI to Program C
invoke(
&instruction_for_program_c,
&[user.clone()], // user still a signer!
)?;
```
**Depth**: Up to 4 levels of CPI (5 total stack height including initial transaction)
---
## Common CPI Patterns
### 1. System Program: Create Account
**Most common CPI**: Creating new accounts.
```rust
use solana_program::{
program::invoke_signed,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};
pub fn create_pda_account(
program_id: &Pubkey,
payer: &AccountInfo,
pda_account: &AccountInfo,
system_program: &AccountInfo,
space: usize,
seeds: &[&[u8]],
bump: u8,
) -> ProgramResult {
// Calculate rent
let rent = Rent::get()?;
let rent_lamports = rent.minimum_balance(space);
// Create account instruction
let create_account_ix = system_instruction::create_account(
payer.key,
pda_account.key,
rent_lamports,
space as u64,
program_id,
);
// Prepare signer seeds
let mut full_seeds = seeds.to_vec();
full_seeds.push(&[bump]);
let signer_seeds: &[&[&[u8]]] = &[&full_seeds];
// Execute CPI
invoke_signed(
&create_account_ix,
&[payer.clone(), pda_account.clone(), system_program.clone()],
signer_seeds,
)?;
Ok(())
}
```
### 2. System Program: Transfer SOL
```rust
use solana_program::system_instruction;
// From regular account
let transfer_ix = system_instruction::transfer(from_key, to_key, lamports);
invoke(&transfer_ix, &[from_account, to_account, system_program])?;
// From PDA
let transfer_ix = system_instruction::transfer(pda_key, to_key, lamports);
let signer_seeds: &[&[&[u8]]] = &[&[seeds, &[bump]]];
invoke_signed(&transfer_ix, &[pda_account, to_account, system_program], signer_seeds)?;
```
### 3. Custom Program CPI
**Calling another custom program:**
```rust
use borsh::BorshSerialize;
#[derive(BorshSerialize)]
struct CustomInstructionData {
amount: u64,
memo: String,
}
pub fn call_custom_program(
custom_program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let user = next_account_info(account_info_iter)?;
let target_account = next_account_info(account_info_iter)?;
let custom_program = next_account_info(account_info_iter)?;
// Serialize instruction data
let instruction_data = CustomInstructionData {
amount,
memo: "Hello from CPI".to_string(),
};
let data = instruction_data.try_to_vec()?;
// Build instruction
let instruction = Instruction {
program_id: *custom_program_id,
accounts: vec![
AccountMeta::new(*user.key, true), // signer, writable
AccountMeta::new(*target_account.key, false), // writable
],
data,
};
// Execute CPI
invoke(
&instruction,
&[user.clone(), target_account.clone(), custom_program.clone()],
)?;
Ok(())
}
```
### 4. Multiple PDAs Signing
```rust
pub fn multi_pda_cpi(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let pda1_seeds = &[b"pda1", &[bump1]];
let pda2_seeds = &[b"pda2", &[bump2]];
// Multiple PDA signers
let signer_seeds: &[&[&[u8]]] = &[
pda1_seeds, // First PDA
pda2_seeds, // Second PDA
];
invoke_signed(&instruction, &accounts, signer_seeds)?;
Ok(())
}
```
### 5. Chained CPIs
**Program A calls Program B, which calls Program C:**
```rust
// In Program A
pub fn program_a_handler(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
// Call Program B
let instruction_for_b = build_program_b_instruction();
invoke(&instruction_for_b, accounts)?;
Ok(())
}
// In Program B
pub fn program_b_handler(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
// Call Program C
let instruction_for_c = build_program_c_instruction();
invoke(&instruction_for_c, accounts)?;
Ok(())
}
```
**Depth tracking**: User→A→B→C = stack depth 4 (within limit)
---
## CPI Limits and Constraints
### Stack Depth Limit
**Maximum call depth:** 5 (including initial transaction)
```
Depth 1: User Transaction
Depth 2: Program A (first CPI)
Depth 3: Program B (second CPI)
Depth 4: Program C (third CPI)
Depth 5: Program D (fourth CPI)
Depth 6: ❌ ERROR - MAX_INSTRUCTION_STACK_DEPTH exceeded
```
**Constant:**
```rust
// From agave source
pub const MAX_INSTRUCTION_STACK_DEPTH: usize = 5;
```
**Error when exceeded:**
```
Error: CallDepth(5)
```
### Account Limits
- **Max accounts per instruction:** 256 (practical limit ~64 without ALTs)
- **Max writable accounts:** Limited by transaction size
- **Duplicate accounts:** Allowed but share state (mutations visible to all references)
### Compute Unit Costs
CPI operations consume compute units:
| Operation | Approximate CU Cost |
|-----------|---------------------|
| `invoke` base cost | ~1,000 CU |
| `invoke_signed` base cost | ~1,000 CU |
| Per account passed | ~50-100 CU |
| PDA derivation in runtime | ~1,500 CU |
| Actual callee logic | Variable |
**Tip:** Pre-derive PDAs and store bumps to save CU.
### Data Size Limits
- **Instruction data:** No hard limit, but affects transaction size (1232 bytes max for non-ALT transactions)
- **Account data modification:** Accounts can be resized via `realloc` (up to 10 MiB)
---
## Security Considerations
### 1. Validate PDA Derivation Before CPI
**❌ Vulnerable:**
```rust
pub fn vulnerable_cpi(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let pda_account = &accounts[0];
// No validation!
let signer_seeds: &[&[&[u8]]] = &[&[b"vault", &[bump]]];
invoke_signed(&instruction, &[pda_account.clone()], signer_seeds)?;
Ok(())
}
```
**✅ Secure:**
```rust
pub fn secure_cpi(
program_id: &Pubkey,
accounts: &[AccountInfo],
bump: u8,
) -> ProgramResult {
let pda_account = &accounts[0];
// Validate PDA before CPI
let (expected_pda, _) = Pubkey::find_program_address(&[b"vault"], program_id);
if expected_pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
let signer_seeds: &[&[&[u8]]] = &[&[b"vault", &[bump]]];
invoke_signed(&instruction, &[pda_account.clone()], signer_seeds)?;
Ok(())
}
```
### 2. Verify Signer Requirements
**Always check `is_signer` before making CPIs that transfer value:**
```rust
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
let transfer_ix = system_instruction::transfer(user.key, vault.key, amount);
invoke(&transfer_ix, &[user.clone(), vault.clone(), system_program.clone()])?;
```
### 3. Program ID Verification
**Verify the program being called is the expected program:**
```rust
const EXPECTED_PROGRAM: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
if token_program.key.to_string() != EXPECTED_PROGRAM {
return Err(ProgramError::IncorrectProgramId);
}
```
### 4. Privilege Leakage
**Be careful about which accounts you pass in CPIs:**
```rust
// ❌ Dangerous - passes admin with signer privilege
invoke(
&untrusted_program_instruction,
&[admin.clone(), user_data.clone()], // Admin is a signer!
)?;
// ✅ Safe - only pass necessary accounts
invoke(
&untrusted_program_instruction,
&[user_data.clone()], // Admin not included
)?;
```
### 5. Reent rancy Considerations
**Solana programs are generally safe from reentrancy** because:
- Accounts are locked during instruction execution
- Runtime prevents concurrent modifications
**However, be cautious with:**
- State assumptions across CPI boundaries
- Read-modify-write patterns split across CPIs
### 6. Error Handling
**CPI errors propagate to the caller:**
```rust
// If CPI fails, entire transaction reverts
match invoke(&instruction, &accounts) {
Ok(()) => msg!("CPI succeeded"),
Err(e) => {
msg!("CPI failed: {:?}", e);
return Err(e); // Propagate error
}
}
```
**All state changes are atomic** - if CPI fails, all changes rollback.
---
## Best Practices
### 1. Derive PDAs Once
```rust
// ❌ Wasteful - derives multiple times
pub fn wasteful(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let (pda, bump) = Pubkey::find_program_address(&[b"data"], program_id);
// ... use pda
let (pda_again, bump_again) = Pubkey::find_program_address(&[b"data"], program_id);
// ... use pda_again (same as pda!)
}
// ✅ Efficient - derive once, reuse
pub fn efficient(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let (pda, bump) = Pubkey::find_program_address(&[b"data"], program_id);
// Reuse pda and bump
}
```
### 2. Store and Reuse Bumps
```rust
#[derive(BorshSerialize, BorshDeserialize)]
pub struct VaultData {
pub bump: u8, // Store on creation
// ... other fields
}
// On CPI: use stored bump
let vault_data = VaultData::try_from_slice(&vault_pda.data.borrow())?;
let signer_seeds: &[&[&[u8]]] = &[&[b"vault", &[vault_data.bump]]];
```
**Benefit:** Saves ~2,700 CU per operation.
### 3. Helper Functions for Common CPIs
```rust
pub mod cpi_helpers {
use super::*;
pub fn transfer_sol(
from: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
amount: u64,
) -> ProgramResult {
let ix = system_instruction::transfer(from.key, to.key, amount);
invoke(&ix, &[from.clone(), to.clone(), system_program.clone()])
}
pub fn transfer_sol_from_pda(
from_pda: &AccountInfo,
to: &AccountInfo,
system_program: &AccountInfo,
amount: u64,
signer_seeds: &[&[&[u8]]],
) -> ProgramResult {
let ix = system_instruction::transfer(from_pda.key, to.key, amount);
invoke_signed(&ix, &[from_pda.clone(), to.clone(), system_program.clone()], signer_seeds)
}
}
```
### 4. Validate All CPI Inputs
**Checklist before CPI:**
- ✅ Verify signer requirements (`is_signer`)
- ✅ Validate PDA derivation
- ✅ Check program IDs match expectations
- ✅ Verify account ownership
- ✅ Validate data integrity
### 5. Document CPI Dependencies
```rust
/// Transfers SOL from program vault to recipient.
///
/// # Accounts
/// 0. `[writable]` vault_pda - Program vault (PDA, signer)
/// 1. `[writable]` recipient - Receives SOL
/// 2. `[]` system_program - System Program (11111...)
///
/// # CPIs Made
/// - System Program: transfer (from vault to recipient)
pub fn withdraw_from_vault(
program_id: &Pubkey,
accounts: &[AccountInfo],
amount: u64,
) -> ProgramResult {
// ...
}
```
### 6. Error Context
```rust
invoke(&instruction, &accounts).map_err(|e| {
msg!("CPI to System Program failed");
e
})?;
```
### 7. Minimize CPI Depth
**Keep call chains shallow:**
- Reduces compute units
- Easier to debug
- Lower risk of hitting stack limit
- Better user experience (simpler transactions)
---
## Summary
**Key Takeaways:**
1. **CPI enables composability** - programs call other programs
2. **Use `invoke` for regular signers**, `invoke_signed` for PDAs
3. **Privileges propagate** - signers and writable flags extend through CPIs
4. **Maximum depth is 5** - including initial transaction
5. **Always validate PDAs** before using in `invoke_signed`
6. **Verify signer requirements** to prevent unauthorized operations
7. **Store bumps** in account data to save compute units
8. **CPIs are atomic** - failures rollback all changes
**Security Checklist:**
- ✅ Validate PDA derivation with canonical bump
- ✅ Verify `is_signer` for value transfers
- ✅ Check program IDs match expectations
- ✅ Only pass necessary accounts (avoid privilege leakage)
- ✅ Handle CPI errors appropriately
**Common Pattern:**
```rust
// 1. Validate inputs
if !user.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// 2. Derive and validate PDA if needed
let (pda, bump) = Pubkey::find_program_address(&seeds, program_id);
if pda != *pda_account.key {
return Err(ProgramError::InvalidSeeds);
}
// 3. Build instruction
let ix = build_instruction();
// 4. Execute CPI
invoke_signed(&ix, &accounts, &[&[seeds, &[bump]]])?;
```
CPI is the foundation of program composability on Solana. Master it to build powerful, modular programs that leverage the entire ecosystem.