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:
InitializeCounter
: Crea e inicializa una nueva cuenta con un valor inicial.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
.
cargo init counter_program --lib
Navega al directorio del proyecto. Deberías ver los archivos predeterminados
src/lib.rs
y Cargo.toml
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.
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.
[lib]crate-type = ["cdylib", "lib"]
Tu archivo Cargo.toml
debería verse como el siguiente:
[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:
- Importa las dependencias requeridas de
solana_program
- Define el punto de entrada del programa usando el macro
entrypoint!
- Implementa la función
process_instruction
que dirigirá las instrucciones a las funciones de manejo apropiadas
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 logicOk(())}
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
: ElAccountInfo
para las cuentas requeridas por la instrucción que se está invocandoinstruction_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:
- Añade el crate
borsh
como dependencia a tuCargo.toml
:
cargo add borsh
- 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.
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 logicOk(())}#[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 0IncrementCounter, // 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:
- Separa el primer byte para obtener la variante de instrucción
- Hace una coincidencia con la variante y analiza cualquier parámetro adicional de los bytes restantes
- 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 bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet 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.
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 logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet 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:
entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Unpack instruction datalet instruction = CounterInstruction::unpack(instruction_data)?;// Match instruction typematch 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:
- Crea y asigna espacio para una nueva cuenta para almacenar los datos del contador
- Inicializa los datos de la cuenta con
initial_value
pasado a la instrucción
// Initialize a new counter accountfn 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 accountlet account_space = 8; // Size in bytes to store a u64// Calculate minimum balance for rent exemptionlet rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);// Create the counter accountinvoke(&system_instruction::create_account(payer_account.key, // Account paying for the new accountcounter_account.key, // Account to be createdrequired_lamports, // Amount of lamports to transfer to the new accountaccount_space as u64, // Size in bytes to allocate for the data fieldprogram_id, // Set program owner to our program),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;// Create a new CounterAccount struct with the initial valuelet counter_data = CounterAccount {count: initial_value,};// Get a mutable reference to the counter account's datalet mut account_data = &mut counter_account.data.borrow_mut()[..];// Serialize the CounterAccount struct into the account's datacounter_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.
// Update an existing counter's valuefn 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 ownershipif counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}// Mutable borrow the account datalet mut data = counter_account.data.borrow_mut();// Deserialize the account data into our CounterAccount structlet mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;// Increment the counter valuecounter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;// Serialize the updated counter data back into the accountcounter_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
.
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo 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.
cargo test-sbf -- --nocapture
#[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 accountlet counter_keypair = Keypair::new();let initial_value: u64 = 42;// Step 1: Initialize the counterprintln!("Testing counter initialization...");// Create initialization instructionlet mut init_instruction_data = vec![0]; // 0 = initialize instructioninit_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 instructionlet 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 datalet 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 counterprintln!("Testing counter increment...");// Create increment instructionlet increment_instruction = Instruction::new_with_bytes(program_id,&[1], // 1 = increment instructionvec![AccountMeta::new(counter_keypair.pubkey(), true)],);// Send transaction with increment instructionlet 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 datalet 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:
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 successtest test::test_counter_program ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
Is this page helpful?