Estructura de programa en Rust

Los programas de Solana escritos en Rust tienen requisitos estructurales mínimos, lo que permite flexibilidad en la organización del código. El único requisito es que un programa debe tener un entrypoint, que define dónde comienza la ejecución de un programa.

Estructura del programa

Aunque no existen reglas estrictas para la estructura de archivos, los programas de Solana típicamente siguen un patrón común:

  • entrypoint.rs: Define el punto de entrada que dirige las instrucciones entrantes.
  • state.rs: Define el estado específico del programa (datos de cuenta).
  • instructions.rs: Define las instrucciones que el programa puede ejecutar.
  • processor.rs: Define los manejadores de instrucciones (funciones) que implementan la lógica de negocio para cada instrucción.
  • error.rs: Define errores personalizados que el programa puede devolver.

Puedes encontrar ejemplos en la Solana Program Library.

Programa de ejemplo

Para demostrar cómo construir un programa nativo en Rust con múltiples instrucciones, veremos un programa contador simple que implementa dos instrucciones:

  1. InitializeCounter: Crea e inicializa una nueva cuenta con un valor inicial.
  2. IncrementCounter: Incrementa el valor almacenado en una cuenta existente.

Para simplificar, el programa se implementará en un solo archivo lib.rs, aunque en la práctica es posible que quieras dividir programas más grandes en múltiples archivos.

Crear un nuevo programa

Primero, crea un nuevo proyecto de Rust usando el comando estándar cargo init con la bandera --lib.

Terminal
cargo init counter_program --lib

Navega al directorio del proyecto. Deberías ver los archivos predeterminados src/lib.rs y Cargo.toml

Terminal
cd counter_program

A continuación, añade la dependencia solana-program. Esta es la dependencia mínima requerida para construir un programa de Solana.

Terminal
cargo add solana-program@1.18.26

A continuación, añade el siguiente fragmento a Cargo.toml. Si no incluyes esta configuración, el directorio target/deploy no se generará cuando construyas el programa.

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

Tu archivo Cargo.toml debería verse como el siguiente:

Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
solana-program = "1.18.26"

Punto de entrada del programa

Un punto de entrada de un programa de Solana es la función que se llama cuando se invoca un programa. El punto de entrada tiene la siguiente definición básica y los desarrolladores son libres de crear su propia implementación de la función de punto de entrada.

Para simplificar, usa el macro entrypoint! del crate solana_program para definir el punto de entrada en tu programa.

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

Reemplaza el código predeterminado en lib.rs con el siguiente código. Este fragmento:

  1. Importa las dependencias requeridas de solana_program
  2. Define el punto de entrada del programa usando el macro entrypoint!
  3. Implementa la función process_instruction que dirigirá las instrucciones a las funciones de manejo apropiadas
lib.rs
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Your program logic
Ok(())
}

El macro entrypoint! requiere una función con la siguiente firma de tipo como argumento:

pub type ProcessInstruction =
fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;

Cuando se invoca un programa de Solana, el punto de entrada deserializa los datos de entrada (proporcionados como bytes) en tres valores y los pasa a la función process_instruction:

  • program_id: La clave pública del programa que se está invocando (programa actual)
  • accounts: El AccountInfo para las cuentas requeridas por la instrucción que se está invocando
  • instruction_data: Datos adicionales pasados al programa que especifican la instrucción a ejecutar y sus argumentos requeridos

Estos tres parámetros corresponden directamente a los datos que los clientes deben proporcionar al construir una instrucción para invocar un programa.

Definir el estado del programa

Al construir un programa de Solana, normalmente comenzarás definiendo el estado de tu programa - los datos que se almacenarán en las cuentas creadas y propiedad de tu programa.

El estado del programa se define utilizando estructuras de Rust que representan la disposición de datos de las cuentas de tu programa. Puedes definir múltiples estructuras para representar diferentes tipos de cuentas para tu programa.

Cuando trabajas con cuentas, necesitas una forma de convertir los tipos de datos de tu programa a y desde los bytes sin procesar almacenados en el campo de datos de una cuenta:

  • Serialización: Convertir tus tipos de datos en bytes para almacenarlos en el campo de datos de una cuenta
  • Deserialización: Convertir los bytes almacenados en una cuenta de nuevo en tus tipos de datos

Aunque puedes usar cualquier formato de serialización para el desarrollo de programas en Solana, Borsh es comúnmente utilizado. Para usar Borsh en tu programa de Solana:

  1. Añade el crate borsh como dependencia a tu Cargo.toml:
Terminal
cargo add borsh
  1. Importa los traits de Borsh y usa la macro derive para implementar los traits para tus estructuras:
use borsh::{BorshSerialize, BorshDeserialize};
// Define struct representing our counter account's data
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
count: u64,
}

Añade la estructura CounterAccount a lib.rs para definir el estado del programa. Esta estructura se utilizará tanto en la inicialización como en las instrucciones de incremento.

lib.rs
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
use borsh::{BorshSerialize, BorshDeserialize};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Your program logic
Ok(())
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
count: u64,
}

Definir instrucciones

Las instrucciones se refieren a las diferentes operaciones que tu programa de Solana puede realizar. Piensa en ellas como APIs públicas para tu programa - definen qué acciones pueden realizar los usuarios cuando interactúan con tu programa.

Las instrucciones se definen típicamente usando un enum de Rust donde:

  • Cada variante del enum representa una instrucción diferente
  • La carga útil de la variante representa los parámetros de la instrucción

Ten en cuenta que las variantes de enumeración de Rust se numeran implícitamente comenzando desde 0.

A continuación se muestra un ejemplo de una enumeración que define dos instrucciones:

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}

Cuando un cliente invoca tu programa, debe proporcionar instruction data (como un buffer de bytes) donde:

  • El primer byte identifica qué variante de instrucción ejecutar (0, 1, etc.)
  • Los bytes restantes contienen los parámetros de instrucción serializados (si son necesarios)

Para convertir los instruction data (bytes) en una variante de la enumeración, es común implementar un método auxiliar. Este método:

  1. Separa el primer byte para obtener la variante de instrucción
  2. Hace una coincidencia con la variante y analiza cualquier parámetro adicional de los bytes restantes
  3. Devuelve la variante de enumeración correspondiente

Por ejemplo, el método unpack para la enumeración CounterInstruction:

impl CounterInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// Get the instruction variant from the first byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let initial_value = u64::from_le_bytes(
rest.try_into()
.map_err(|_| ProgramError::InvalidInstructionData)?
);
Ok(Self::InitializeCounter { initial_value })
}
1 => Ok(Self::IncrementCounter), // No additional data needed
_ => Err(ProgramError::InvalidInstructionData),
}
}
}

Agrega el siguiente código a lib.rs para definir las instrucciones para el programa contador.

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg,
program_error::ProgramError, pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Your program logic
Ok(())
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}
impl CounterInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// Get the instruction variant from the first byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let initial_value = u64::from_le_bytes(
rest.try_into()
.map_err(|_| ProgramError::InvalidInstructionData)?,
);
Ok(Self::InitializeCounter { initial_value })
}
1 => Ok(Self::IncrementCounter), // No additional data needed
_ => Err(ProgramError::InvalidInstructionData),
}
}
}

Manejadores de instrucciones

Los manejadores de instrucciones se refieren a las funciones que contienen la lógica de negocio para cada instrucción. Es común nombrar las funciones manejadoras como process_<instruction_name>, pero eres libre de elegir cualquier convención de nomenclatura.

Agrega el siguiente código a lib.rs. Este código usa la enumeración CounterInstruction y el método unpack definido en el paso anterior para dirigir las instrucciones entrantes a las funciones manejadoras apropiadas:

lib.rs
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Unpack instruction data
let instruction = CounterInstruction::unpack(instruction_data)?;
// Match instruction type
match instruction {
CounterInstruction::InitializeCounter { initial_value } => {
process_initialize_counter(program_id, accounts, initial_value)?
}
CounterInstruction::IncrementCounter => process_increment_counter(program_id, accounts)?,
};
}
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
// Implementation details...
Ok(())
}
fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
// Implementation details...
Ok(())
}

A continuación, agrega la implementación de la función process_initialize_counter. Este manejador de instrucciones:

  1. Crea y asigna espacio para una nueva cuenta para almacenar los datos del contador
  2. Inicializa los datos de la cuenta con initial_value pasado a la instrucción

lib.rs
// Initialize a new counter account
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
// Size of our counter account
let account_space = 8; // Size in bytes to store a u64
// Calculate minimum balance for rent exemption
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
// Create the counter account
invoke(
&system_instruction::create_account(
payer_account.key, // Account paying for the new account
counter_account.key, // Account to be created
required_lamports, // Amount of lamports to transfer to the new account
account_space as u64, // Size in bytes to allocate for the data field
program_id, // Set program owner to our program
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
// Create a new CounterAccount struct with the initial value
let counter_data = CounterAccount {
count: initial_value,
};
// Get a mutable reference to the counter account's data
let mut account_data = &mut counter_account.data.borrow_mut()[..];
// Serialize the CounterAccount struct into the account's data
counter_data.serialize(&mut account_data)?;
msg!("Counter initialized with value: {}", initial_value);
Ok(())
}

A continuación, añade la implementación de la función process_increment_counter. Esta instrucción incrementa el valor de una cuenta de contador existente.

lib.rs
// Update an existing counter's value
fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
// Verify account ownership
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// Mutable borrow the account data
let mut data = counter_account.data.borrow_mut();
// Deserialize the account data into our CounterAccount struct
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
// Increment the counter value
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
// Serialize the updated counter data back into the account
counter_data.serialize(&mut &mut data[..])?;
msg!("Counter incremented to: {}", counter_data.count);
Ok(())
}

Prueba de instrucciones

Para probar las instrucciones del programa, añade las siguientes dependencias a Cargo.toml.

Terminal
cargo add solana-program-test@1.18.26 --dev
cargo add solana-sdk@1.18.26 --dev
cargo add tokio --dev

Luego añade el siguiente módulo de prueba a lib.rs y ejecuta cargo test-sbf para ejecutar las pruebas. Opcionalmente, usa la bandera --nocapture para ver las declaraciones de impresión en la salida.

Terminal
cargo test-sbf -- --nocapture

lib.rs
#[cfg(test)]
mod test {
use super::*;
use solana_program_test::*;
use solana_sdk::{
instruction::{AccountMeta, Instruction},
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
#[tokio::test]
async fn test_counter_program() {
let program_id = Pubkey::new_unique();
let (mut banks_client, payer, recent_blockhash) = ProgramTest::new(
"counter_program",
program_id,
processor!(process_instruction),
)
.start()
.await;
// Create a new keypair to use as the address for our counter account
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
// Step 1: Initialize the counter
println!("Testing counter initialization...");
// Create initialization instruction
let mut init_instruction_data = vec![0]; // 0 = initialize instruction
init_instruction_data.extend_from_slice(&initial_value.to_le_bytes());
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&init_instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
// Send transaction with initialize instruction
let mut transaction =
Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &counter_keypair], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
// Check account data
let account = banks_client
.get_account(counter_keypair.pubkey())
.await
.expect("Failed to get counter account");
if let Some(account_data) = account {
let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data)
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 42);
println!(
"✅ Counter initialized successfully with value: {}",
counter.count
);
}
// Step 2: Increment the counter
println!("Testing counter increment...");
// Create increment instruction
let increment_instruction = Instruction::new_with_bytes(
program_id,
&[1], // 1 = increment instruction
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
// Send transaction with increment instruction
let mut transaction =
Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &counter_keypair], recent_blockhash);
banks_client.process_transaction(transaction).await.unwrap();
// Check account data
let account = banks_client
.get_account(counter_keypair.pubkey())
.await
.expect("Failed to get counter account");
if let Some(account_data) = account {
let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data)
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 43);
println!("✅ Counter incremented successfully to: {}", counter.count);
}
}
}

Ejemplo de salida:

Terminal
running 1 test
[2024-10-29T20:51:13.783708000Z INFO solana_program_test] "counter_program" SBF program from /counter_program/target/deploy/counter_program.so, modified 2 seconds, 169 ms, 153 µs and 461 ns ago
[2024-10-29T20:51:13.855204000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1]
[2024-10-29T20:51:13.856052000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2]
[2024-10-29T20:51:13.856135000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success
[2024-10-29T20:51:13.856242000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter initialized with value: 42
[2024-10-29T20:51:13.856285000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 3791 of 200000 compute units
[2024-10-29T20:51:13.856307000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success
[2024-10-29T20:51:13.860038000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1]
[2024-10-29T20:51:13.860333000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter incremented to: 43
[2024-10-29T20:51:13.860355000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 756 of 200000 compute units
[2024-10-29T20:51:13.860375000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success
test test::test_counter_program ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s

Is this page helpful?

Tabla de Contenidos

Editar Página