Struttura del programma Rust

I programmi Solana scritti in Rust hanno requisiti strutturali minimi, permettendo flessibilità nell'organizzazione del codice. L'unico requisito è che un programma deve avere un entrypoint, che definisce dove inizia l'esecuzione di un programma.

Struttura del programma

Sebbene non ci siano regole rigide per la struttura dei file, i programmi Solana tipicamente seguono uno schema comune:

  • entrypoint.rs: Definisce l'entrypoint che indirizza le istruzioni in arrivo.
  • state.rs: Definiscono lo stato specifico del programma (dati dell'account).
  • instructions.rs: Definisce le istruzioni che il programma può eseguire.
  • processor.rs: Definisce i gestori di istruzioni (funzioni) che implementano la logica di business per ogni istruzione.
  • error.rs: Definisce gli errori personalizzati che il programma può restituire.

Puoi trovare esempi nella Solana Program Library.

Programma di esempio

Per dimostrare come costruire un programma Rust nativo con multiple istruzioni, esamineremo un semplice programma contatore che implementa due istruzioni:

  1. InitializeCounter: Crea e inizializza un nuovo account con un valore iniziale.
  2. IncrementCounter: Incrementa il valore memorizzato in un account esistente.

Per semplicità, il programma sarà implementato in un singolo file lib.rs, anche se nella pratica potresti voler dividere programmi più grandi in più file.

Creare un nuovo programma

Prima, crea un nuovo progetto Rust usando il comando standard cargo init con l'opzione --lib.

Terminal
cargo init counter_program --lib

Naviga nella directory del progetto. Dovresti vedere i file predefiniti src/lib.rs e Cargo.toml

Terminal
cd counter_program

Successivamente, aggiungi la dipendenza solana-program. Questa è la dipendenza minima richiesta per costruire un programma Solana.

Terminal
cargo add solana-program@1.18.26

Poi, aggiungi il seguente snippet a Cargo.toml. Se non includi questa configurazione, la directory target/deploy non verrà generata quando compili il programma.

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

Il tuo file Cargo.toml dovrebbe apparire come segue:

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

Entrypoint del programma

L'entrypoint di un programma Solana è la funzione che viene chiamata quando un programma è invocato. L'entrypoint ha la seguente definizione di base e gli sviluppatori sono liberi di creare la propria implementazione della funzione entrypoint.

Per semplicità, usa la macro entrypoint! dal crate solana_program per definire l'entrypoint nel tuo programma.

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

Sostituisci il codice predefinito in lib.rs con il seguente codice. Questo snippet:

  1. Importa le dipendenze necessarie da solana_program
  2. Definisce l'entrypoint del programma usando la macro entrypoint!
  3. Implementa la funzione process_instruction che indirizzerà le istruzioni alle funzioni handler appropriate
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(())
}

La macro entrypoint! richiede una funzione con la seguente firma di tipo come argomento:

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

Quando un programma Solana viene invocato, l'entrypoint deserializza i dati di input (forniti come byte) in tre valori e li passa alla funzione process_instruction:

  • program_id: La chiave pubblica del programma che viene invocato (programma corrente)
  • accounts: Il AccountInfo per gli account richiesti dall'istruzione che viene invocata
  • instruction_data: Dati aggiuntivi passati al programma che specificano l'istruzione da eseguire e i suoi argomenti richiesti

Questi tre parametri corrispondono direttamente ai dati che i client devono fornire quando costruiscono un'istruzione per invocare un programma.

Definire lo stato del programma

Quando si costruisce un programma Solana, in genere si inizia definendo lo stato del programma - i dati che saranno memorizzati negli account creati e posseduti dal tuo programma.

Lo stato del programma è definito utilizzando struct Rust che rappresentano il layout dei dati degli account del tuo programma. Puoi definire più struct per rappresentare diversi tipi di account per il tuo programma.

Quando lavori con gli account, hai bisogno di un modo per convertire i tipi di dati del tuo programma in e dai byte grezzi memorizzati nel campo dati di un account:

  • Serializzazione: Conversione dei tuoi tipi di dati in byte da memorizzare nel campo dati di un account
  • Deserializzazione: Conversione dei byte memorizzati in un account nei tuoi tipi di dati

Sebbene sia possibile utilizzare qualsiasi formato di serializzazione per lo sviluppo di programmi Solana, Borsh è comunemente utilizzato. Per utilizzare Borsh nel tuo programma Solana:

  1. Aggiungi il crate borsh come dipendenza al tuo Cargo.toml:
Terminal
cargo add borsh
  1. Importa i trait Borsh e usa la macro derive per implementare i trait per le tue struct:
use borsh::{BorshSerialize, BorshDeserialize};
// Define struct representing our counter account's data
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
count: u64,
}

Aggiungi la struct CounterAccount a lib.rs per definire lo stato del programma. Questa struct sarà utilizzata sia nelle istruzioni di inizializzazione che in quelle di 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,
}

Definire le istruzioni

Le istruzioni si riferiscono alle diverse operazioni che il tuo programma Solana può eseguire. Pensale come API pubbliche per il tuo programma - definiscono quali azioni gli utenti possono intraprendere quando interagiscono con il tuo programma.

Le istruzioni sono tipicamente definite utilizzando un enum Rust dove:

  • Ogni variante dell'enum rappresenta un'istruzione diversa
  • Il payload della variante rappresenta i parametri dell'istruzione

Nota che le varianti degli enum in Rust sono implicitamente numerate a partire da 0.

Di seguito un esempio di un enum che definisce due istruzioni:

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

Quando un client invoca il tuo programma, deve fornire instruction data (come un buffer di byte) dove:

  • Il primo byte identifica quale variante di istruzione eseguire (0, 1, ecc.)
  • I byte rimanenti contengono i parametri dell'istruzione serializzati (se richiesti)

Per convertire l'instruction data (byte) in una variante dell'enum, è comune implementare un metodo di supporto. Questo metodo:

  1. Separa il primo byte per ottenere la variante dell'istruzione
  2. Fa il match sulla variante e analizza eventuali parametri aggiuntivi dai byte rimanenti
  3. Restituisce la variante enum corrispondente

Per esempio, il metodo unpack per l'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),
}
}
}

Aggiungi il seguente codice a lib.rs per definire le istruzioni per il programma contatore.

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

Gestori di istruzioni

I gestori di istruzioni si riferiscono alle funzioni che contengono la logica di business per ogni istruzione. È comune nominare le funzioni di gestione come process_<instruction_name>, ma sei libero di scegliere qualsiasi convenzione di denominazione.

Aggiungi il seguente codice a lib.rs. Questo codice utilizza l'enum CounterInstruction e il metodo unpack definiti nel passaggio precedente per indirizzare le istruzioni in arrivo alle appropriate funzioni di gestione:

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

Successivamente, aggiungi l'implementazione della funzione process_initialize_counter. Questo gestore di istruzioni:

  1. Crea e alloca spazio per un nuovo account per memorizzare i dati del contatore
  2. Inizializza i dati dell'account con initial_value passato all'istruzione

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

Successivamente, aggiungi l'implementazione della funzione process_increment_counter. Questa istruzione incrementa il valore di un account contatore esistente.

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

Test delle istruzioni

Per testare le istruzioni del programma, aggiungi le seguenti dipendenze 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

Poi aggiungi il seguente modulo di test a lib.rs ed esegui cargo test-sbf per eseguire i test. Facoltativamente, usa il flag --nocapture per vedere le istruzioni di stampa nell'output.

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

Esempio di output:

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?

Indice

Modifica Pagina