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
: Definisce lo stato 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.
Per esempio, vedi il Token Program.
Programma di esempio
Per dimostrare come costruire un programma nativo in Rust 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 in pratica potresti voler dividere programmi più grandi in più file.
Parte 1: Scrivere il programma
Iniziamo costruendo il programma contatore. Creeremo un programma che può inizializzare un contatore con un valore iniziale e incrementarlo.
Crea un nuovo programma
Prima di tutto, creiamo un nuovo progetto Rust per il nostro programma Solana.
$cargo new counter_program --lib$cd counter_program
Dovresti vedere i file predefiniti src/lib.rs
e Cargo.toml
.
Aggiorna il campo edition
in Cargo.toml
a 2021. Altrimenti, potresti
incontrare un errore durante la compilazione del programma.
Aggiungere le dipendenze
Ora aggiungiamo le dipendenze necessarie per costruire un programma Solana.
Abbiamo bisogno di solana-program
per l'SDK principale e borsh
per la
serializzazione.
$cargo add solana-program@2.2.0$cargo add borsh
Non è obbligatorio utilizzare Borsh. Tuttavia, è una libreria di serializzazione comunemente utilizzata per i programmi Solana.
Configurare crate-type
I programmi Solana devono essere compilati come librerie dinamiche. Aggiungi la
sezione [lib]
per configurare come Cargo compila il programma.
[lib]crate-type = ["cdylib", "lib"]
Se non includi questa configurazione, la directory target/deploy non verrà generata quando compili il programma.
Configurare il punto di ingresso del programma
Ogni programma Solana ha un punto di ingresso, che è la funzione che viene chiamata quando il programma viene invocato. Iniziamo aggiungendo le importazioni necessarie per il programma e configurando il punto di ingresso.
Aggiungi il seguente codice a lib.rs
:
use borsh::{BorshDeserialize, BorshSerialize};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 {Ok(())}
La macro
entrypoint
gestisce la deserializzazione dei dati input
nei parametri della funzione
process_instruction
.
Un entrypoint
di un programma Solana ha la seguente firma di funzione. Gli
sviluppatori sono liberi di creare la propria implementazione della funzione
entrypoint
.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Definire lo stato del programma
Ora definiamo la struttura dati che sarà memorizzata nei nostri account
contatore. Questi sono i dati che saranno memorizzati nel campo data
dell'account.
Aggiungi il seguente codice a lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {pub count: u64,}
Definire l'enum delle istruzioni
Definiamo le istruzioni che il nostro programma può eseguire. Useremo un enum dove ogni variante rappresenta un'istruzione diversa.
Aggiungi il seguente codice a lib.rs
:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 },IncrementCounter,}
Implementare la deserializzazione delle istruzioni
Ora dobbiamo deserializzare il instruction_data
(byte grezzi) in una delle
nostre varianti dell'enum CounterInstruction
. Il metodo Borsh try_from_slice
gestisce questa conversione automaticamente.
Aggiorna la funzione process_instruction
per utilizzare la deserializzazione
Borsh:
pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {let instruction = CounterInstruction::try_from_slice(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;Ok(())}
Instradare le istruzioni ai gestori
Ora aggiorniamo la funzione principale process_instruction
per instradare le
istruzioni ai loro gestori appropriati.
Questo modello di instradamento è comune nei programmi Solana. Il
instruction_data
viene deserializzato in una variante di un enum che
rappresenta l'istruzione, quindi viene chiamata la funzione gestore appropriata.
Ogni funzione gestore include l'implementazione per quella istruzione.
Aggiungi il seguente codice a lib.rs
aggiornando la funzione
process_instruction
e aggiungendo i gestori per le istruzioni
InitializeCounter
e IncrementCounter
:
pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {let instruction = CounterInstruction::try_from_slice(instruction_data).map_err(|_| ProgramError::InvalidInstructionData)?;match instruction {CounterInstruction::InitializeCounter { initial_value } => {process_initialize_counter(program_id, accounts, initial_value)?}CounterInstruction::IncrementCounter => {process_increment_counter(program_id, accounts)?}};Ok(())}fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {Ok(())}fn process_increment_counter(program_id: &Pubkey,accounts: &[AccountInfo],) -> ProgramResult {Ok(())}
Implementare il gestore di inizializzazione
Implementiamo il gestore per creare e inizializzare un nuovo account contatore. Poiché solo il System Program può creare account su Solana, useremo una Cross Program Invocation (CPI), essenzialmente chiamando un altro programma dal nostro programma.
Il nostro programma effettua una CPI per chiamare l'istruzione create_account
del System Program. Il nuovo account viene creato con il nostro programma come
proprietario, dando al nostro programma la capacità di scrivere nell'account e
inizializzare i dati.
Aggiungi il seguente codice a lib.rs
aggiornando la funzione
process_initialize_counter
:
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)?;let account_space = 8;let rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);invoke(&system_instruction::create_account(payer_account.key,counter_account.key,required_lamports,account_space as u64,program_id,),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;let counter_data = CounterAccount {count: initial_value,};let mut account_data = &mut counter_account.data.borrow_mut()[..];counter_data.serialize(&mut account_data)?;msg!("Counter initialized with value: {}", initial_value);Ok(())}
Questa istruzione è solo a scopo dimostrativo. Non include controlli di sicurezza e validazione necessari per i programmi in produzione.
Implementa il gestore di incremento
Ora implementiamo il gestore che incrementa un contatore esistente. Questa istruzione:
- Legge il campo
data
dell'account per ilcounter_account
- Lo deserializza in una struttura
CounterAccount
- Incrementa il campo
count
di 1 - Serializza nuovamente la struttura
CounterAccount
nel campodata
dell'account
Aggiungi il seguente codice a lib.rs
aggiornando la funzione
process_increment_counter
:
fn process_increment_counter(program_id: &Pubkey,accounts: &[AccountInfo],) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;if counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}let mut data = counter_account.data.borrow_mut();let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;counter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;counter_data.serialize(&mut &mut data[..])?;msg!("Counter incremented to: {}", counter_data.count);Ok(())}
Questa istruzione è solo a scopo dimostrativo. Non include controlli di sicurezza e validazione necessari per i programmi in produzione.
Programma completato
Congratulazioni! Hai creato un programma Solana completo che dimostra la struttura di base condivisa da tutti i programmi Solana:
- Entrypoint: Definisce dove inizia l'esecuzione del programma e indirizza tutte le richieste in arrivo ai gestori di istruzioni appropriati
- Gestione delle istruzioni: Definisce le istruzioni e le relative funzioni di gestione
- Gestione dello stato: Definisce le strutture dei dati degli account e gestisce il loro stato negli account di proprietà del programma
- Cross Program Invocation (CPI): Chiama il System Program per creare nuovi account di proprietà del programma
Il prossimo passo è testare il programma per assicurarsi che tutto funzioni correttamente.
Parte 2: Testare il programma
Ora testiamo il nostro programma contatore. Useremo LiteSVM, un framework di testing che ci permette di testare i programmi senza doverli distribuire su un cluster.
Aggiungi le dipendenze per i test
Prima di tutto, aggiungiamo le dipendenze necessarie per i test. Useremo
litesvm
per i test e solana-sdk
.
$cargo add litesvm@0.6.1 --dev$cargo add solana-sdk@2.2.0 --dev
Creare un modulo di test
Ora aggiungiamo un modulo di test al nostro programma. Inizieremo con la struttura di base e le importazioni.
Aggiungi il seguente codice a lib.rs
, direttamente sotto il codice del
programma:
#[cfg(test)]mod test {use super::*;use litesvm::LiteSVM;use solana_sdk::{account::ReadableAccount,instruction::{AccountMeta, Instruction},message::Message,signature::{Keypair, Signer},system_program,transaction::Transaction,};#[test]fn test_counter_program() {// Test implementation will go here}}
L'attributo #[cfg(test)]
assicura che questo codice venga compilato solo
durante l'esecuzione dei test.
Inizializzare l'ambiente di test
Configuriamo l'ambiente di test con LiteSVM e finanziamo un account pagante.
LiteSVM simula l'ambiente di runtime di Solana, permettendoci di testare il nostro programma senza doverlo distribuire su un cluster reale.
Aggiungi il seguente codice a lib.rs
aggiornando la funzione
test_counter_program
:
let mut svm = LiteSVM::new();let payer = Keypair::new();svm.airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to airdrop");
Caricare il programma
Ora dobbiamo compilare e caricare il nostro programma nell'ambiente di test.
Esegui il comando cargo build-sbf
per compilare il programma. Questo genererà
il file counter_program.so
nella directory target/deploy
.
$cargo build-sbf
Assicurati che edition
in Cargo.toml
sia impostato su 2021
.
Dopo la compilazione, possiamo caricare il programma.
Aggiorna la funzione test_counter_program
per caricare il programma
nell'ambiente di test.
let program_keypair = Keypair::new();let program_id = program_keypair.pubkey();svm.add_program_from_file(program_id,"target/deploy/counter_program.so").expect("Failed to load program");
Devi eseguire cargo build-sbf
prima di eseguire i test per generare il file
.so
. Il test carica il programma compilato.
Testare l'istruzione di inizializzazione
Testiamo l'istruzione di inizializzazione creando un nuovo account contatore con un valore iniziale.
Aggiungi il seguente codice a lib.rs
aggiornando la funzione
test_counter_program
:
let counter_keypair = Keypair::new();let initial_value: u64 = 42;println!("Testing counter initialization...");let init_instruction_data =borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value }).expect("Failed to serialize instruction");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),],);let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));let transaction = Transaction::new(&[&payer, &counter_keypair],message,svm.latest_blockhash());let result = svm.send_transaction(transaction);assert!(result.is_ok(), "Initialize transaction should succeed");let logs = result.unwrap().logs;println!("Transaction logs:\n{:#?}", logs);
Verifica dell'inizializzazione
Dopo l'inizializzazione, verifichiamo che l'account del contatore sia stato creato correttamente con il valore previsto.
Aggiungi il seguente codice a lib.rs
aggiornando la funzione
test_counter_program
:
let account = svm.get_account(&counter_keypair.pubkey()).expect("Failed to get counter account");let counter: CounterAccount = CounterAccount::try_from_slice(account.data()).expect("Failed to deserialize counter data");assert_eq!(counter.count, 42);println!("Counter initialized successfully with value: {}", counter.count);
Test dell'istruzione di incremento
Ora testiamo l'istruzione di incremento per assicurarci che aggiorni correttamente il valore del contatore.
Aggiungi il seguente codice a lib.rs
aggiornando la funzione
test_counter_program
:
println!("Testing counter increment...");let increment_instruction_data =borsh::to_vec(&CounterInstruction::IncrementCounter).expect("Failed to serialize instruction");let increment_instruction = Instruction::new_with_bytes(program_id,&increment_instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true)],);let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));let transaction = Transaction::new(&[&payer, &counter_keypair],message,svm.latest_blockhash());let result = svm.send_transaction(transaction);assert!(result.is_ok(), "Increment transaction should succeed");let logs = result.unwrap().logs;println!("Transaction logs:\n{:#?}", logs);
Verifica dei risultati finali
Infine, verifichiamo che l'incremento abbia funzionato correttamente controllando il valore aggiornato del contatore.
Aggiungi il seguente codice a lib.rs
aggiornando la funzione
test_counter_program
:
let account = svm.get_account(&counter_keypair.pubkey()).expect("Failed to get counter account");let counter: CounterAccount = CounterAccount::try_from_slice(account.data()).expect("Failed to deserialize counter data");assert_eq!(counter.count, 43);println!("Counter incremented successfully to: {}", counter.count);
Esegui i test con il seguente comando. Il flag --nocapture
mostra l'output del
test.
$cargo test -- --nocapture
Output previsto:
Testing counter initialization...Transaction logs:["Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]","Program 11111111111111111111111111111111 invoke [2]","Program 11111111111111111111111111111111 success","Program log: Counter initialized with value: 42","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",]Counter initialized successfully with value: 42Testing counter increment...Transaction logs:["Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]","Program log: Counter incremented to: 43","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units","Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",]Counter incremented successfully to: 43
Parte 3: Invocazione del programma
Ora aggiungiamo uno script client per invocare il programma.
Crea esempio di client
Creiamo un client Rust per interagire con il nostro programma distribuito.
$mkdir examples$touch examples/client.rs
Aggiungi la seguente configurazione a Cargo.toml
:
[[example]]name = "client"path = "examples/client.rs"
Installa le dipendenze del client:
$cargo add solana-client@2.2.0 --dev$cargo add tokio --dev
Implementa il codice del client
Ora implementiamo il client che invocherà il nostro programma distribuito.
Esegui il seguente comando per ottenere l'ID del tuo programma dal file keypair:
$solana address -k ./target/deploy/counter_program-keypair.json
Aggiungi il codice del client a examples/client.rs
e sostituisci il
program_id
con l'output del comando precedente:
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");
use solana_client::rpc_client::RpcClient;use solana_sdk::{commitment_config::CommitmentConfig,instruction::{AccountMeta, Instruction},pubkey::Pubkey,signature::{Keypair, Signer},system_program,transaction::Transaction,};use std::str::FromStr;use counter_program::CounterInstruction;#[tokio::main]async fn main() {// Replace with your actual program ID from deploymentlet program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");// Connect to local clusterlet rpc_url = String::from("http://localhost:8899");let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());// Generate a new keypair for paying feeslet payer = Keypair::new();// Request airdrop of 1 SOL for transaction feesprintln!("Requesting airdrop...");let airdrop_signature = client.request_airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to request airdrop");// Wait for airdrop confirmationloop {if client.confirm_transaction(&airdrop_signature).unwrap_or(false){break;}std::thread::sleep(std::time::Duration::from_millis(500));}println!("Airdrop confirmed");println!("\nInitializing counter...");let counter_keypair = Keypair::new();let initial_value = 100u64;// Serialize the initialize instruction datalet instruction_data = borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value }).expect("Failed to serialize instruction");let initialize_instruction = Instruction::new_with_bytes(program_id,&instruction_data,vec![AccountMeta::new(counter_keypair.pubkey(), true),AccountMeta::new(payer.pubkey(), true),AccountMeta::new_readonly(system_program::id(), false),],);let mut transaction =Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));let blockhash = client.get_latest_blockhash().expect("Failed to get blockhash");transaction.sign(&[&payer, &counter_keypair], blockhash);match client.send_and_confirm_transaction(&transaction) {Ok(signature) => {println!("Counter initialized!");println!("Transaction: {}", signature);println!("Counter address: {}", counter_keypair.pubkey());}Err(err) => {eprintln!("Failed to initialize counter: {}", err);return;}}println!("\nIncrementing counter...");// Serialize the increment instruction datalet increment_data = borsh::to_vec(&CounterInstruction::IncrementCounter).expect("Failed to serialize instruction");let increment_instruction = Instruction::new_with_bytes(program_id,&increment_data,vec![AccountMeta::new(counter_keypair.pubkey(), true)],);let mut transaction =Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], blockhash);match client.send_and_confirm_transaction(&transaction) {Ok(signature) => {println!("Counter incremented!");println!("Transaction: {}", signature);}Err(err) => {eprintln!("Failed to increment counter: {}", err);}}}
Parte 4: Distribuzione del programma
Ora che abbiamo il nostro programma e il client pronti, costruiamo, distribuiamo e invochiamo il programma.
Costruisci il programma
Prima di tutto, costruiamo il nostro programma.
$cargo build-sbf
Questo comando compila il tuo programma e genera due file importanti in
target/deploy/
:
counter_program.so # The compiled programcounter_program-keypair.json # Keypair for the program ID
Puoi visualizzare l'ID del tuo programma eseguendo il seguente comando:
$solana address -k ./target/deploy/counter_program-keypair.json
Esempio di output:
HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Avvia il validator locale
Per lo sviluppo, utilizzeremo un validator di test locale.
Prima, configura la CLI di Solana per utilizzare localhost:
$solana config set -ul
Esempio di output:
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed
Ora avvia il validator di test in un terminale separato:
$solana-test-validator
Distribuisci il programma
Con il validator in esecuzione, distribuisci il tuo programma sul cluster locale:
$solana program deploy ./target/deploy/counter_program.so
Esempio di output:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
Puoi verificare la distribuzione utilizzando il comando solana program show
con l'ID del tuo programma:
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Output di esempio:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
Esegui il client
Con il validator locale ancora in esecuzione, esegui il client:
$cargo run --example client
Output previsto:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
Con il validator locale in esecuzione, puoi visualizzare le transazioni su
Solana Explorer utilizzando le
firme delle transazioni nell'output. Nota che il cluster su Solana Explorer deve
essere impostato su "Custom RPC URL", che per impostazione predefinita è
http://localhost:8899
su cui il solana-test-validator
è in esecuzione.
Is this page helpful?