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:

  1. InitializeCounter: Cria e inicializa uma nova conta com um valor inicial.
  2. 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.

Terminal
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

Terminal
cd counter_program

Em seguida, adicione a dependência solana-program. Esta é a dependência mínima necessária para construir um programa Solana.

Terminal
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.

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

Seu arquivo Cargo.toml deve se parecer com o seguinte:

Cargo.toml
[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:

  1. Importa as dependências necessárias de solana_program
  2. Define o ponto de entrada do programa usando a macro entrypoint!
  3. Implementa a função process_instruction que irá direcionar instruções para as funções manipuladoras apropriadas
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(())
}

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: O AccountInfo para contas requeridas pela instrução sendo invocada
  • instruction_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:

  1. Adicione o crate borsh como dependência ao seu Cargo.toml:
Terminal
cargo add borsh
  1. 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.

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 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 0
IncrementCounter, // 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:

  1. Separa o primeiro byte para obter a variante da instrução
  2. Faz correspondência na variante e analisa quaisquer parâmetros adicionais dos bytes restantes
  3. 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 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),
}
}
}

Adicione o seguinte código ao lib.rs para definir as instruções para o programa de 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),
}
}
}

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:

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(())
}

Em seguida, adicione a implementação da função process_initialize_counter. Este manipulador de instrução:

  1. Cria e aloca espaço para uma nova conta para armazenar os dados do contador
  2. Inicializa os dados da conta com initial_value passado para a instrução

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(())
}

Em seguida, adicione a implementação da função process_increment_counter. Esta instrução incrementa o valor de uma conta 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(())
}

Teste de instruções

Para testar as instruções do programa, adicione as seguintes dependências ao Cargo.toml.

Terminal
cargo add solana-program-test@1.18.26 --dev
cargo add solana-sdk@1.18.26 --dev
cargo 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.

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);
}
}
}

Exemplo de saída:

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?

Índice

Editar Página