Rust-Programmstruktur

Solana-Programme, die in Rust geschrieben sind, haben minimale strukturelle Anforderungen und bieten Flexibilität bei der Organisation des Codes. Die einzige Anforderung ist, dass ein Programm einen entrypoint haben muss, der definiert, wo die Ausführung eines Programms beginnt.

Programmstruktur

Obwohl es keine strengen Regeln für die Dateistruktur gibt, folgen Solana-Programme typischerweise einem gemeinsamen Muster:

  • entrypoint.rs: Definiert den Einstiegspunkt, der eingehende Anweisungen weiterleitet.
  • state.rs: Definieren programmspezifischen Zustand (Kontendaten).
  • instructions.rs: Definiert die Anweisungen, die das Programm ausführen kann.
  • processor.rs: Definiert die Anweisungshandler (Funktionen), die die Geschäftslogik für jede Anweisung implementieren.
  • error.rs: Definiert benutzerdefinierte Fehler, die das Programm zurückgeben kann.

Beispiele finden Sie in der Solana Program Library.

Beispielprogramm

Um zu demonstrieren, wie man ein natives Rust-Programm mit mehreren Anweisungen erstellt, werden wir ein einfaches Zählerprogramm durchgehen, das zwei Anweisungen implementiert:

  1. InitializeCounter: Erstellt und initialisiert ein neues Konto mit einem Anfangswert.
  2. IncrementCounter: Erhöht den in einem bestehenden Konto gespeicherten Wert.

Der Einfachheit halber wird das Programm in einer einzigen lib.rs-Datei implementiert, obwohl Sie in der Praxis größere Programme möglicherweise auf mehrere Dateien aufteilen möchten.

Ein neues Programm erstellen

Erstellen Sie zunächst ein neues Rust-Projekt mit dem Standard cargo init-Befehl und der --lib-Flag.

Terminal
cargo init counter_program --lib

Navigieren Sie zum Projektverzeichnis. Sie sollten die Standard src/lib.rs und Cargo.toml Dateien sehen

Terminal
cd counter_program

Füge als Nächstes die solana-program Abhängigkeit hinzu. Dies ist die minimale Abhängigkeit, die zum Erstellen eines Solana-Programms erforderlich ist.

Terminal
cargo add solana-program@1.18.26

Füge als Nächstes den folgenden Ausschnitt zu Cargo.toml hinzu. Wenn du diese Konfiguration nicht einschließt, wird das target/deploy Verzeichnis beim Erstellen des Programms nicht generiert.

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

Deine Cargo.toml Datei sollte wie folgt aussehen:

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

Programm-Einstiegspunkt

Ein Solana-Programm-Einstiegspunkt ist die Funktion, die aufgerufen wird, wenn ein Programm ausgeführt wird. Der Einstiegspunkt hat die folgende grundlegende Definition, und Entwickler können ihre eigene Implementierung der Einstiegspunktfunktion erstellen.

Der Einfachheit halber verwende das entrypoint! Makro aus dem solana_program Crate, um den Einstiegspunkt in deinem Programm zu definieren.

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

Ersetze den Standardcode in lib.rs durch den folgenden Code. Dieser Ausschnitt:

  1. Importiert die erforderlichen Abhängigkeiten aus solana_program
  2. Definiert den Programm-Einstiegspunkt mit dem entrypoint! Makro
  3. Implementiert die process_instruction Funktion, die Anweisungen an die entsprechenden Handler-Funktionen weiterleitet
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(())
}

Das entrypoint! Makro erfordert eine Funktion mit der folgenden Typsignatur als Argument:

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

Wenn ein Solana-Programm aufgerufen wird, deserialisiert der Einstiegspunkt die Eingabedaten (bereitgestellt als Bytes) in drei Werte und übergibt sie an die process_instruction Funktion:

  • program_id: Der öffentliche Schlüssel des aufgerufenen Programms (aktuelles Programm)
  • accounts: Die AccountInfo für Konten, die von der aufgerufenen Anweisung benötigt werden
  • instruction_data: Zusätzliche Daten, die an das Programm übergeben werden und die auszuführende Anweisung sowie ihre erforderlichen Argumente angeben

Diese drei Parameter entsprechen direkt den Daten, die Clients bereitstellen müssen, wenn sie eine Anweisung zum Aufrufen eines Programms erstellen.

Programm-Status definieren

Beim Erstellen eines Solana-Programms beginnt man typischerweise mit der Definition des Programm-Status - den Daten, die in Konten gespeichert werden, die von deinem Programm erstellt und verwaltet werden.

Der Programm-Status wird mit Rust-Strukturen definiert, die das Datenlayout der Konten deines Programms repräsentieren. Du kannst mehrere Strukturen definieren, um verschiedene Arten von Konten für dein Programm darzustellen.

Bei der Arbeit mit Konten benötigst du eine Möglichkeit, die Datentypen deines Programms in die rohen Bytes umzuwandeln, die im Datenfeld eines Kontos gespeichert werden, und umgekehrt:

  • Serialisierung: Umwandlung deiner Datentypen in Bytes zur Speicherung im Datenfeld eines Kontos
  • Deserialisierung: Umwandlung der in einem Konto gespeicherten Bytes zurück in deine Datentypen

Obwohl du jedes Serialisierungsformat für die Solana-Programmentwicklung verwenden kannst, wird Borsh häufig verwendet. Um Borsh in deinem Solana-Programm zu verwenden:

  1. Füge die borsh Crate als Abhängigkeit zu deiner Cargo.toml hinzu:
Terminal
cargo add borsh
  1. Importiere die Borsh-Traits und verwende das Derive-Makro, um die Traits für deine Strukturen zu implementieren:
use borsh::{BorshSerialize, BorshDeserialize};
// Define struct representing our counter account's data
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
count: u64,
}

Füge die CounterAccount Struktur zu lib.rs hinzu, um den Programm-Status zu definieren. Diese Struktur wird sowohl in den Initialisierungs- als auch in den Inkrement-Anweisungen verwendet.

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

Anweisungen definieren

Anweisungen beziehen sich auf die verschiedenen Operationen, die dein Solana-Programm ausführen kann. Betrachte sie als öffentliche APIs für dein Programm - sie definieren, welche Aktionen Benutzer ausführen können, wenn sie mit deinem Programm interagieren.

Anweisungen werden typischerweise mit einem Rust-Enum definiert, wobei:

  • Jede Enum-Variante eine andere Anweisung repräsentiert
  • Die Nutzlast der Variante die Parameter der Anweisung darstellt

Beachte, dass Rust-Enum-Varianten implizit beginnend mit 0 nummeriert werden.

Hier ist ein Beispiel für ein Enum, das zwei Anweisungen definiert:

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

Wenn ein Client dein Programm aufruft, muss er instruction data (als Byte-Puffer) bereitstellen, wobei:

  • Das erste Byte identifiziert, welche Anweisungsvariante ausgeführt werden soll (0, 1, usw.)
  • Die verbleibenden Bytes enthalten die serialisierten Anweisungsparameter (falls erforderlich)

Um die instruction data (Bytes) in eine Variante des Enums umzuwandeln, ist es üblich, eine Hilfsmethode zu implementieren. Diese Methode:

  1. Trennt das erste Byte ab, um die Anweisungsvariante zu erhalten
  2. Prüft die Variante und analysiert alle zusätzlichen Parameter aus den verbleibenden Bytes
  3. Gibt die entsprechende Enum-Variante zurück

Zum Beispiel die unpack Methode für das 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),
}
}
}

Füge den folgenden Code zu lib.rs hinzu, um die Anweisungen für das Counter-Programm zu definieren.

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

Anweisungshandler

Anweisungshandler beziehen sich auf die Funktionen, die die Geschäftslogik für jede Anweisung enthalten. Es ist üblich, Handler-Funktionen als process_<instruction_name> zu benennen, aber du kannst jede beliebige Namenskonvention wählen.

Füge den folgenden Code zu lib.rs hinzu. Dieser Code verwendet das CounterInstruction Enum und die unpack Methode, die im vorherigen Schritt definiert wurden, um eingehende Anweisungen an die entsprechenden Handler-Funktionen weiterzuleiten:

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

Als Nächstes füge die Implementierung der process_initialize_counter Funktion hinzu. Dieser Anweisungshandler:

  1. Erstellt ein neues Konto und weist Speicherplatz für die Counter-Daten zu
  2. Initialisiert die Kontodaten mit initial_value, das an die Anweisung übergeben wurde

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

Als nächstes fügen wir die Implementierung der process_increment_counter Funktion hinzu. Diese Anweisung erhöht den Wert eines bestehenden Zählerkontos.

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

Anweisungen testen

Um die Programm-Anweisungen zu testen, fügen Sie die folgenden Abhängigkeiten zu Cargo.toml hinzu.

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

Fügen Sie dann das folgende Test-Modul zu lib.rs hinzu und führen Sie cargo test-sbf aus, um die Tests auszuführen. Optional können Sie das Flag --nocapture verwenden, um die Print- Anweisungen in der Ausgabe zu sehen.

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

Beispielausgabe:

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?

Inhaltsverzeichnis

Seite bearbeiten