Skip to content

Module 2 - Basics of Rust

Rust is a systems programming language that focuses on safety, speed, and concurrency. It was designed to provide memory safety guarantees without using a garbage collector, making it ideal for performance-critical applications like blockchain systems.

Key features that make Rust suitable for blockchain development:

  • Memory safety without garbage collection - Rust’s ownership system prevents memory-related bugs at compile time
  • Concurrency without data races - Safe concurrency is built into the language
  • Zero-cost abstractions - High-level constructs compile to efficient low-level code
  • Minimal runtime - Rust has a small footprint and can run in resource-constrained environments
  • Robust type system - Provides strong guarantees about code behavior

In Rust, variables are immutable by default:

let x = 5; // Immutable
// x = 6; // This would cause a compilation error
// To create a mutable variable:
let mut y = 5;
y = 6; // This works fine

Rust is statically typed, meaning all variables must have their types known at compile time.

Basic types:

// Integers
let a: i32 = 42; // 32-bit signed integer
let b: u64 = 100; // 64-bit unsigned integer
// Floating-point numbers
let c: f32 = 3.14; // 32-bit float
let d: f64 = 2.71828; // 64-bit float (double)
// Boolean type
let e: bool = true;
// Character type
let f: char = 'z';
// String types
let g: &str = "hello"; // String slice, borrowed
let h: String = String::from("world"); // Owned string

Conditional statements:

let number = 7;
if number < 5 {
println!("Less than five!");
} else if number == 5 {
println!("Equal to five!");
} else {
println!("Greater than five!");
}
// If expressions return values
let result = if number > 5 { "big" } else { "small" };

Loops:

// Loop indefinitely until break
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // Return a value from the loop
}
};
// While loop
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
// For loop over a range
for i in 1..5 {
println!("Value: {}", i); // Prints 1 through 4
}
// For loop over an iterator
let animals = vec!["dog", "cat", "bird"];
for animal in animals.iter() {
println!("{}", animal);
}

Ownership is Rust’s most unique feature and enables memory safety guarantees without garbage collection.

Core principles:

  1. Each value has a single owner
  2. When the owner goes out of scope, the value is dropped (memory freed)
  3. Ownership can be transferred (moved)
fn main() {
// s1 is valid in this scope
let s1 = String::from("hello");
// Ownership of the string moves to s2
// s1 is no longer valid after this line
let s2 = s1;
// This would cause an error:
// println!("{}", s1);
// When s2 goes out of scope, memory is freed
}

Instead of transferring ownership, you can borrow references to values:

fn main() {
let s1 = String::from("hello");
// s1 is borrowed here (immutable borrow)
let len = calculate_length(&s1);
// s1 is still valid here
println!("The length of '{}' is {}.", s1, len);
// Mutable borrowing
let mut s2 = String::from("hello");
change(&mut s2);
println!("Modified: {}", s2);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
fn change(s: &mut String) {
s.push_str(", world");
}

Borrowing rules:

  1. You can have either:
    • One mutable reference
    • Any number of immutable references
  2. References must always be valid (no dangling references)

Structs group related data together:

// Definition
struct Account {
address: String,
balance: u64,
active: bool,
}
// Usage
fn main() {
let mut account = Account {
address: String::from("0x123..."),
balance: 1000,
active: true,
};
// Accessing fields
println!("Address: {}", account.address);
// Modifying fields (if mutable)
account.balance += 50;
// Using the entire struct
print_account_info(&account);
}
fn print_account_info(account: &Account) {
println!("Account {} has balance: {}", account.address, account.balance);
}

You can implement methods on structs:

struct Account {
address: String,
balance: u64,
active: bool,
}
// Implementation block
impl Account {
// Constructor
fn new(address: &str) -> Account {
Account {
address: String::from(address),
balance: 0,
active: true,
}
}
// Method (uses &self)
fn get_balance(&self) -> u64 {
self.balance
}
// Method with mutation (uses &mut self)
fn deposit(&mut self, amount: u64) {
self.balance += amount;
}
}
fn main() {
let mut account = Account::new("0x456...");
account.deposit(500);
println!("Balance: {}", account.get_balance());
}

Enums allow you to define a type that can be one of several variants:

enum TransactionStatus {
Pending,
Confirmed,
Failed(String), // Variant with associated data
}
enum Transaction {
Transfer { from: String, to: String, amount: u64 },
Stake { address: String, amount: u64 },
Unstake { address: String },
}
fn process_transaction(transaction: Transaction) {
match transaction {
Transaction::Transfer { from, to, amount } => {
println!("Transfer {} from {} to {}", amount, from, to);
},
Transaction::Stake { address, amount } => {
println!("Stake {} from account {}", amount, address);
},
Transaction::Unstake { address } => {
println!("Unstake from account {}", address);
},
}
}

Rust has two main types of errors:

  1. Recoverable errors using the Result<T, E> type
  2. Unrecoverable errors using the panic! macro
// Result is an enum with two variants: Ok and Err
enum Result<T, E> {
Ok(T), // Success case with value of type T
Err(E), // Error case with value of type E
}
// Function that returns a Result
fn parse_address(input: &str) -> Result<String, String> {
if input.starts_with("0x") && input.len() == 42 {
Ok(input.to_string())
} else {
Err(String::from("Invalid address format"))
}
}
// Using the Result
fn main() {
let address = "0x123...";
match parse_address(address) {
Ok(valid_address) => println!("Valid address: {}", valid_address),
Err(e) => println!("Error: {}", e),
}
// The ? operator for propagating errors
fn validate_transaction(address: &str) -> Result<String, String> {
let valid_address = parse_address(address)?; // Returns error if parse_address fails
Ok(format!("Transaction with {} is valid", valid_address))
}
}

The ? operator is used to propagate errors:

fn read_and_process_file(path: &str) -> Result<String, std::io::Error> {
let mut file = std::fs::File::open(path)?; // Returns error if open fails
let mut contents = String::new();
file.read_to_string(&mut contents)?; // Returns error if read fails
Ok(contents)
}

Rust provides several collection types in its standard library.

Vectors store multiple values of the same type:

// Creating a vector
let mut numbers: Vec<i32> = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
// Vector macro
let strings = vec!["hello", "world"];
// Accessing elements
println!("First element: {}", numbers[0]);
// Safe access with get() method
match numbers.get(2) {
Some(value) => println!("Third element: {}", value),
None => println!("No third element"),
}
// Iteration
for number in &numbers {
println!("{}", number);
}

HashMaps store key-value pairs:

use std::collections::HashMap;
// Creating a HashMap
let mut balances = HashMap::new();
balances.insert(String::from("0x123..."), 1000);
balances.insert(String::from("0x456..."), 2000);
// Accessing values
match balances.get("0x123...") {
Some(balance) => println!("Balance: {}", balance),
None => println!("Account not found"),
}
// Update or insert
balances.entry(String::from("0x123...")).or_insert(500);
// Iteration
for (address, balance) in &balances {
println!("Address: {}, Balance: {}", address, balance);
}

Rust code is organized into:

  • Crates: Packages that can be shared via crates.io
  • Modules: Organize and control scope within a crate
// Define a module
mod blockchain {
// Public function
pub fn validate_transaction(tx_hash: &str) -> bool {
// Implementation
true
}
// Nested module
pub mod crypto {
pub fn hash(data: &str) -> String {
// Implementation
String::from("hashed_data")
}
// Private function (not accessible outside)
fn verify_signature() {
// Implementation
}
}
}
// Use the module
fn main() {
let valid = blockchain::validate_transaction("0x123...");
let hash = blockchain::crypto::hash("data");
}

Add dependencies to your Cargo.toml:

[dependencies]
serde = "1.0"
serde_json = "1.0"

Use them in your code:

use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Transaction {
from: String,
to: String,
amount: u64,
}
fn main() {
let tx = Transaction {
from: String::from("0x123..."),
to: String::from("0x456..."),
amount: 100,
};
let serialized = serde_json::to_string(&tx).unwrap();
println!("JSON: {}", serialized);
}

Rust has excellent support for asynchronous programming with async/await syntax:

use futures::executor::block_on;
async fn get_blockchain_data() -> String {
// Simulating an async operation
String::from("blockchain data")
}
async fn process_data() -> String {
let data = get_blockchain_data().await;
format!("Processed: {}", data)
}
fn main() {
let result = block_on(process_data());
println!("{}", result);
}
use tokio;
#[tokio::main]
async fn main() {
let result = process_data().await;
println!("{}", result);
}
async fn process_data() -> String {
// Implementation
String::from("processed data")
}

Let’s create a simplified example that demonstrates how these Rust concepts might be used in a blockchain context:

// Basic blockchain structures
struct Block {
id: u64,
timestamp: u64,
transactions: Vec<Transaction>,
previous_hash: String,
hash: String,
}
struct Transaction {
from: String,
to: String,
amount: u64,
signature: String,
}
// Implementation for Block
impl Block {
fn new(id: u64, previous_hash: &str, transactions: Vec<Transaction>) -> Self {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let mut block = Block {
id,
timestamp,
transactions,
previous_hash: previous_hash.to_string(),
hash: String::new(),
};
block.hash = block.calculate_hash();
block
}
fn calculate_hash(&self) -> String {
// Simple hash calculation (in a real app, use a crypto library)
format!("{}-{}-{}", self.id, self.timestamp, self.previous_hash)
}
}
// Blockchain implementation
struct Blockchain {
blocks: Vec<Block>,
pending_transactions: Vec<Transaction>,
}
impl Blockchain {
fn new() -> Self {
let genesis_block = Block::new(0, "0", vec![]);
Blockchain {
blocks: vec![genesis_block],
pending_transactions: vec![],
}
}
fn add_transaction(&mut self, transaction: Transaction) -> Result<(), String> {
// Validate transaction
if transaction.from.is_empty() || transaction.to.is_empty() {
return Err(String::from("Invalid addresses"));
}
if transaction.amount == 0 {
return Err(String::from("Amount must be greater than 0"));
}
// Add to pending transactions
self.pending_transactions.push(transaction);
Ok(())
}
fn mine_pending_transactions(&mut self) -> Result<Block, String> {
if self.pending_transactions.is_empty() {
return Err(String::from("No pending transactions to mine"));
}
let latest_block = self.blocks.last().unwrap();
let new_block = Block::new(
latest_block.id + 1,
&latest_block.hash,
self.pending_transactions.clone(),
);
self.blocks.push(new_block.clone());
self.pending_transactions.clear();
Ok(new_block)
}
}
// Example usage
fn main() {
let mut blockchain = Blockchain::new();
// Create a transaction
let transaction = Transaction {
from: String::from("0xAlice..."),
to: String::from("0xBob..."),
amount: 100,
signature: String::from("signed"),
};
// Add transaction and mine block
match blockchain.add_transaction(transaction) {
Ok(_) => println!("Transaction added successfully"),
Err(e) => println!("Error adding transaction: {}", e),
}
match blockchain.mine_pending_transactions() {
Ok(block) => println!("Block mined with ID: {}", block.id),
Err(e) => println!("Mining error: {}", e),
}
// Print blockchain state
println!("Blockchain has {} blocks", blockchain.blocks.len());
}