Rust-programmastructuur

Solana-programma's geschreven in Rust hebben minimale structurele vereisten, wat flexibiliteit biedt in hoe code wordt georganiseerd. De enige vereiste is dat een programma een entrypoint moet hebben, dat definieert waar de uitvoering van een programma begint.

Programmastructuur

Hoewel er geen strikte regels zijn voor bestandsstructuur, volgen Solana-programma's meestal een gemeenschappelijk patroon:

  • entrypoint.rs: Definieert het entrypoint dat binnenkomende instructies routeert.
  • state.rs: Definiëren programmaspecifieke status (accountgegevens).
  • instructions.rs: Definieert de instructies die het programma kan uitvoeren.
  • processor.rs: Definieert de instructiehandlers (functies) die de bedrijfslogica voor elke instructie implementeren.
  • error.rs: Definieert aangepaste fouten die het programma kan teruggeven.

Je kunt voorbeelden vinden in de Solana Program Library.

Voorbeeldprogramma

Om te demonstreren hoe je een native Rust-programma bouwt met meerdere instructies, behandelen we een eenvoudig tellerprogramma dat twee instructies implementeert:

  1. InitializeCounter: Maakt een nieuw account aan en initialiseert het met een beginwaarde.
  2. IncrementCounter: Verhoogt de waarde die is opgeslagen in een bestaand account.

Voor de eenvoud wordt het programma geïmplementeerd in één enkel lib.rsbestand, hoewel je in de praktijk grotere programma's misschien wilt opsplitsen in meerdere bestanden.

Een nieuw programma maken

Maak eerst een nieuw Rust-project aan met het standaard cargo initcommando met de --libvlag.

Terminal
cargo init counter_program --lib

Navigeer naar de projectmap. Je zou de standaard src/lib.rs en Cargo.tomlbestanden moeten zien

Terminal
cd counter_program

Voeg vervolgens de solana-program dependency toe. Dit is de minimale dependency die nodig is om een Solana-programma te bouwen.

Terminal
cargo add solana-program@1.18.26

Voeg daarna het volgende fragment toe aan Cargo.toml. Als je deze configuratie niet opneemt, zal de target/deploy map niet worden gegenereerd wanneer je het programma bouwt.

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

Je Cargo.toml bestand zou er als volgt uit moeten zien:

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

Programma-entrypoint

Een Solana-programma-entrypoint is de functie die wordt aangeroepen wanneer een programma wordt gestart. De entrypoint heeft de volgende ruwe definitie en ontwikkelaars zijn vrij om hun eigen implementatie van de entrypoint-functie te maken.

Voor de eenvoud, gebruik de entrypoint! macro uit de solana_program crate om de entrypoint in je programma te definiëren.

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

Vervang de standaardcode in lib.rs met de volgende code. Dit fragment:

  1. Importeert de vereiste dependencies van solana_program
  2. Definieert de programma-entrypoint met behulp van de entrypoint! macro
  3. Implementeert de process_instruction functie die instructies naar de juiste handlefuncties zal routeren
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(())
}

De entrypoint! macro vereist een functie met de volgende type signature als argument:

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

Wanneer een Solana-programma wordt aangeroepen, deserialiseert de entrypoint de invoergegevens (aangeleverd als bytes) naar drie waarden en geeft deze door aan de process_instruction functie:

  • program_id: De publieke sleutel van het programma dat wordt aangeroepen (huidige programma)
  • accounts: De AccountInfo voor accounts die vereist zijn door de instructie die wordt aangeroepen
  • instruction_data: Aanvullende gegevens die aan het programma worden doorgegeven en die de uit te voeren instructie en de vereiste argumenten specificeren

Deze drie parameters komen rechtstreeks overeen met de gegevens die clients moeten verstrekken bij het bouwen van een instructie om een programma aan te roepen.

Programma-status definiëren

Bij het bouwen van een Solana-programma begin je meestal met het definiëren van de status van je programma - de gegevens die worden opgeslagen in accounts die door je programma zijn gemaakt en beheerd.

De programma-status wordt gedefinieerd met behulp van Rust-structs die de gegevensindeling van de accounts van je programma weergeven. Je kunt meerdere structs definiëren om verschillende soorten accounts voor je programma te representeren.

Bij het werken met accounts heb je een manier nodig om de gegevenstypen van je programma om te zetten naar en van de ruwe bytes die zijn opgeslagen in het gegevensveld van een account:

  • Serialisatie: Het omzetten van je gegevenstypen naar bytes om op te slaan in het gegevensveld van een account
  • Deserialisatie: Het omzetten van de bytes die zijn opgeslagen in een account terug naar je gegevenstypen

Hoewel je elk serialisatieformaat kunt gebruiken voor Solana-programmaontwikkeling, wordt Borsh vaak gebruikt. Om Borsh in je Solana-programma te gebruiken:

  1. Voeg de borsh crate toe als een afhankelijkheid aan je Cargo.toml:
Terminal
cargo add borsh
  1. Importeer de Borsh-traits en gebruik de derive-macro om de traits voor je structs te implementeren:
use borsh::{BorshSerialize, BorshDeserialize};
// Define struct representing our counter account's data
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
count: u64,
}

Voeg de CounterAccount struct toe aan lib.rs om de programma-status te definiëren. Deze struct zal worden gebruikt in zowel de initialisatie- als de increment-instructies.

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

Instructies definiëren

Instructies verwijzen naar de verschillende operaties die je Solana-programma kan uitvoeren. Zie ze als openbare API's voor je programma - ze definiëren welke acties gebruikers kunnen ondernemen bij interactie met je programma.

Instructies worden meestal gedefinieerd met behulp van een Rust-enum waarbij:

  • Elke enum-variant een andere instructie vertegenwoordigt
  • De payload van de variant de parameters van de instructie vertegenwoordigt

Merk op dat Rust enum varianten impliciet genummerd worden vanaf 0.

Hieronder staat een voorbeeld van een enum die twee instructies definieert:

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

Wanneer een client je programma aanroept, moeten ze instruction data (als een buffer van bytes) verstrekken waarbij:

  • De eerste byte identificeert welke instructievariant moet worden uitgevoerd (0, 1, enz.)
  • De overige bytes bevatten de geserialiseerde instructieparameters (indien vereist)

Om de instruction data (bytes) om te zetten naar een variant van de enum, is het gebruikelijk om een hulpmethode te implementeren. Deze methode:

  1. Splitst de eerste byte om de instructievariant te krijgen
  2. Matcht op de variant en parseert eventuele aanvullende parameters uit de resterende bytes
  3. Retourneert de corresponderende enum variant

Bijvoorbeeld, de unpack methode voor de CounterInstruction enum:

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

Voeg de volgende code toe aan lib.rs om de instructies voor het counter programma te definiëren.

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

Instructie handlers

Instructie handlers verwijzen naar de functies die de bedrijfslogica bevatten voor elke instructie. Het is gebruikelijk om handlefuncties te benoemen als process_<instruction_name>, maar je bent vrij om elke naamgevingsconventie te kiezen.

Voeg de volgende code toe aan lib.rs. Deze code gebruikt de CounterInstruction enum en unpack methode die in de vorige stap is gedefinieerd om binnenkomende instructies naar de juiste handlefuncties te routeren:

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

Voeg vervolgens de implementatie van de process_initialize_counter functie toe. Deze instructie handler:

  1. Maakt een nieuw account aan en wijst ruimte toe om de countergegevens op te slaan
  2. Initialiseert de accountgegevens met initial_value die aan de instructie is doorgegeven

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

Voeg vervolgens de implementatie van de process_increment_counter functie toe. Deze instructie verhoogt de waarde van een bestaand counter-account.

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

Instructie testen

Om de programma-instructies te testen, voeg de volgende dependencies toe aan Cargo.toml.

Terminal
cargo add solana-program-test@1.18.26 --dev
cargo add solana-sdk@1.18.26 --dev
cargo add tokio --dev

Voeg vervolgens de volgende testmodule toe aan lib.rs en voer cargo test-sbf uit om de tests uit te voeren. Optioneel kun je de --nocapture flag gebruiken om de print statements in de output te zien.

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

Voorbeelduitvoer:

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?

Inhoudsopgave

Pagina Bewerken