Estrutura de programa em Rust
Os programas Solana escritos em Rust têm requisitos estruturais mínimos,
permitindo flexibilidade na organização do código. O único requisito é que um
programa deve ter um entrypoint
, que define onde a execução de um programa se
inicia.
Estrutura do programa
Embora não existam regras rígidas para a estrutura de arquivos, os programas Solana geralmente seguem um padrão comum:
entrypoint.rs
: Define o ponto de entrada que direciona as instruções recebidas.state.rs
: Define o estado específico do programa (dados da conta).instructions.rs
: Define as instruções que o programa pode executar.processor.rs
: Define os manipuladores de instruções (funções) que implementam a lógica de negócios para cada instrução.error.rs
: Define erros personalizados que o programa pode retornar.
Você pode encontrar exemplos na Solana Program Library.
Exemplo de programa
Para demonstrar como construir um programa nativo em Rust com múltiplas instruções, vamos percorrer um programa contador simples que implementa duas instruções:
InitializeCounter
: Cria e inicializa uma nova conta com um valor inicial.IncrementCounter
: Incrementa o valor armazenado em uma conta existente.
Para simplificar, o programa será implementado em um único arquivo lib.rs
,
embora na prática você possa querer dividir programas maiores em vários
arquivos.
Criar um novo programa
Primeiro, crie um novo projeto Rust usando o comando padrão cargo init
com a
flag --lib
.
cargo init counter_program --lib
Navegue até o diretório do projeto. Você deverá ver os arquivos padrão
src/lib.rs
e Cargo.toml
cd counter_program
Em seguida, adicione a dependência solana-program
. Esta é a dependência mínima
necessária para construir um programa Solana.
cargo add solana-program@1.18.26
Em seguida, adicione o seguinte trecho ao Cargo.toml
. Se você não incluir esta
configuração, o diretório target/deploy
não será gerado quando você compilar o
programa.
[lib]crate-type = ["cdylib", "lib"]
Seu arquivo Cargo.toml
deve se parecer com o seguinte:
[package]name = "counter_program"version = "0.1.0"edition = "2021"[lib]crate-type = ["cdylib", "lib"][dependencies]solana-program = "1.18.26"
Ponto de entrada do programa
Um ponto de entrada de programa Solana é a função que é chamada quando um programa é invocado. O ponto de entrada tem a seguinte definição bruta e os desenvolvedores são livres para criar sua própria implementação da função de ponto de entrada.
Para simplificar, use a macro
entrypoint!
do crate solana_program
para definir o ponto de entrada no seu programa.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Substitua o código padrão em lib.rs
pelo seguinte código. Este trecho:
- Importa as dependências necessárias de
solana_program
- Define o ponto de entrada do programa usando a macro
entrypoint!
- Implementa a função
process_instruction
que irá direcionar instruções para as funções manipuladoras apropriadas
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(())}
A macro entrypoint!
requer uma função com a seguinte
assinatura de tipo
como argumento:
pub type ProcessInstruction =fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
Quando um programa Solana é invocado, o ponto de entrada
desserializa
os
dados de entrada
(fornecidos como bytes) em três valores e os passa para a função
process_instruction
:
program_id
: A chave pública do programa sendo invocado (programa atual)accounts
: OAccountInfo
para contas requeridas pela instrução sendo invocadainstruction_data
: Dados adicionais passados ao programa que especificam a instrução a ser executada e seus argumentos necessários
Estes três parâmetros correspondem diretamente aos dados que os clientes devem fornecer ao construir uma instrução para invocar um programa.
Definir o estado do programa
Ao construir um programa Solana, você geralmente começará definindo o estado do seu programa - os dados que serão armazenados em contas criadas e possuídas pelo seu programa.
O estado do programa é definido usando structs do Rust que representam o layout de dados das contas do seu programa. Você pode definir múltiplas structs para representar diferentes tipos de contas para o seu programa.
Ao trabalhar com contas, você precisa de uma maneira de converter os tipos de dados do seu programa para e dos bytes brutos armazenados no campo de dados de uma conta:
- Serialização: Convertendo seus tipos de dados em bytes para armazenar no campo de dados de uma conta
- Desserialização: Convertendo os bytes armazenados em uma conta de volta para seus tipos de dados
Embora você possa usar qualquer formato de serialização para o desenvolvimento de programas Solana, Borsh é comumente usado. Para usar Borsh no seu programa Solana:
- Adicione o crate
borsh
como dependência ao seuCargo.toml
:
cargo add borsh
- Importe os traits do Borsh e use a macro derive para implementar os traits para suas structs:
use borsh::{BorshSerialize, BorshDeserialize};// Define struct representing our counter account's data#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Adicione a struct CounterAccount
ao lib.rs
para definir o estado do
programa. Esta struct será usada tanto nas instruções de inicialização quanto 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 instruções
Instruções referem-se às diferentes operações que seu programa Solana pode executar. Pense nelas como APIs públicas para o seu programa - elas definem quais ações os usuários podem realizar ao interagir com seu programa.
As instruções são tipicamente definidas usando um enum do Rust onde:
- Cada variante do enum representa uma instrução diferente
- A carga útil da variante representa os parâmetros da instrução
Observe que as variantes de enum do Rust são implicitamente numeradas começando do 0.
Abaixo está um exemplo de um enum definindo duas instruções:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}
Quando um cliente invoca seu programa, ele deve fornecer instruction data (como um buffer de bytes) onde:
- O primeiro byte identifica qual variante de instrução executar (0, 1, etc.)
- Os bytes restantes contêm os parâmetros de instrução serializados (se necessário)
Para converter os instruction data (bytes) em uma variante do enum, é comum implementar um método auxiliar. Este método:
- Separa o primeiro byte para obter a variante da instrução
- Faz correspondência na variante e analisa quaisquer parâmetros adicionais dos bytes restantes
- Retorna a variante de enum correspondente
Por exemplo, o método unpack
para o enum 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),}}}
Adicione o seguinte código ao lib.rs
para definir as instruções para o
programa de 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),}}}
Manipuladores de Instrução
Manipuladores de instrução referem-se às funções que contêm a lógica de negócios
para cada instrução. É comum nomear funções manipuladoras como
process_<instruction_name>
, mas você é livre para escolher qualquer convenção
de nomenclatura.
Adicione o seguinte código ao lib.rs
. Este código usa o enum
CounterInstruction
e o método unpack
definido na etapa anterior para
direcionar as instruções recebidas para as funções manipuladoras apropriadas:
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(())}
Em seguida, adicione a implementação da função process_initialize_counter
.
Este manipulador de instrução:
- Cria e aloca espaço para uma nova conta para armazenar os dados do contador
- Inicializa os dados da conta com
initial_value
passado para a instrução
// 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(())}
Em seguida, adicione a implementação da função process_increment_counter
. Esta
instrução incrementa o valor de uma conta 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(())}
Teste de instruções
Para testar as instruções do programa, adicione as seguintes dependências ao
Cargo.toml
.
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo add tokio --dev
Em seguida, adicione o seguinte módulo de teste ao lib.rs
e execute
cargo test-sbf
para executar os testes. Opcionalmente, use a flag
--nocapture
para ver as declarações de impressão na saída.
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);}}}
Exemplo de saída:
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?