La Invocación entre programas (CPI) es la forma en que los programas llaman a otros programas en la blockchain de Solana. En este tutorial, aprenderemos cómo hacer llamadas CPI en Rust nativo.
Ya hemos utilizado CPI en nuestros tutoriales anteriores de Anchor al transferir SOL o acuñar tokens a través del programa SPL Token. En Anchor, una llamada CPI se ve así:
let cpi_ctx = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
}
);
system_program::transfer(cpi_ctx, amount)?;
El código anterior crea un contexto CPI con el System Program y las cuentas necesarias para una transferencia, luego llama a system_program::transfer para ejecutar el CPI.
Este tutorial explica qué sucede entre bastidores cuando realizas llamadas CPI en Anchor, y luego muestra cómo usar las funciones CPI nativas de Solana directamente.
Cubriremos:
- Las dos funciones CPI principales en Solana:
invokeeinvoke_signed - Cómo Anchor abstrae estas funciones principales
- Cómo construir instrucciones CPI manualmente en Rust nativo mostrando un ejemplo práctico donde construiremos dos programas: un programa destino que devuelve 42 y un programa llamador que lo invoca vía CPI
Comencemos por entender las funciones CPI invoke e invoke_signed.
Comprendiendo las funciones CPI principales de Solana
Solana tiene dos funciones principales para realizar Invocaciones entre programas:
invoke: Se utiliza para llamadas CPI que no requieren firma de PDA (utiliza los firmantes originales de la transacción)invoke_signed: Se utiliza para llamadas CPI que requieren firma de PDA (cuando un programa necesita firmar en nombre de una PDA que controla)
Veamos estas funciones en detalle:
1. La función invoke
La función invoke llama a otro programa con cuentas y datos de instrucción. Se utiliza cuando tu programa necesita invocar a otro programa utilizando los firmantes originales de la transacción para la autorización.
pub fn invoke(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
) -> ProgramResult
Los parámetros son:
instruction: UnaInstructionstruct que contiene:program_id: La clave pública del programa destinoaccounts: Un vector de structsAccountMeta. Cada struct contiene tres campos:pubkey(la clave pública de la cuenta),is_signer(si esta cuenta debe firmar la transacción), eis_writable(si el programa puede modificar esta cuenta)data: Un arreglo de bytes que contiene los datos de la instrucción. Normalmente incluye un discriminador (para identificar qué instrucción ejecutar) seguido de cualquier parámetro que la instrucción espere. El diseño exacto es definido por el programa destino
account_infos: Un slice de structsAccountInfo. Esto debe incluir todas las cuentas referenciadas en el campoaccountsde la instrucción, más la cuenta del programa destino. El runtime las utiliza para acceder a los datos reales de la cuenta durante la ejecución
AccountMeta en la instrucción le dice a Solana qué cuentas necesitas y cómo serán utilizadas. AccountInfo proporciona los datos reales de la cuenta y el estado en el que tu programa lee o escribe.
2. La función invoke_signed
La función invoke_signed llama a otro programa con cuentas y datos de instrucción, al igual que invoke, pero se utiliza cuando tu programa debe firmar en nombre de una PDA. Así es como funciona:
- Al usar
invoke_signed, debes proporcionar las semillas utilizadas para derivar la PDA - El runtime utiliza estas semillas para verificar que tu programa derivó la PDA (es decir, la PDA pertenece a tu programa)
- Esto permite que tu programa firme en nombre de una PDA, ya que las PDA no tienen claves privadas y no pueden firmar directamente
pub fn invoke_signed(
instruction: &Instruction,
account_infos: &[AccountInfo<'_>],
signers_seeds: &[&[&[u8]]],
) -> ProgramResult
Esta función tiene los mismos parámetros que invoke, con un parámetro adicional:
signers_seeds: Las semillas de derivación para las PDA que necesitan firmar la instrucción CPI. El runtime utiliza estas semillas para volver a derivar la PDA y verificar que pertenece a tu programa.
Ahora que entendemos las funciones CPI nativas que proporciona Solana, veamos cómo las utiliza Anchor.
Cómo Anchor abstrae las funciones CPI de Solana
Anchor reduce la complejidad de construir llamadas CPI proporcionando dos enfoques que envuelven las funciones invoke e invoke_signed:
1. Llamadas CPI regulares (usando los firmantes originales de la transacción):
Anchor utiliza CpiContext::new() para envolver la función invoke nativa cuando las cuentas requeridas por el programa invocado ya son firmantes en la transacción original. Aquí hay un ejemplo transfiriendo SOL a través del System Program:
let cpi_ctx = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
}
);
system_program::transfer(cpi_ctx, amount)?;
2. Llamadas CPI firmadas por PDA (cuando tu programa necesita firmar en nombre de una PDA):
Anchor utiliza CpiContext::new_with_signer() para envolver la función invoke_signed nativa cuando una PDA controlada por tu programa debe firmar la instrucción invocada. El tercer parámetro (&[&seeds]) proporciona las semillas para derivar y firmar con la PDA:
let seeds = &[
b"seed-prefix",
payer.key.as_ref(),
&[bump],
];
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: ctx.accounts.from.to_account_info(),
to: ctx.accounts.to.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
&[&seeds],
);
token::transfer(cpi_ctx, amount)?;
Después de construir el contexto de la instrucción con cualquiera de los dos enfoques, llamas a la función auxiliar CPI apropiada de Anchor (como system_program::transfer o token::transfer) con el contexto.
Entre bastidores, Anchor utiliza invoke_signed para todas las llamadas CPI. Esto se debe a que:
- Sin semillas de firmante, funciona exactamente como
invoke - Con semillas de firmante, habilita la firma de PDA
Este enfoque unificado significa que Anchor solo necesita una ruta de código para todas las operaciones CPI.
La función invoke funciona de esta manera porque su implementación utiliza la función invoke_signed pero pasa un slice de bytes vacío para las semillas de firmante de la PDA.
pub fn invoke(instruction: &Instruction, account_infos: &[AccountInfo]) -> ProgramResult {
invoke_signed(instruction, account_infos, &[])
}
Dentro del runtime BPF de Solana, tanto invoke como invoke_signed llaman a la syscall sol_invoke_signed_rust. Esta syscall realiza la invocación entre programas real suspendiendo la ejecución del llamador, invocando el programa destino, gestionando la pila de llamadas y verificando las firmas derivadas de PDA cuando se proporcionan semillas de firmante. También hay una variante de ABI del lenguaje C, sol_invoke_signed_c, que expone el mismo comportamiento para los programas escritos en el lenguaje de programación C.
Para ver cómo Anchor usa invoke_signed, inspeccionemos la función system_program::transfer en el código fuente de Anchor. Nota que construye una instrucción usando system_instruction::transfer, y luego llama a invoke_signed con ctx.signer_seeds:
pub fn transfer<'info>(
ctx: CpiContext<'_, '_, '_, 'info, Transfer<'info>>,
lamports: u64,
) -> Result<()> {
let ix = crate::solana_program::system_instruction::transfer(
ctx.accounts.from.key,
ctx.accounts.to.key,
lamports,
);
crate::solana_program::program::invoke_signed(
&ix,
&[ctx.accounts.from, ctx.accounts.to],
ctx.signer_seeds,
)
.map_err(Into::into)
}
El mismo patrón es visible en la función transfer del token SPL desde el crate de Anchor SPL token:
pub fn transfer<'info>(
ctx: CpiContext<'_, '_, '_, 'info, Transfer<'info>>,
amount: u64,
) -> Result<()> {
let ix = spl_token::instruction::transfer(
&spl_token::ID,
ctx.accounts.from.key,
ctx.accounts.to.key,
ctx.accounts.authority.key,
&[],
amount,
)?;
anchor_lang::solana_program::program::invoke_signed(
&ix,
&[ctx.accounts.from, ctx.accounts.to, ctx.accounts.authority],
ctx.signer_seeds,
)
.map_err(Into::into)
}
Nota en ambos ejemplos que el código utiliza sistemáticamente invoke_signed con ctx.signer_seeds. Como se mencionó anteriormente, cuando no se proporcionan semillas (CPI regular), signer_seeds vacío hace que invoke_signed se comporte exactamente como invoke. Cuando se proporcionan semillas (firma de PDA), permite al programa firmar en nombre de sus PDAs.
Ahora entendemos cómo Anchor abstrae las invocaciones entre programas. A continuación, aprenderemos cómo construir instrucciones CPI manualmente.
Construyendo instrucciones CPI manualmente en Rust nativo
Para construir y realizar una llamada CPI en un programa de Solana en Rust nativo, necesitamos construir una instrucción y luego llamar a la función CPI apropiada.
Para hacer esto, tendremos que:
- Crear una instrucción con el
program_iddel programa al que queremos llamar, una lista de cuentas que el programa necesitará leer o escribir durante la CPI y los datos de la instrucción a enviar. - Luego llamamos a la función CPI apropiada (
invokeoinvoke_signed) con la instrucción y las cuentas
Como se mencionó al inicio de este tutorial, crearemos dos programas de Solana separados que trabajan juntos:
- Un programa destino: que devuelve el número 42 al ser llamado (veremos cómo más adelante)
- Un programa llamador: que realiza llamadas CPI al programa destino usando la función
invoke.
Al implementar ambos programas, podemos observar el proceso completo de CPI desde ambas perspectivas y ver cómo fluyen los datos entre los programas.
Antes de crear nuestros programas, configuremos la estructura de nuestro proyecto:
mkdir solana-cpi-example
cd solana-cpi-example
Creando el programa destino
Mantendremos el programa destino simple, simplemente devolverá un valor para el programa llamador.
Primero, creamos el directorio del programa destino y lo inicializamos (dentro de solana-cpi-example):
mkdir target-program
cd target-program
cargo init --lib
Actualiza target-program/Cargo.toml:
[package]
name = "target-program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
solana-program = "1.18"
Reemplaza el código en target-program/src/lib.rs con este código. En este programa, nosotros:
- Importamos las dependencias necesarias de Solana incluyendo
set_return_datadel cratesolana_program(explicamos esto después del código a continuación) - Definimos una función
process_instructionque:- Crea una variable con el valor 42, y
- Utiliza
set_return_datapara devolver este valor al llamador
// target-program/src/lib.rs
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
program::set_return_data,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
// Create the data we want to return
let return_value: u64 = 42;
let return_bytes = return_value.to_le_bytes();
// Set the return data that the calling program can access
set_return_data(&return_bytes);
Ok(())
}
La función set_return_data
La función set_return_data (del crate solana_program) almacena datos en un búfer que el programa llamador puede leer después de que la CPI retorna. El tamaño máximo de los datos de retorno es de 1024 bytes.
Convertimos nuestro valor u64 a bytes y lo configuramos como datos de retorno. Necesitamos esto porque el tipo de retorno ProgramResult solo indica éxito o fracaso al runtime, no datos reales. Solana introdujo set_return_data para habilitar el paso directo de datos entre programas sin requerir cuentas adicionales. En Anchor, esto se maneja automáticamente a través de los tipos de retorno en tus funciones de instrucción.
Veremos cómo recuperar estos datos cuando construyamos el programa llamador a continuación.
Ahora construye el programa destino:
cargo build-sbf
Creando el programa llamador
A continuación, implementaremos el programa que realiza las llamadas CPI al programa destino. Esto demostrará cómo un programa puede llamar a otro.
El programa llamador manejará las llamadas CPI y la recuperación de datos desde el programa destino.
Volvamos a la raíz del proyecto y creemos nuestro programa llamador:
cd ..
mkdir caller-program
cd caller-program
cargo init --lib
Actualiza caller-program/Cargo.toml:
[package]
name = "caller-program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
solana-program = "1.18"
Reemplaza el código en caller-program/src/lib.rs con el código a continuación. En este programa, nosotros:
- Importamos las dependencias necesarias de Solana incluyendo
get_return_datadel cratesolana_program - Definimos una función
process_instructionpara nuestro programa que:- Extrae el ID del programa destino de la primera cuenta en el arreglo de cuentas
- Construye una instrucción CPI sin datos de instrucción
- Realiza la llamada CPI usando
invoke() - Recupera los datos retornados usando
get_return_data() - Registra el valor real recibido del programa destino
Exploraremos la construcción de la CPI en detalle después del bloque de código.
// caller-program/src/lib.rs
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
instruction::{AccountMeta, Instruction},
msg,
program::{invoke, get_return_data},
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
msg!("Caller program: Making CPI call to target program");
// Extract the target program's program_id from the accounts array
let target_program_account = &accounts[0];
let target_program_id = *target_program_account.key;
// In production, verify the program ID matches expected value and is executable:
// assert!(target_program_id == EXPECTED_PROGRAM_ID);
// assert!(target_program_account.executable);
// Build CPI instruction with empty data
let instruction = Instruction {
program_id: target_program_id,
accounts: vec![], // No additional accounts needed for our simple target program
data: vec![], // No instruction data needed
};
// Make the CPI call using invoke
// We pass target_program_account (an AccountInfo from our accounts parameter)
// because invoke needs the AccountInfo of the program we're calling
invoke(&instruction, &[target_program_account.clone()])?;
// Get the return data from the called program
let (_, return_data) = get_return_data().unwrap();
let value = u64::from_le_bytes(return_data[0..8].try_into().unwrap());
msg!("Caller program: Target program returned {}", value);
Ok(())
}// caller-program/src/lib.rs
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
instruction::{AccountMeta, Instruction},
msg,
program::{invoke, get_return_data},
pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
msg!("Caller program: Making CPI call to target program");
// Extract the target program's program_id from the accounts array
let target_program_account = &accounts[0];
let target_program_id = *target_program_account.key;
// In production, verify the program ID matches expected value and is executable:
// assert!(target_program_id == EXPECTED_PROGRAM_ID);
// assert!(target_program_account.executable);
// Build CPI instruction with empty data
let instruction = Instruction {
program_id: target_program_id,
accounts: vec![], // No additional accounts needed for our simple target program
data: vec![], // No instruction data needed
};
// Make the CPI call using invoke
// We pass target_program_account (an AccountInfo from our accounts parameter)
// because invoke needs the AccountInfo of the program we're calling
invoke(&instruction, &[target_program_account.clone()])?;
// Get the return data from the called program
let (_, return_data) = get_return_data().unwrap();
let value = u64::from_le_bytes(return_data[0..8].try_into().unwrap());
msg!("Caller program: Target program returned {}", value);
Ok(())
}
La función get_return_data
La función get_return_data permite que nuestro programa llamador recupere datos del programa que acabamos de llamar.
Esta función devuelve Option<(Pubkey, Vec<u8>)>—una tupla que contiene el ID del programa que configuró los datos y los datos en sí. Solo nos importan los datos de retorno (el segundo elemento), por lo que lo desestructuramos como let (_, return_data) = get_return_data().unwrap(). Luego convertimos esos bytes de nuevo a nuestro tipo de datos esperado (u64 en este caso).
cargo build-sbf
Desglosando la construcción de la CPI en el programa llamador
Primero, extrajimos el ID del programa destino del arreglo de cuentas:

Luego construimos la instrucción que define nuestra llamada CPI:

Este struct Instruction tiene tres componentes:
program_id: La dirección del programa al que estamos llamando (el ID del programa destino en este caso)accounts: La lista de cuentas que necesita nuestra instrucción (vacía en este caso)data: Cualquier dato de instrucción para pasar (también vacío)
Finalmente, ejecutamos la CPI usando la función invoke:

La función invoke toma:
- La instrucción que construimos
- Una lista de todos los account infos necesarios para la instrucción
Si el programa llamado devuelve un ProgramError, el error se propaga al runtime, la ejecución se detiene inmediatamente y toda la transacción falla. El control no regresa al programa llamador.
Pero cuando la función invoke se ejecuta con éxito, esto es lo que sucede:
- El programa llamador invoca a
invoke()con la instrucción y los account infos - El runtime de Solana suspende el programa llamador y transfiere la ejecución al programa destino
- El programa destino se ejecuta, llama a
set_return_data(&return_bytes)para almacenar el valor 42, y devuelveOk(()) - El runtime reanuda el programa llamador desde donde lo dejó (justo después de la llamada a
invoke()) - El programa llamador llama a
get_return_data()para recuperar los bytes del búfer - Convierte esos bytes de nuevo a un valor u64 (42) y lo registra
Desplegando y probando ambos programas
Desplegando ambos programas
Ahora que hemos escrito ambos programas —el programa destino y el llamador que realiza una CPI— podemos desplegarlos para probar que nuestra implementación funciona.
Ejecuta los siguientes comandos en pestañas de terminal separadas para iniciar un test validator local y ver los registros (logs):
solana-test-validator # in a separate terminal
solana logs # in another terminal
Despliega primero el programa destino:
cd target-program
solana program deploy target/deploy/target_program.so
Verás una salida mostrando el ID del programa. Copia este Program ID—lo necesitarás al configurar la prueba del cliente.

Despliega el programa llamador:
cd ../caller-program
solana program deploy target/deploy/caller_program.so
Copia también este Program ID.
Probando ambos programas
Con ambos programas desplegados, necesitamos activar nuestro programa llamador y observar los resultados. Haremos esto con un cliente TypeScript.
Crearemos un cliente TypeScript que envía una transacción a nuestro programa llamador. Este cliente inicia el proceso: el cliente llama al programa llamador, el cual luego realiza llamadas CPI al programa destino.
Ahora, volvamos a la raíz del proyecto y configuremos nuestro cliente TypeScript:
cd ..
mkdir client && cd client
npm init -y
npm install @solana/web3.js typescript ts-node @types/node
Actualiza client/package.json:
{
"scripts": {
"test": "ts-node client.ts"
}
}
Crea client/tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["*.ts"]
}
A continuación, crea client/client.ts y añade el código de abajo. En este código del cliente, nosotros:
- Configuramos la conexión al clúster local de Solana
- Definimos las constantes para los IDs del programa destino y llamador (
TARGET_PROGRAM_IDyCALLER_PROGRAM_ID) - Creamos y financiamos una cuenta firmante con SOL para pruebas
- Creamos una instrucción que llama a nuestro programa llamador
- Pasamos el ID del programa destino como una cuenta para que el programa llamador sepa a qué programa llamar vía CPI
- Ejecutamos la transacción y mostramos la firma de la transacción
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
Transaction,
TransactionInstruction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
// Replace these with your actual program IDs
const TARGET_PROGRAM_ID = new PublicKey("YOUR_TARGET_PROGRAM_ID_HERE");
const CALLER_PROGRAM_ID = new PublicKey("YOUR_CALLER_PROGRAM_ID_HERE");
const connection = new Connection("http://localhost:8899", "confirmed");
async function testCPI() {
console.log("Testing Cross-Program Invocation\n");
// Create and fund a signer account
const signer = Keypair.generate();
console.log("Funding signer account...");
await connection.requestAirdrop(signer.publicKey, LAMPORTS_PER_SOL);
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log(`Signer: ${signer.publicKey.toString()}`);
console.log(`Target Program: ${TARGET_PROGRAM_ID.toString()}`);
console.log(`Caller Program: ${CALLER_PROGRAM_ID.toString()}\n`);
// Create instruction to call our caller program
// The caller program expects the target program ID as the first account
const instruction = new TransactionInstruction({
keys: [{ pubkey: TARGET_PROGRAM_ID, isSigner: false, isWritable: false }],
programId: CALLER_PROGRAM_ID,
data: Buffer.alloc(0), // No instruction data needed
});
const transaction = new Transaction().add(instruction);
console.log("Executing CPI test...");
const signature = await sendAndConfirmTransaction(
connection,
transaction,
[signer],
{
commitment: "confirmed",
preflightCommitment: "confirmed",
},
);
}
testCPI().catch(console.error);
Asegúrate de reemplazar TARGET_PROGRAM_ID y CALLER_PROGRAM_ID con los IDs de programa reales de tus despliegues.
Ahora ejecuta la prueba:
npm run test # inside the client/ directory
Si miramos los registros, podemos ver que la llamada CPI del programa llamador al programa destino fue exitosa, y que el programa llamador recibió el valor 42 del programa destino, el cual registró.

Resumen de la ejecución del programa
Cuando ejecutamos nuestra prueba de programa, ocurre una secuencia de eventos. Aquí tienes un resumen de lo que sucede:
- El cliente TypeScript llama al programa llamador
- El programa llamador prepara la instrucción CPI y llama a
invoke() - El runtime de Solana cambia la ejecución al programa destino
- El programa destino se ejecuta, almacena el valor de retorno 42 y devuelve
Ok(()) - Finalmente, el runtime cambia la ejecución de vuelta al programa llamador, que recupera y registra el valor 42
Próximos pasos
No utilizamos invoke_signed en este tutorial porque requiere crear cuentas y PDAs en Rust nativo, lo cual aún no hemos cubierto. Veremos un ejemplo de invoke_signed en otro tutorial cuando aprendamos sobre la creación de PDAs en programas en Rust nativo.
Ahora, construye el programa llamador:
Esta función devuelve Option<(Pubkey, Vec<u8>)>—una tupla que contiene el ID del programa que configuró los datos y los datos en sí. Solo nos importan los datos de retorno (el segundo elemento), por lo que lo desestructuramos como let (_, return_data) = get_return_data().unwrap(). Luego convertimos esos bytes de nuevo a nuestro tipo de datos esperado (u64 en este caso).
La función get_return_data permite a nuestro programa llamador recuperar datos del programa que acabamos de llamar.
Este mecanismo permite que los programas no solo se llamen entre sí, sino que también devuelvan datos al llamador.
- Convierte esos bytes de nuevo a un valor u64 (42) y lo registra
- El programa llamador llama a
get_return_data()para recuperar los bytes del búfer - El runtime reanuda el programa llamador desde donde lo dejó (justo después de la llamada a
invoke()) - El programa destino se ejecuta, llama a
set_return_data(&return_bytes)para almacenar el valor 42, y devuelveOk(()) - El runtime de Solana suspende el programa llamador y transfiere la ejecución al programa destino
Este artículo es parte de una serie de tutoriales sobre desarrollo en Solana.