La introspección de instrucciones permite que un programa de Solana lea una instrucción distinta a la suya dentro de la misma transacción.
Normalmente, un programa solo puede leer la instrucción dirigida a sí mismo. El entorno de ejecución (runtime) de Solana enruta cada instrucción al programa especificado en la instrucción.
Una transacción de Solana puede contener múltiples instrucciones, cada una dirigida a un programa diferente. Por ejemplo, el programa A podría recibir la instrucción Ax y el programa B la instrucción Bx en la misma transacción. Mediante la introspección, el programa B puede leer el contenido tanto de la instrucción Ax como de la Bx.
Por ejemplo, supongamos que deseas asegurarte de que cualquier interacción con tu programa DeFi deba ir precedida de una transferencia de 0.5 SOL a tu tesorería dentro de la misma transacción. Puedes hacer cumplir esta regla inspeccionando las instrucciones y rechazando la transacción completa si la instrucción de transferencia de 0.5 SOL requerida no se incluye antes de la instrucción que interactúa con tu programa.
En este artículo, aprenderemos cómo funciona la introspección y cómo implementarla en tu programa de Solana.
Transacción e instrucciones
Antes de echar un vistazo a la introspección de instrucciones, revisemos las transacciones y las instrucciones en detalle.
Una transacción de Solana es un struct con dos campos: un mensaje y las firmas que lo autorizaron. El mensaje contiene un array de instrucciones que se ejecutarán de forma secuencial.

El código a continuación (que proviene directamente del Solana SDK) muestra la representación en struct de una transacción:
pub struct Transaction {
pub signatures: Vec<Signature>,
pub message: Message,
}
El mensaje de la transacción
El mensaje de la transacción contiene la lista de instrucciones y la unión de todas las claves de cuenta (account keys) a las que las instrucciones accederán en conjunto. También contiene algunos datos adicionales que el entorno de ejecución necesita, como el recent block hash y el encabezado del mensaje (message header).
pub struct Message {
pub instructions: Vec<Instruction>,
pub account_keys: Vec<Address>,
pub recent_blockhash: Hash,
pub header: MessageHeader,
}
Aquí hay un desglose detallado de cada componente:
- Instructions: Cada instrucción es una llamada a un programa on-chain. Una instrucción contiene tres componentes:
- Program ID: La dirección del programa que contiene la lógica de negocio para la instrucción que se está llamando.
- Accounts: Índices hacia las account keys de la transacción. Los índices mapean la instrucción a las cuentas específicas de las que necesita leer o en las que necesita escribir.
- Instruction data: Un array de bytes que especifica qué función llamar en el programa y cualquier argumento requerido por la instrucción.
- Account keys: Es la unión de todas las cuentas listadas en cada una de las instrucciones.
- Recent blockhash: Un hash de bloque reciente que vincula la transacción a una ventana corta de slots y previene ataques de repetición (replay).
- Message header: Especifica cuántas cuentas han firmado la transacción y qué cuentas son de solo lectura frente a las que son de escritura.
Struct Instruction
A continuación se muestra la definición del struct Instruction, del código fuente de Solana en GitHub:
pub struct Instruction {
/// Pubkey of the program that executes this instruction.
pub program_id: Pubkey,
/// Metadata describing accounts that should be passed to the program.
pub accounts: Vec<AccountMeta>,
/// Opaque data passed to the program for its own interpretation.
pub data: Vec<u8>,
}
pub struct AccountMeta {
/// An account's public key.
pub pubkey: Pubkey,
// True if the instruction requires a signature for this pubkey
/// in the transaction's signatures list.
pub is_signer: bool,
/// True if the account data or metadata may be mutated during program execution.
pub is_writable: bool,
}
Cada una de las cuentas que utiliza una instrucción está representada por el tipo AccountMeta, el cual almacena la clave pública de la cuenta junto con los indicadores (flags) de firmante (signer) y modificable (writable).
Resumen de la relación entre transacciones e instrucciones
Para juntarlo todo, la imagen a continuación muestra las relaciones entre una transacción, un mensaje y las instrucciones.
Una Transaction contiene una lista de firmas y un mensaje. Un Message contiene un encabezado, una lista de account keys, un recent blockhash y una lista de instrucciones. Una Instruction contiene un program ID, las cuentas que utiliza (que se indexan en la lista de account keys dentro del struct Message) y los datos de la instrucción.

Introspección de instrucciones con el Sysvar de instrucciones
Hablemos de cómo funciona la introspección examinando primero la cuenta Sysvar de Solana.
Un sysvar es una cuenta especial de solo lectura que contiene datos actualizados dinámicamente, mantenida por el entorno de ejecución de Solana y que expone el estado interno de la red a los programas. Estamos literalmente leyendo los datos de esta cuenta — no estamos haciendo una CPI a un programa.
Hemos analizado los diferentes tipos de Sysvars en un artículo anterior de esta serie. Para aprender más sobre ellos, lee el artículo “Solana Sysvars Explained”.
La introspección de instrucciones utiliza la cuenta Sysvar de instrucciones para acceder al vector serializado de instrucciones (program_id, accounts y data) de la transacción actual. Por ejemplo, en una transacción con múltiples instrucciones, un programa puede leer y analizar cualquiera de las instrucciones, no solo la instrucción actual.
Esta animación muestra un escenario de introspección de instrucciones donde, mientras se ejecuta la Instruction 1, el programa puede leer el contenido de la Instruction 2 y de la Instruction 3.
A diferencia de las cuentas normales en Solana, la cuenta Sysvar de instrucciones no persiste los datos; solo se llena durante la vida útil de la transacción y se borra una vez que se completa la ejecución.
La dirección de la cuenta Sysvar de instrucciones es Sysvar1nstructions1111111111111111111111111. Contiene la lista serializada de todas las instrucciones de la transacción actual. Cada entrada incluye el program ID, accounts y los datos de la instrucción, tal como vimos anteriormente. A continuación se muestra el struct de Rust de cada instrucción deserializada, reproducido de antes:
pub struct Instruction {
/// Pubkey of the program that executes this instruction
pub program_id: Pubkey,
/// Metadata describing accounts that should be passed to the program
pub accounts: Vec<AccountMeta>,
/// Opaque data passed to the program for its own interpretation
pub data: Vec<u8>,
}
El SDK de Rust de Solana proporciona varias funciones auxiliares para acceder a las instrucciones serializadas en la cuenta sysvar de instrucciones. Sin embargo, el SDK no proporciona una única función que devuelva todas las instrucciones; en su lugar, solo proporciona funciones que deserializan una sola instrucción en un índice específico.
Todavía puedes leer y deserializar la lista de instrucciones en la cuenta sysvar de forma manual, pero hacerlo es propenso a errores, por lo tanto, se debe usar el SDK para deserializar las instrucciones.
Aquí están las dos funciones auxiliares clave que proporciona el SDK de Rust de Solana para la introspección:
load_current_index_checked– Los programas pueden usar esta función auxiliar para conocer su propio índice dentro de la lista de transacciones y luego buscar otra instrucción mediante su posición relativa.load_instruction_at_checked– Carga la instrucción en un índice específico y la deserializa en un structInstruction. Una vez que tienes el índice actual usando la funciónload_current_index_checked, puedes usar esta función para inspeccionar instrucciones anteriores o posteriores. Veremos cómo hacer esto en una sección posterior de este artículo.
Primero, para entender cómo funcionan estas funciones auxiliares, veamos la estructura (layout) de la cuenta sysvar de instrucciones. Está organizada en tres regiones:
- El encabezado (header)
- Las instrucciones
- Y el índice de la instrucción que se está ejecutando actualmente
1. La región del encabezado
El encabezado especifica el número de instrucciones en la transacción y los desplazamientos (offsets) de las instrucciones (que apuntan a donde comienzan las instrucciones). El diagrama a continuación muestra un encabezado para una transacción con 2 instrucciones, por lo que hay dos offsets: uno que comienza en la ubicación de memoria 6 y el otro en la ubicación de memoria 20.

2. La región de instrucciones
La región de instrucciones comienza en la posición del byte indicada por el offset (el cuadro rojo en el diagrama a continuación es solo un marcador visual para el offset, no una ubicación de memoria real). A partir de esa posición, contiene los metadatos de la cuenta, el program ID, la longitud de los datos de la instrucción y, finalmente, los datos de la instrucción en sí. Si tenemos más de una instrucción, esta estructura se repite para cada una.

3. El índice de la instrucción que se está ejecutando actualmente
Y finalmente, el índice de la instrucción que se está ejecutando actualmente se almacena al final de la estructura del Sysvar.

Si el programa conoce el índice de la instrucción que se está ejecutando actualmente, puede obtener las otras instrucciones relativas a él.
Acceso a las instrucciones
Ahora que hemos analizado cómo están organizados los datos en la cuenta Sysvar, veamos un ejemplo práctico. Usaremos los dos métodos auxiliares para la introspección: load_current_index_checked y load_instruction_at_checked, para acceder a las instrucciones en una transacción. Para el propósito de este artículo, usaremos una transacción de transferencia básica.
Nuestro programa de ejemplo verificará que una instrucción de transferencia del sistema precede a su propia instrucción. La transacción tendrá éxito solo si se cumple esta condición.
Transaction:
├── Instruction 0: System Transfer (user pays X lamports)
└── Instruction 1: This program (verifies the payment)
Configuración del programa
Para seguir el tutorial, deberías tener configurado un entorno de desarrollo de Solana. Si no lo has hecho, lee nuestro primer artículo de esta serie.
Inicializa una nueva aplicación de Anchor:
anchor init instruction-introspection
Actualiza tu dependencia en program/src/Cargo.toml para incluir bincode (bincode=1.3.3). Usaremos la librería bincode para deserializar la instrucción del sistema:
//... rest of toml file content
[dependencies]
anchor-lang = "0.31.1"
**bincode = "1.3.3" # add this**
Usaremos Devnet para este proyecto. Crea un archivo .env en tu directorio raíz y añade los exports del provider y de la wallet que se muestran a continuación:
export ANCHOR_PROVIDER_URL=https://api.devnet.solana.com
export ANCHOR_WALLET=~/.config/solana/id.json
Actualiza también el archivo Anchor.toml para utilizar el provider y la wallet de Devnet.
[provider]
cluster = "https://api.devnet.solana.com"
wallet = "~/.config/solana/id.json"
Además, dado que necesitarás algunos SOL para pagar las tarifas en Devnet, ejecuta solana airdrop 2 para obtener 2 SOL, lo cual será más que suficiente para este ejemplo.
Importaciones
Ahora, importaremos las dependencias de Anchor que usaremos para este ejemplo para reemplazar el código en el archivo program/src/lib.rs. Es importante destacar que importamos load_instruction_at_checked y load_current_index_checked de sysvar::instructions:
use anchor_lang::prelude::*;
use anchor_lang::solana_program::{
system_program,
sysvar::instructions::{
load_instruction_at_checked,
load_current_index_checked
},
system_instruction::SystemInstruction,
};
Y luego declararemos el program ID y añadiremos una función verify_transfer, la cual hará lo siguiente:
- Obtener el índice de la instrucción actual para entender la posición de la transacción que se está ejecutando.
- Cargar la instrucción anterior deserializando la lista de instrucciones en la cuenta sysvar utilizando el SDK de Rust de Solana on-chain.
- Verificar que la instrucción cargada es una instrucción de transferencia del sistema (system transfer) comprobando que el program ID coincida con el System Program; luego, parsear los datos de la instrucción para confirmar que la cantidad de la transferencia coincida con la cantidad esperada.
- Verificar que el número de cuentas involucradas en la instrucción sea 2.
- Y finalmente, definiremos el struct para la cuenta sysvar.
Mira el código completo a continuación. Hemos añadido comentarios para señalar los pasos enumerados anteriormente:
use anchor_lang::prelude::*;
use anchor_lang::solana_program::{
system_program,
sysvar::instructions::{load_instruction_at_checked, load_current_index_checked},
system_instruction::SystemInstruction,
};
declare_id!("BxQuawTcvJkT2JM1qKeW6wyM4i5VCuM122v9tVsSrmwm");
#[program]
pub mod check_transfer {
use super::*;
pub fn verify_transfer(ctx: Context<VerifyTransfer>, expected_amount: u64) -> Result<()> {
// Step 1: Get current instruction index to understand our position
**let current_ix_index = load_current_index_checked(&ctx.accounts.instruction_sysvar)?;**
msg!("Currently executing instruction index: {}", current_ix_index);
// Step 2: Load the previous instruction
let transfer_ix = load_instruction_at_checked(
(current_ix_index - 1) as usize,
&ctx.accounts.instruction_sysvar
).map_err(|_| error!(ErrorCode::MissingInstruction))?;
// Step 3: Verify it's a system program instruction
require_keys_eq!(transfer_ix.program_id, system_program::ID, ErrorCode::NotSystemProgram);
// Step 4: Parse the system instruction data
let system_ix = bincode::deserialize(&transfer_ix.data)
.map_err(|_| error!(ErrorCode::InvalidInstructionData))?;
match system_ix {
SystemInstruction::Transfer { lamports } => {
require_eq!(lamports, expected_amount, ErrorCode::IncorrectAmount);
msg!("✅ Verified transfer of {} lamports", lamports);
}
_ => return Err(error!(ErrorCode::NotTransferInstruction)),
}
// Step 5: Verify accounts involved in the transfer
require_gte!(transfer_ix.accounts.len(), 2, ErrorCode::InsufficientAccounts);
let from_account = &transfer_ix.accounts[0];
let to_account = &transfer_ix.accounts[1];
require!(from_account.is_signer, ErrorCode::FromAccountNotSigner);
require!(from_account.is_writable, ErrorCode::FromAccountNotWritable);
require!(to_account.is_writable, ErrorCode::ToAccountNotWritable);
msg!("✅ Transfer accounts properly configured");
msg!("From: {}", from_account.pubkey);
msg!("To: {}", to_account.pubkey);
Ok(())
}
}
#[derive(Accounts)]
pub struct VerifyTransfer<'info> {
/// CHECK: This is the instruction sysvar account
#[account(address = anchor_lang::solana_program::sysvar::instructions::ID)]
pub instruction_sysvar: AccountInfo<'info>,
}
Aquí están los códigos de error que usamos, debes añadirlos al mismo archivo:
#[error_code]
pub enum ErrorCode {
/// Thrown when attempting to load an instruction at an index that doesn't exist
/// in the transaction (e.g., trying to access index -1 when current is 0)
#[msg("Missing required instruction in transaction")]
MissingInstruction,
/// Thrown when the previous instruction's program_id doesn't match the System Program
/// Ensures we're only validating actual system program instructions
#[msg("Instruction is not from System Program")]
NotSystemProgram,
/// Thrown when bincode fails to deserialize the instruction data into SystemInstruction
/// Indicates malformed or corrupted instruction data
#[msg("Invalid instruction data format")]
InvalidInstructionData,
/// Thrown when the SystemInstruction variant is not Transfer
/// (e.g., it's CreateAccount, Allocate, or another system instruction type)
#[msg("Instruction is not a transfer")]
NotTransferInstruction,
/// Thrown when the actual lamports amount in the transfer doesn't equal expected_amount
/// Protects against front-running or incorrect payment amounts
#[msg("Transfer amount does not match expected amount")]
IncorrectAmount,
/// Thrown when the transfer instruction has fewer than 2 accounts
/// A valid transfer requires at least [from, to] accounts
#[msg("Transfer instruction has insufficient accounts")]
InsufficientAccounts,
/// Thrown when the 'from' account in the transfer didn't sign the transaction
/// Prevents unauthorized transfers
#[msg("From account is not a signer")]
FromAccountNotSigner,
/// Thrown when the 'from' account is not marked as writable
/// Required because the account balance will be debited
#[msg("From account is not writable")]
FromAccountNotWritable,
/// Thrown when the 'to' account is not marked as writable
/// Required because the account balance will be credited
#[msg("To account is not writable")]
ToAccountNotWritable,
}
En el código anterior, obtuvimos nuestro índice de instrucción actual y usamos el ID para cargar la instrucción anterior y así inspeccionarla. Podemos cargarla simplemente restando 1 al índice actual, ya que las instrucciones están en orden secuencial.
Ahora, construyamos e implementemos el programa, e interactuemos con él usando JavaScript.
Ejecuta anchor build && anchor deploy para construir e implementar el proyecto. Deberías ver una salida como esta que demuestra que se implementó correctamente:

Interactuando con el código del programa usando Typescript
Crea un script simple en Typescript para transferir 1 SOL a una dirección con nuestro programa.
Para ejecutar los archivos de Typescript directamente, usarás bun.js. Si aún no lo tienes instalado, puedes instalarlo ejecutando curl -fsSL [https://bun.sh/install](https://bun.sh/install) | bash en tu terminal.
Crea una carpeta scripts/, añade un archivo introspect.ts y pega el código a continuación en él. He añadido comentarios para ayudarte a entender el flujo de ideas en el código.
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { SystemProgram, SYSVAR_INSTRUCTIONS_PUBKEY, Transaction, Keypair } from "@solana/web3.js";
import { CheckTransfer } from "../target/types/check_transfer";
async function main() {
console.log("🚀 Starting verification script...");
// --- Setup Connection and Program ---
// Configure the client to use the local cluster.
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
// Load the Anchor program from the workspace.
const program = anchor.workspace.CheckTransfer as Program<CheckTransfer>;
// --- Prepare Accounts and Data ---
// The 'payer' is the wallet that signs and pays for the transaction.
const payer = provider.wallet.publicKey;
// A new, random keypair to act as the recipient.
const recipient = Keypair.generate().publicKey;
// Define the transfer amount using anchor.BN for u64 safety.
const transferAmount = new anchor.BN(1_000_000_000); // 1 SOL
console.log(`- Payer: ${payer}`);
console.log(`- Recipient: ${recipient}`);
console.log(`- Amount: ${transferAmount.toString()} lamports`);
// --- Build the Transaction ---
// A transaction is a container for one or more instructions.
const tx = new Transaction();
// Instruction 0: The System Program Transfer.
// This must immediately precede our program's instruction.
tx.add(
SystemProgram.transfer({
fromPubkey: payer,
toPubkey: recipient,
lamports: transferAmount.toNumber(), // Safe for 1 SOL
})
);
// Instruction 1: Our program's verification instruction.
tx.add(
await program.methods
.verifyTransfer(transferAmount)
.accounts({
instructionSysvar: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.instruction()
);
// --- Send Transaction and Verify Outcome ---
try {
const sig = await provider.sendAndConfirm(tx);
console.log("\n✅ Transaction confirmed!");
console.log(`Signature: ${sig}`);
// Fetch the transaction details to inspect the logs.
const txInfo = await provider.connection.getTransaction(sig, {
commitment: "confirmed",
maxSupportedTransactionVersion: 0,
});
console.log("\n📄 Program Logs:");
console.log(txInfo?.meta?.logMessages?.join("\n"));
// Check for the success message in the logs.
const logs = txInfo?.meta?.logMessages;
if (!logs || !logs.some(log => log.includes(`Verified transfer of ${transferAmount} lamports`))) {
throw new Error("Verification log message not found!");
}
console.log("\n✅ Verification successful!");
} catch (error) {
console.error("\n❌ Transaction failed!");
console.error(error);
process.exit(1); // Exit with a non-zero error code
}
}
// --- Script Entrypoint ---
main().then(
() => process.exit(0),
err => {
console.error(err);
process.exit(1);
}
);
Cuando ejecutamos el código del cliente con bun run script/introspect.ts, deberíamos ver que funciona con una salida como esta:

Precaución para la introspección de instrucciones: evita usar índices absolutos durante la inspección
Cargar una instrucción desde un índice absoluto como 0 desde la cuenta sysvar puede permitir a un atacante reutilizar esa instrucción en múltiples llamadas.
Por ejemplo, si tu programa requiere que un usuario transfiera fondos a tu tesorería antes de retirar en la misma transacción, usar un índice absoluto podría permitirle a un atacante colocar una sola transferencia en el índice 0 y luego realizar múltiples retiros que se validarían todos con esa misma transferencia.
En su lugar, usa un indexado de instrucciones relativo para asegurarte de que la transferencia ocurra inmediatamente antes de la instrucción de retiro, tal como mostramos anteriormente en nuestro ejemplo.
let transfer_ix = load_instruction_at_checked(
(current_ix_index - 1) as usize,
&ctx.accounts.instruction_sysvar
)
Esto asegura que la instrucción inspeccionada es la transferencia correcta para el retiro actual, no una transferencia reutilizada de un momento anterior en la transacción.
Este artículo es parte de una serie de tutoriales sobre Solana.