Module 2 - Basics of Rust
What is Rust?
Section titled “What is 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
Section 1: Rust Syntax Basics
Section titled “Section 1: Rust Syntax Basics”Variables and Mutability
Section titled “Variables and Mutability”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
Data Types
Section titled “Data Types”Rust is statically typed, meaning all variables must have their types known at compile time.
Basic types:
// Integerslet a: i32 = 42; // 32-bit signed integerlet b: u64 = 100; // 64-bit unsigned integer
// Floating-point numberslet c: f32 = 3.14; // 32-bit floatlet d: f64 = 2.71828; // 64-bit float (double)
// Boolean typelet e: bool = true;
// Character typelet f: char = 'z';
// String typeslet g: &str = "hello"; // String slice, borrowedlet h: String = String::from("world"); // Owned string
Control Flow
Section titled “Control Flow”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 valueslet result = if number > 5 { "big" } else { "small" };
Loops:
// Loop indefinitely until breaklet mut counter = 0;let result = loop { counter += 1; if counter == 10 { break counter * 2; // Return a value from the loop }};
// While looplet mut number = 3;while number != 0 { println!("{}!", number); number -= 1;}
// For loop over a rangefor i in 1..5 { println!("Value: {}", i); // Prints 1 through 4}
// For loop over an iteratorlet animals = vec!["dog", "cat", "bird"];for animal in animals.iter() { println!("{}", animal);}
Section 2: Ownership and Borrowing
Section titled “Section 2: Ownership and Borrowing”The Ownership Model
Section titled “The Ownership Model”Ownership is Rust’s most unique feature and enables memory safety guarantees without garbage collection.
Core principles:
- Each value has a single owner
- When the owner goes out of scope, the value is dropped (memory freed)
- 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}
Borrowing
Section titled “Borrowing”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:
- You can have either:
- One mutable reference
- Any number of immutable references
- References must always be valid (no dangling references)
Section 3: Structs and Enums
Section titled “Section 3: Structs and Enums”Structs
Section titled “Structs”Structs group related data together:
// Definitionstruct Account { address: String, balance: u64, active: bool,}
// Usagefn 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);}
Methods
Section titled “Methods”You can implement methods on structs:
struct Account { address: String, balance: u64, active: bool,}
// Implementation blockimpl 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); }, }}
Section 4: Error Handling
Section titled “Section 4: Error Handling”Rust has two main types of errors:
- Recoverable errors using the
Result<T, E>
type - Unrecoverable errors using the
panic!
macro
Result Type
Section titled “Result Type”// Result is an enum with two variants: Ok and Errenum 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 Resultfn 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 Resultfn 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)) }}
Error Propagation
Section titled “Error Propagation”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)}
Section 5: Collections
Section titled “Section 5: Collections”Rust provides several collection types in its standard library.
Vectors
Section titled “Vectors”Vectors store multiple values of the same type:
// Creating a vectorlet mut numbers: Vec<i32> = Vec::new();numbers.push(1);numbers.push(2);numbers.push(3);
// Vector macrolet strings = vec!["hello", "world"];
// Accessing elementsprintln!("First element: {}", numbers[0]);
// Safe access with get() methodmatch numbers.get(2) { Some(value) => println!("Third element: {}", value), None => println!("No third element"),}
// Iterationfor number in &numbers { println!("{}", number);}
HashMaps
Section titled “HashMaps”HashMaps store key-value pairs:
use std::collections::HashMap;
// Creating a HashMaplet mut balances = HashMap::new();balances.insert(String::from("0x123..."), 1000);balances.insert(String::from("0x456..."), 2000);
// Accessing valuesmatch balances.get("0x123...") { Some(balance) => println!("Balance: {}", balance), None => println!("Account not found"),}
// Update or insertbalances.entry(String::from("0x123...")).or_insert(500);
// Iterationfor (address, balance) in &balances { println!("Address: {}, Balance: {}", address, balance);}
Section 6: Modules and Crates
Section titled “Section 6: Modules and Crates”Project Structure
Section titled “Project Structure”Rust code is organized into:
- Crates: Packages that can be shared via crates.io
- Modules: Organize and control scope within a crate
// Define a modulemod 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 modulefn main() { let valid = blockchain::validate_transaction("0x123..."); let hash = blockchain::crypto::hash("data");}
Using External Crates
Section titled “Using External Crates”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);}
Section 7: Asynchronous Programming
Section titled “Section 7: Asynchronous Programming”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);}
Using Tokio (common async runtime):
Section titled “Using Tokio (common async runtime):”use tokio;
#[tokio::main]async fn main() { let result = process_data().await; println!("{}", result);}
async fn process_data() -> String { // Implementation String::from("processed data")}
Section 8: Putting It All Together
Section titled “Section 8: Putting It All Together”Let’s create a simplified example that demonstrates how these Rust concepts might be used in a blockchain context:
// Basic blockchain structuresstruct 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 Blockimpl 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 implementationstruct 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 usagefn 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());}