A lo largo de esta serie, hemos utilizado el framework Anchor para construir programas en Solana. Este tutorial te enseña cómo escribirlos en Rust nativo sin Anchor.
Hay varias razones por las que podrías querer hacer esto, tales como:
- Control de bajo nivel: Tienes control sobre cómo serializas y deserializas datos, validas cuentas y estructuras la lógica de tu programa
- Rendimiento: En Rust nativo, puedes omitir los pasos de serialización, deserialización y validación de cuentas de Anchor para operaciones simples cuando no son necesarios, lo que resultará en un menor uso de unidades de cómputo
- Menor tamaño del binario: Sin la sobrecarga de Anchor (macros y dependencias adicionales de Rust) significa programas desplegados más pequeños
- Aprendizaje: Comprender las mecánicas subyacentes te convierte en un mejor desarrollador de Solana
Hasta ahora, hemos estado usando Anchor para crear nuestros programas y hemos escrito nuestras funciones así:
#[program]
pub mod my_program {
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// function logic
Ok(())
}
pub fn update(ctx: Context<Update>, value: u64) -> Result<()> {
// function logic
Ok(())
}
}
La macro de atributo #[program] genera automáticamente un punto de entrada (entrypoint) del programa en segundo plano. Este entrypoint recibe todas las instrucciones entrantes y las envía a tus funciones individuales (initialize, update, etc.) basándose en los datos de la instrucción enviados por el cliente. En Rust nativo, usaremos la macro entrypoint! del SDK de Solana para crear el entrypoint y manejar el envío manualmente.
¿Qué es un Entrypoint en Solana?
Piensa en un entrypoint como la “puerta principal” de tu programa en Solana. En Ethereum, cada función pública es como tener múltiples puertas principales: la EVM puede llamar a funciones públicas como transfer(), approve() en ERC20, o cualquier otra función pública directamente. Solana funciona de manera diferente. Los programas en Solana tienen exactamente una puerta principal (el entrypoint) que maneja todas las llamadas entrantes de los clientes.
La función del entrypoint no la escribimos directamente nosotros. Se genera en tiempo de compilación mediante una macro entrypoint! proporcionada por el SDK de Solana (como veremos más adelante cuando escribamos nuestro programa nativo). Cuando un cliente invoca un programa en Solana, el runtime llama al entrypoint, que deserializa la instrucción entrante y la pasa a una función procesadora de instrucciones que nosotros definimos (discutiremos esto a continuación). El procesador de instrucciones puede enrutar las instrucciones a la función correcta del programa, realizar la validación de cuentas o manejar la lógica de negocio directamente.
Por lo tanto, la macro entrypoint! maneja todo el código de bajo nivel que el runtime requiere para invocar tu programa, deserializar los parámetros de la instrucción y reenviarlos al procesador de instrucciones. Esto te permite escribir la lógica de tu programa utilizando funciones y tipos normales de Rust, mientras la macro gestiona la interfaz con Solana.
El Procesador de Instrucciones
En los programas de Solana en Rust nativo, necesitamos definir un procesador de instrucciones: una función que procesa las instrucciones entrantes. Cuando un cliente envía una instrucción a tu programa, el runtime de Solana invoca el entrypoint de tu programa, el cual luego deserializa los parámetros de instrucción de nivel superior y los pasa a tu función procesadora de instrucciones. Así es como tu programa recibe y procesa cada instrucción.
Este procesador de instrucciones tiene una firma de función estándar:
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult
Los tres parámetros de la función process_instruction son:
program_id: La dirección de tu programaaccounts: Todas las cuentas de las que tu programa necesita leer o en las que necesita escribirinstruction_data: Bytes sin procesar que contienen los datos de la instrucción serializados para tu programa
El tipo de retorno ProgramResult es un alias de tipo para Result<(), ProgramError>, donde ProgramError es un enum que define los posibles errores que un programa de Solana puede devolver.
En Anchor, los parámetros crudos de process_instruction y el tipo de retorno están ocultos. En su lugar, tu manejador recibe un Context<T> que contiene cuentas y datos de instrucción completamente deserializados, con validación automática aplicada, para que puedas trabajar directamente con structs tipados en lugar de slices de bytes crudos.
Puedes nombrar a esta función procesadora de instrucciones como quieras, pero la convención en el ecosistema de Solana es process_instruction. Esta es la función que pasas a la macro entrypoint! (como discutimos anteriormente).
Ahora vamos a escribir un programa en Solana con un procesador de instrucciones conectado mediante la macro entrypoint! y a ejecutarlo. Probaremos nuestro programa con un cliente en TypeScript para ver cómo funciona en la práctica.
Construyendo Nuestro Programa de Solana
Configuración del Proyecto
Construiremos un programa de Solana sencillo con un procesador de instrucciones que realiza aritmética básica y registra el resultado. Esto demostrará cómo el entrypoint y el procesador de instrucciones trabajan juntos en la práctica.
Deberías tener Rust y Solana instalados localmente si has estado siguiendo tutoriales anteriores. Si no, por favor consulta Solana Hello World (Installation and Troubleshooting).
Ahora vamos a crear un nuevo directorio para nuestro programa y a inicializarlo ejecutando los siguientes comandos:
mkdir solana-entrypoint-tutorial # Crea un nuevo directorio para nuestro programa
cd solana-entrypoint-tutorial # Entra en el directorio
cargo init --lib # Inicializa una nueva biblioteca de Rust
A continuación, actualiza el Cargo.toml del proyecto para que se vea así:
[package]
name = "solana-entrypoint-tutorial"
version = "0.1.0"
edition = "2021" # añadido
## RECIÉN AÑADIDO ##
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
solana-program = "3.0.0"
Hemos añadido dos configuraciones:
crate-type = ["cdylib", "lib"]: Le dice a Rust que compile nuestra biblioteca como una biblioteca dinámica que Solana puede cargarsolana-program = "3.0.0": La biblioteca principal de programas de Solana que proporciona todos los tipos y funciones que necesitamos para programas on-chain
Ahora vamos a crear nuestro programa.
Escribiendo el Código de Nuestro Programa
Empezaremos con un procesador de instrucciones que realiza aritmética básica y registra el resultado.
En Anchor, podrías definir una función que haga matemáticas básicas dentro de tu módulo #[program] así:
#[program]
pub mod some_program {
pub fn do_math(ctx: Context<DoMath>) -> Result<()> {
let result = 5 + 3;
msg!("Result: {}", result);
Ok(())
}
}
Pero para los programas de Solana en Rust nativo, definimos un procesador de instrucciones y lo conectamos al entrypoint del programa usando la macro entrypoint!. Aunque puedes definir otras funciones públicas, estas deben ser llamadas desde el procesador de instrucciones, ya que toda la ejecución comienza allí.
La macro entrypoint! hace el trabajo pesado: genera el código real del entrypoint al que llama el runtime de Solana, deserializa los datos entrantes y los envía a tu función procesadora de instrucciones. De esta manera, escribes la lógica de negocio en tu procesador de instrucciones mientras la macro maneja la configuración del entrypoint de bajo nivel.
Ahora reemplaza el código por defecto en src/lib.rs con el siguiente código. En el código, nosotros:
- Importamos las dependencias necesarias del programa de Solana para nuestro programa:
AccountInfo,entrypoint,ProgramResult,msgyPubkey. - Usamos la macro
entrypoint!para conectarprocess_instructioncomo el procesador de instrucciones de nuestro programa. - Definimos una función procesadora de instrucciones llamada
process_instructioncon los tres parámetros estándar (todos con guiones bajos porque aún no los estamos usando). - Realizamos aritmética simple sumando dos números codificados en duro (5 + 3).
- Usamos la macro
msg!para registrar el resultado del cálculo en los registros de la transacción. - Devolvemos
Ok(())para indicar una ejecución exitosa.
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
msg,
pubkey::Pubkey,
};
entrypoint!(process_instruction); // Registra process_instruction como nuestro procesador de instrucciones
pub fn process_instruction(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let a: u64 = 5;
let b: u64 = 3;
let result = a + b;
msg!("Calculating {} + {} = {}", a, b, result);
Ok(())
}
Comprendiendo la Macro entrypoint!
Anteriormente, mencionamos que Solana proporciona una macro entrypoint! para conectar tu procesador de instrucciones al entrypoint del programa.
En el código anterior, la macro entrypoint! hace tres cosas:
- Genera la función real del entrypoint a la que llama el runtime de Solana
- Deserializa la entrada del runtime (que contiene el program id, accounts e instruction data)
- Llama a tu función procesadora de instrucciones (
process_instruction) con los parámetros deserializados
Cuando escribes entrypoint!(process_instruction);, se expande a un código que se ve así:
#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
// Deserializa la entrada cruda del runtime de Solana
// `input` es memoria cruda del runtime (sólo bytes)
let (program_id, accounts, instruction_data) = unsafe { deserialize(input) };
// Llama a tu función procesadora de instrucciones con los datos deserializados
// program_id: &Pubkey
// accounts: Vec<AccountInfo>
// instruction_data: &[u8]
match process_instruction(program_id, &accounts, instruction_data) {
Ok(()) => 0, // Devuelve 0 en caso de éxito
Err(error) => error.into(), // Devuelve código de error en caso de fallo
}
}
Esta función generada es el puente entre el runtime de Solana y tu código en Rust. El runtime llama a este entrypoint con un puntero a los datos de la instrucción en la memoria (input: *mut u8). Este puntero señala a una ubicación de memoria que contiene los parámetros de la instrucción serializados (program ID, accounts e instruction data) como bytes crudos. La función deserialize(input) lee desde esta ubicación de memoria y convierte esos bytes en tres valores:
program_id(que ya es un&Pubkey),accounts(unVec<AccountInfo>), yinstruction_data(que ya es un&[u8]).
En el SDK de Solana, la firma de la función deserialize se define como:
pub unsafe fn deserialize<'a>(input: *mut u8) -> (&Pubkey, Vec<AccountInfo>, &[u8])
En la llamada process_instruction(program_id, &accounts, instruction_data), solo accounts necesita un &.
Eso se debe a que deserialize devuelve program_id e instruction_data como referencias directamente (&Pubkey y &[u8]—como vemos en la firma anterior), pero accounts como un Vec<AccountInfo>.
En el código generado, &accounts crea un &Vec<AccountInfo>, el cual Rust convierte automáticamente en el slice &[AccountInfo] que process_instruction espera.
La macro entrypoint! te permite concentrarte en implementar process_instruction mientras la macro maneja la interacción con el runtime de Solana. Puedes ver la implementación completa de la macro entrypoint aquí.
Por otro lado, en Anchor, el atributo #[program] genera automáticamente el entrypoint, deserializa los datos de la instrucción y las cuentas, y envía las instrucciones a las funciones correspondientes.
Ahora, vamos a compilar y desplegar nuestro programa para que podamos probarlo.
Construir y desplegar el programa
Para construir y desplegar el programa, ejecuta los siguientes comandos:
cargo build-sbf --tools-version v1.52
solana-test-validator # en otra terminal
solana program deploy target/deploy/solana_entrypoint_tutorial.so
Esto es lo que hace cada comando:
cargo build-sbf: Construye nuestro programa de Rust para el runtime de Solana y crea una carpetatarget/deploy/que contiene un archivo.so(objeto compartido) que es nuestro programa compilado. A diferencia del comando de compilación de Anchor, esto no genera un IDL ni incluye los discriminadores de Anchor ni el código de validación automática. Esto resulta en binarios más pequeños ya que no hay sobrecarga del framework. El flag--tools-version v1.52fija la toolchain de la plataforma de Solana para la compilación. Esto asegura una versión compatible de Rust y Cargo y evita problemas por herramientas incompatibles o desactualizadas.solana-test-validator: Al igual que en tutoriales anteriores, usamos esto para iniciar un validador local de Solana para pruebas (ejecuta esto en una terminal separada)solana program deploy: Toma el archivo.socreado por el comando de construcción y lo despliega en el validador local
Después de desplegar en el nodo del validador local, deberías ver algo como esto.

Copia el Program ID de la salida de despliegue, lo necesitarás para las pruebas.
Si obtienes un error de compilación, asegúrate de que tu toolchain de Solana esté actualizada ejecutando este comando: curl --proto '=https' --tlsv1.2 -sSfL https://solana-install.solana.workers.dev | bash
Etapa de Pruebas 1: Aritmética Básica y Registros (Logging)
Ahora vamos a crear un cliente en TypeScript para probar nuestro programa de entrypoint básico. Primero, necesitamos configurar el entorno del cliente, luego agregaremos el código del cliente.
Configura el cliente de TypeScript dentro de nuestro proyecto con los siguientes comandos:
mkdir client && cd client
npm init -y
npm install @solana/web3.js typescript ts-node @types/node
Reemplaza el script test por defecto en client/package.json con lo siguiente:
"scripts": {
"test": "ts-node client.ts"
},
Esto nos permite ejecutar nuestro cliente de TypeScript con npm run test, ya que el script de prueba por defecto generado por npm init no soporta código TypeScript.
Crea un client/tsconfig.json y añade esto:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": [
"node"
]
},
"include": [
"*.ts"
]
}
Esta configuración nos permitirá ejecutar el cliente con npm run test.
Ahora crea un archivo client/client.ts y añade el siguiente código. En este código, nosotros:
- Importamos las dependencias necesarias de web3.js de Solana para crear conexiones, transacciones y pares de claves.
- Configuramos una conexión al validador local de Solana que se ejecuta en el puerto 8899 (el puerto por defecto para
solana-test-validator). - Creamos una función
testBasicEntrypointque genera un nuevo par de claves para pagar la transacción. - Solicitamos un airdrop de SOL para financiar las tarifas de la transacción.
- Creamos una
TransactionInstructionsin cuentas y sin datos de instrucción (ya que nuestro programa aún no los usa). - Construimos y enviamos la transacción a nuestro programa.
- Registramos la firma de la transacción para su verificación.
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
Transaction,
TransactionInstruction,
sendAndConfirmTransaction,
} from '@solana/web3.js';
import { Buffer } from 'buffer';
// === REEMPLAZA CON TU PROGRAM ID REAL === //
const PROGRAM_ID = new PublicKey('7x8574zHWf6cRQJrE5T3cfUdcgDi6Vt6q7HhLfHkHZQ4'); // Reemplaza con tu program ID real
const connection = new Connection('http://localhost:8899', 'confirmed');
async function testBasicEntrypoint() {
const payer = Keypair.generate();
// Airdrop de algo de SOL para pagar la transacción
await connection.requestAirdrop(payer.publicKey, LAMPORTS_PER_SOL);
await new Promise(resolve => setTimeout(resolve, 1000)); // Espera al airdrop
// Crea una instrucción que llama a nuestro programa
const instruction = new TransactionInstruction({
keys: [], // keys es el arreglo de metadatos de cuentas; no se necesitan cuentas para este ejemplo sencillo
programId: PROGRAM_ID,
data: Buffer.alloc(0), // No se necesitan datos de instrucción
});
const transaction = new Transaction().add(instruction);
console.log('Calling our program...');
const signature = await sendAndConfirmTransaction(connection, transaction, [payer]);
console.log(`Transaction confirmed: ${signature}`);
}
testBasicEntrypoint().catch(console.error);
Reemplaza el ID del programa en PROGRAM_ID con tu program ID real.
Antes de ejecutar la prueba, asegúrate de que el validador local sigue funcionando y el programa ha sido desplegado. Luego ejecuta solana logs en una nueva terminal para observar los registros de nuestro programa.
Ahora ejecuta la prueba con:
cd client
npm run test
Deberías ver un registro del programa como este:

Nuestro programa se ejecutó con éxito. Nota lo mucho más sencillo que es esto en comparación con Anchor: sin validación de cuentas ni análisis de instrucciones.
Este artículo es parte de una serie de tutoriales sobre el desarrollo en Solana.