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:
InitializeCounter
: Crea e inizializza un nuovo account con un valore iniziale.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
.
cargo init counter_program --lib
Naviga nella directory del progetto. Dovresti vedere i file predefiniti
src/lib.rs
e Cargo.toml
cd counter_program
Successivamente, aggiungi la dipendenza solana-program
. Questa è la dipendenza
minima richiesta per costruire un programma Solana.
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.
[lib]crate-type = ["cdylib", "lib"]
Il tuo file Cargo.toml
dovrebbe apparire come segue:
[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:
- Importa le dipendenze necessarie da
solana_program
- Definisce l'entrypoint del programma usando la macro
entrypoint!
- Implementa la funzione
process_instruction
che indirizzerà le istruzioni alle funzioni handler appropriate
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(())}
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
: IlAccountInfo
per gli account richiesti dall'istruzione che viene invocatainstruction_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:
- Aggiungi il crate
borsh
come dipendenza al tuoCargo.toml
:
cargo add borsh
- 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.
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,}
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 0IncrementCounter, // 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:
- Separa il primo byte per ottenere la variante dell'istruzione
- Fa il match sulla variante e analizza eventuali parametri aggiuntivi dai byte rimanenti
- 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 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),}}}
Aggiungi il seguente codice a lib.rs
per definire le istruzioni per il
programma contatore.
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),}}}
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:
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(())}
Successivamente, aggiungi l'implementazione della funzione
process_initialize_counter
. Questo gestore di istruzioni:
- Crea e alloca spazio per un nuovo account per memorizzare i dati del contatore
- Inizializza i dati dell'account con
initial_value
passato all'istruzione
// 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(())}
Successivamente, aggiungi l'implementazione della funzione
process_increment_counter
. Questa istruzione incrementa il valore di un
account contatore esistente.
// 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(())}
Test delle istruzioni
Per testare le istruzioni del programma, aggiungi le seguenti dipendenze a
Cargo.toml
.
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo 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.
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);}}}
Esempio di output:
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?