Cross Program Invocation (CPI)
Una Cross Program Invocation (CPI) se refiere a cuando un programa invoca las instrucciones de otro programa. Esto permite la composición de programas en Solana.
Puedes pensar en las instrucciones como puntos finales de API que un programa expone a la red y en un CPI como una API que invoca internamente a otra API.
Cross Program Invocation
Puntos clave
- Las Cross Program Invocations permiten que las instrucciones de programas de Solana invoquen directamente instrucciones en otro programa.
- Los privilegios de firmante de un programa que llama se extienden al programa llamado.
- Al realizar una Cross Program Invocation, los programas pueden firmar en nombre de PDAs derivadas de su propio ID de programa.
- El programa llamado puede hacer más CPIs a otros programas, hasta una profundidad de 4.
¿Qué es un CPI?
Una Cross Program Invocation (CPI) es cuando un programa invoca las instrucciones de otro programa.
Escribir una instrucción de programa con un CPI sigue el mismo patrón que construir una instrucción para añadir a una transacción. Internamente, cada instrucción CPI debe especificar:
- Dirección del programa: Especifica el programa a invocar
- Cuentas: Enumera cada cuenta de la que la instrucción lee o en la que escribe, incluyendo otros programas
- Datos de instrucción: Especifica qué instrucción invocar en el programa, además de cualquier dato que la instrucción necesite (argumentos de función)
Cuando un programa realiza una Cross Program Invocation (CPI) a otro programa:
- Los privilegios del firmante de la transacción inicial se extienden al programa invocado (ej. A->B)
- El programa invocado puede realizar más CPIs a otros programas, hasta una profundidad de 4 (ej. B->C, C->D)
- Los programas pueden "firmar" en nombre de las PDAs derivadas de su ID de programa
El entorno de ejecución del programa Solana establece una constante
max_instruction_stack_depth
MAX_INSTRUCTION_STACK_DEPTH
de 5. Esto representa la altura máxima de la pila de invocación de
instrucciones del programa. La altura de la pila comienza en 1 para la
transacción inicial y aumenta en 1 cada vez que un programa invoca otra
instrucción. Esta configuración limita la profundidad de invocación para CPIs
a 4.
Cuando se procesa una transacción, los privilegios de la cuenta se extienden de un programa a otro. Esto es lo que significa:
Supongamos que el Programa A recibe una instrucción con:
- Una cuenta que firmó la transacción
- Una cuenta que puede ser modificada (mutable)
Cuando el Programa A hace un CPI al Programa B:
- El Programa B puede usar estas mismas cuentas con sus permisos originales
- El Programa B puede firmar con la cuenta firmante
- El Programa B puede escribir en la cuenta modificable
- El Programa B incluso puede pasar estos mismos permisos si realiza sus propios CPIs
Cross Program Invocation
La función
invoke
maneja CPIs que no requieren firmantes PDA. La función llama a la función
invoke_signed
con un array signers_seeds
vacío, indicando que no se
requieren PDAs para firmar.
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {invoke_signed(instruction, account_infos, &[])}
Los siguientes ejemplos muestran cómo hacer un CPI usando el Anchor Framework y Rust nativo. Los programas de ejemplo incluyen una única instrucción que transfiere SOL de una cuenta a otra usando un CPI.
Framework Anchor
Los siguientes ejemplos presentan tres formas de crear Invocaciones entre Programas (CPIs) en un programa Anchor, cada una con un nivel diferente de abstracción. Todos los ejemplos funcionan de la misma manera. El propósito principal es mostrar los detalles de implementación de un CPI.
- Ejemplo 1: Utiliza
CpiContext
de Anchor y función auxiliar para construir la instrucción CPI. - Ejemplo 2: Utiliza la función
system_instruction::transfer
del cratesolana_program
para construir la instrucción CPI. El ejemplo 1 abstrae esta implementación. - Ejemplo 3: Construye la instrucción CPI manualmente. Este enfoque es útil cuando no existe un crate que ayude a construir la instrucción.
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>,}
Rust nativo
El siguiente ejemplo muestra cómo hacer un CPI desde un programa escrito en Rust nativo. El programa incluye una única instrucción que transfiere SOL de una cuenta a otra utilizando un CPI. El archivo de prueba utiliza LiteSVM para probar el programa.
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(())}}}
Invocaciones entre programas con firmantes PDA
La función
invoke_signed
maneja CPIs que requieren firmantes PDA. La función toma las seeds para
derivación de firmantes PDA como signer_seeds
.
Puedes consultar la página Program Derived Address para obtener detalles sobre cómo derivar PDAs.
pub fn invoke_signed(instruction: &Instruction,account_infos: &[AccountInfo],signers_seeds: &[&[&[u8]]],) -> ProgramResult {// --snip--invoke_signed_unchecked(instruction, account_infos, signers_seeds)}
Al procesar una instrucción que incluye un CPI, el runtime de Solana
internamente llama a
create_program_address
utilizando el signers_seeds
y el program_id
del programa que realiza la
llamada. Cuando se verifica un PDA válido, la dirección es
añadida como firmante válido.
Los siguientes ejemplos demuestran cómo hacer un CPI con firmantes PDA utilizando el Framework Anchor y Rust nativo. Los programas de ejemplo incluyen una única instrucción que transfiere SOL desde un PDA a la cuenta del destinatario utilizando un CPI firmado por el PDA.
Framework Anchor
Los siguientes ejemplos incluyen tres enfoques para implementar Invocaciones entre Programas (CPIs) en un programa Anchor, cada uno con un nivel diferente de abstracción. Todos los ejemplos son funcionalmente equivalentes. El propósito principal es ilustrar los detalles de implementación de un CPI.
- Ejemplo 1: Utiliza
CpiContext
de Anchor y una función auxiliar para construir la instrucción CPI. - Ejemplo 2: Utiliza la función
system_instruction::transfer
del cratesolana_program
para construir la instrucción CPI. El Ejemplo 1 es una abstracción de esta implementación. - Ejemplo 3: Construye la instrucción CPI manualmente. Este enfoque es útil cuando no hay un crate disponible para ayudar a construir la instrucción que deseas invocar.
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>,}
Rust nativo
El siguiente ejemplo muestra cómo realizar un CPI con firmantes PDA desde un programa escrito en Rust nativo. El programa incluye una única instrucción que transfiere SOL desde un PDA a la cuenta del destinatario utilizando un CPI firmado por el PDA. El archivo de prueba utiliza LiteSVM para probar el programa.
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?