Τεκμηρίωση SolanaΑνάπτυξη προγραμμάτωνΠρογράμματα Rust

Δομή προγράμματος Rust

Τα προγράμματα Solana που είναι γραμμένα σε Rust έχουν ελάχιστες δομικές απαιτήσεις, επιτρέποντας ευελιξία στον τρόπο οργάνωσης του κώδικα. Η μόνη απαίτηση είναι ότι ένα πρόγραμμα πρέπει να έχει ένα entrypoint, το οποίο καθορίζει από πού ξεκινά η εκτέλεση ενός προγράμματος.

Δομή προγράμματος

Αν και δεν υπάρχουν αυστηροί κανόνες για τη δομή των αρχείων, τα προγράμματα Solana συνήθως ακολουθούν ένα κοινό μοτίβο:

  • entrypoint.rs: Καθορίζει το σημείο εισόδου που δρομολογεί τις εισερχόμενες εντολές.
  • state.rs: Καθορίζουν την κατάσταση που είναι συγκεκριμένη για το πρόγραμμα (δεδομένα λογαριασμού).
  • instructions.rs: Καθορίζει τις εντολές που μπορεί να εκτελέσει το πρόγραμμα.
  • processor.rs: Καθορίζει τους χειριστές εντολών (συναρτήσεις) που υλοποιούν την επιχειρησιακή λογική για κάθε εντολή.
  • error.rs: Καθορίζει προσαρμοσμένα σφάλματα που μπορεί να επιστρέψει το πρόγραμμα.

Μπορείτε να βρείτε παραδείγματα στη Βιβλιοθήκη Προγραμμάτων Solana.

Παράδειγμα προγράμματος

Για να δείξουμε πώς να δημιουργήσετε ένα εγγενές πρόγραμμα Rust με πολλαπλές εντολές, θα εξετάσουμε ένα απλό πρόγραμμα μετρητή που υλοποιεί δύο εντολές:

  1. InitializeCounter: Δημιουργεί και αρχικοποιεί έναν νέο λογαριασμό με μια αρχική τιμή.
  2. IncrementCounter: Αυξάνει την τιμή που είναι αποθηκευμένη σε έναν υπάρχοντα λογαριασμό.

Για απλότητα, το πρόγραμμα θα υλοποιηθεί σε ένα μόνο αρχείο lib.rs, αν και στην πράξη ίσως θέλετε να χωρίσετε μεγαλύτερα προγράμματα σε πολλαπλά αρχεία.

Δημιουργία νέου προγράμματος

Πρώτα, δημιουργήστε ένα νέο project Rust χρησιμοποιώντας την τυπική εντολή cargo init με τη σημαία --lib.

Terminal
cargo init counter_program --lib

Πλοηγηθείτε στον κατάλογο του project. Θα πρέπει να δείτε τα προεπιλεγμένα αρχεία src/lib.rs και Cargo.toml

Terminal
cd counter_program

Στη συνέχεια, προσθέστε την εξάρτηση solana-program. Αυτή είναι η ελάχιστη εξάρτηση που απαιτείται για τη δημιουργία ενός προγράμματος Solana.

Terminal
cargo add solana-program@1.18.26

Στη συνέχεια, προσθέστε το ακόλουθο απόσπασμα στο Cargo.toml. Εάν δεν συμπεριλάβετε αυτή τη ρύθμιση, ο κατάλογος target/deploy δεν θα δημιουργηθεί όταν κάνετε build το πρόγραμμα.

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

Το αρχείο Cargo.toml θα πρέπει να μοιάζει με το ακόλουθο:

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

Σημείο εισόδου προγράμματος

Το σημείο εισόδου ενός προγράμματος Solana είναι η συνάρτηση που καλείται όταν γίνεται επίκληση ενός προγράμματος. Το σημείο εισόδου έχει τον ακόλουθο βασικό ορισμό και οι προγραμματιστές είναι ελεύθεροι να δημιουργήσουν τη δική τους υλοποίηση της συνάρτησης εισόδου.

Για απλότητα, χρησιμοποιήστε το μακροεντολή entrypoint! από το crate solana_program για να ορίσετε το σημείο εισόδου στο πρόγραμμά σας.

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

Αντικαταστήστε τον προεπιλεγμένο κώδικα στο lib.rs με τον ακόλουθο κώδικα. Αυτό το απόσπασμα:

  1. Εισάγει τις απαιτούμενες εξαρτήσεις από το solana_program
  2. Ορίζει το σημείο εισόδου του προγράμματος χρησιμοποιώντας τη μακροεντολή entrypoint!
  3. Υλοποιεί τη συνάρτηση process_instruction που θα δρομολογεί τις οδηγίες στις κατάλληλες συναρτήσεις χειρισμού
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(())
}

Η μακροεντολή entrypoint! απαιτεί μια συνάρτηση με την ακόλουθη υπογραφή τύπου ως όρισμα:

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

Όταν γίνεται επίκληση ενός προγράμματος Solana, το σημείο εισόδου αποσειριοποιεί τα δεδομένα εισόδου (που παρέχονται ως bytes) σε τρεις τιμές και τις περνάει στη συνάρτηση process_instruction:

  • program_id: Το δημόσιο κλειδί του προγράμματος που καλείται (τρέχον πρόγραμμα)
  • accounts: Το AccountInfo για λογαριασμούς που απαιτούνται από την οδηγία που καλείται
  • instruction_data: Πρόσθετα δεδομένα που περνούν στο πρόγραμμα και καθορίζουν την οδηγία προς εκτέλεση και τα απαιτούμενα ορίσματά της

Αυτές οι τρεις παράμετροι αντιστοιχούν άμεσα στα δεδομένα που πρέπει να παρέχουν οι πελάτες όταν δημιουργούν μια οδηγία για να καλέσουν ένα πρόγραμμα.

Ορισμός Κατάστασης Προγράμματος

Κατά την ανάπτυξη ενός προγράμματος Solana, συνήθως θα ξεκινήσετε ορίζοντας την κατάσταση του προγράμματός σας - τα δεδομένα που θα αποθηκευτούν σε λογαριασμούς που δημιουργούνται και ανήκουν στο πρόγραμμά σας.

Η κατάσταση του προγράμματος ορίζεται χρησιμοποιώντας δομές Rust που αναπαριστούν τη διάταξη δεδομένων των λογαριασμών του προγράμματός σας. Μπορείτε να ορίσετε πολλαπλές δομές για να αναπαραστήσετε διαφορετικούς τύπους λογαριασμών για το πρόγραμμά σας.

Όταν εργάζεστε με λογαριασμούς, χρειάζεστε έναν τρόπο να μετατρέπετε τους τύπους δεδομένων του προγράμματός σας από και προς τα ακατέργαστα bytes που αποθηκεύονται στο πεδίο δεδομένων ενός λογαριασμού:

  • Σειριοποίηση (Serialization): Μετατροπή των τύπων δεδομένων σας σε bytes για αποθήκευση στο πεδίο δεδομένων ενός λογαριασμού
  • Αποσειριοποίηση (Deserialization): Μετατροπή των bytes που αποθηκεύονται σε έναν λογαριασμό πίσω στους τύπους δεδομένων σας

Ενώ μπορείτε να χρησιμοποιήσετε οποιαδήποτε μορφή σειριοποίησης για την ανάπτυξη προγραμμάτων Solana, το Borsh χρησιμοποιείται συχνά. Για να χρησιμοποιήσετε το Borsh στο πρόγραμμα Solana σας:

  1. Προσθέστε το crate borsh ως εξάρτηση στο Cargo.toml σας:
Terminal
cargo add borsh
  1. Εισάγετε τα χαρακτηριστικά Borsh και χρησιμοποιήστε το derive macro για να υλοποιήσετε τα χαρακτηριστικά για τις δομές σας:
use borsh::{BorshSerialize, BorshDeserialize};
// Define struct representing our counter account's data
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
count: u64,
}

Προσθέστε τη δομή CounterAccount στο lib.rs για να ορίσετε την κατάσταση του προγράμματος. Αυτή η δομή θα χρησιμοποιηθεί τόσο στις εντολές αρχικοποίησης όσο και στις εντολές αύξησης.

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

Ορισμός Εντολών

Οι εντολές αναφέρονται στις διαφορετικές λειτουργίες που μπορεί να εκτελέσει το πρόγραμμα Solana σας. Σκεφτείτε τες ως δημόσια APIs για το πρόγραμμά σας - ορίζουν ποιες ενέργειες μπορούν να κάνουν οι χρήστες όταν αλληλεπιδρούν με το πρόγραμμά σας.

Οι εντολές συνήθως ορίζονται χρησιμοποιώντας ένα enum της Rust όπου:

  • Κάθε παραλλαγή του enum αντιπροσωπεύει μια διαφορετική εντολή
  • Το φορτίο της παραλλαγής αντιπροσωπεύει τις παραμέτρους της εντολής

Σημειώστε ότι οι παραλλαγές των enum στη Rust αριθμούνται αυτόματα ξεκινώντας από το 0.

Παρακάτω είναι ένα παράδειγμα ενός enum που ορίζει δύο εντολές:

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

Όταν ένας πελάτης καλεί το πρόγραμμά σας, πρέπει να παρέχει instruction data (ως ένα buffer από bytes) όπου:

  • Το πρώτο byte προσδιορίζει ποια παραλλαγή εντολής θα εκτελεστεί (0, 1, κλπ.)
  • Τα υπόλοιπα bytes περιέχουν τις σειριοποιημένες παραμέτρους της εντολής (αν απαιτούνται)

Για τη μετατροπή των instruction data (bytes) σε μια παραλλαγή του enum, είναι συνηθισμένο να υλοποιείται μια βοηθητική μέθοδος. Αυτή η μέθοδος:

  1. Διαχωρίζει το πρώτο byte για να πάρει την παραλλαγή της εντολής
  2. Κάνει αντιστοίχιση στην παραλλαγή και αναλύει τυχόν πρόσθετες παραμέτρους από τα υπόλοιπα bytes
  3. Επιστρέφει την αντίστοιχη παραλλαγή του enum

Για παράδειγμα, η μέθοδος unpack για το 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),
}
}
}

Προσθέστε τον ακόλουθο κώδικα στο lib.rs για να ορίσετε τις εντολές για το πρόγραμμα μετρητή.

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

Χειριστές εντολών

Οι χειριστές εντολών αναφέρονται στις συναρτήσεις που περιέχουν την επιχειρησιακή λογική για κάθε εντολή. Είναι συνηθισμένο να ονομάζονται οι συναρτήσεις χειριστών ως process_<instruction_name>, αλλά είστε ελεύθεροι να επιλέξετε οποιαδήποτε σύμβαση ονοματοδοσίας.

Προσθέστε τον ακόλουθο κώδικα στο lib.rs. Αυτός ο κώδικας χρησιμοποιεί το enum CounterInstruction και τη μέθοδο unpack που ορίστηκαν στο προηγούμενο βήμα για να δρομολογήσει τις εισερχόμενες εντολές στις κατάλληλες συναρτήσεις χειριστών:

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

Στη συνέχεια, προσθέστε την υλοποίηση της συνάρτησης process_initialize_counter. Αυτός ο χειριστής εντολών:

  1. Δημιουργεί και δεσμεύει χώρο για έναν νέο λογαριασμό για την αποθήκευση των δεδομένων του μετρητή
  2. Αρχικοποιεί τα δεδομένα του λογαριασμού με το initial_value που περνάει στην εντολή

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

Στη συνέχεια, προσθέστε την υλοποίηση της συνάρτησης process_increment_counter. Αυτή η εντολή αυξάνει την τιμή ενός υπάρχοντος λογαριασμού μετρητή.

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

Δοκιμή εντολών

Για να δοκιμάσετε τις εντολές του προγράμματος, προσθέστε τις ακόλουθες εξαρτήσεις στο Cargo.toml.

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

Στη συνέχεια προσθέστε την ακόλουθη ενότητα δοκιμών στο lib.rs και εκτελέστε το cargo test-sbf για να εκτελέσετε τις δοκιμές. Προαιρετικά, χρησιμοποιήστε τη σημαία --nocapture για να δείτε τις εντολές εκτύπωσης στην έξοδο.

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

Παράδειγμα εξόδου:

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?