This workshop will primarily focus on introducing those new to Substrate/Polkadot SDK to Rust (but perhaps not new to programming). It aims to cover the necessary material to introduce Rust tooling, the concepts used in Substrate, and having them run their first lines of Rust code locally on their machine.
Note: if you want to copy-paste this into a hackmd, I recommend copying from here below, not from the very beginning!
- Install Rust and its tooling
- Learn the basics of Rust, along with how certain features are useful for blockchain development.
- Learn why Rust is important as a programming language, especially in the context of blockchains.
- Learn how to write some basic Rust
- Go over the importance of generic programming, and what it can do
- By the end of this workshop, you should:
- Have Rust, cargo, and its associated tooling installed
- Learn overarching Rust concepts needed to start Substrate dev
- Run your first lines of Rust code, and be aware of some of its foundational concepts
- Be exposed to an actual pallet environment
- https://www.rust-lang.org/tools/install
- To verify, run
cargo --versionorrustc --version - Should take you less than 15 mins!
- Also, for later, run:
cargo install cargo-expand(optional)
git clone git@github.com:CrackTheCode016/intro-to-rust-workshop.gitcargo run --bin scratchpad Compiling scratchpad v0.1.0 (/Users/bader/Documents/ethdenver/webzero-intro-rust/scratchpad)
Finished dev [unoptimized + debuginfo] target(s) in 0.93s
Running `target/debug/scratchpad`
Hello, world!cargo check- less than building, checks if it can compilecargo run- builds and runscargo build- just builds
- Rust Launch - 2015
- Polkadot whitepaper in 2016 (genesis block in 2020)
- Polkadot writtten in Rust
- So is Substrate, Cumulus... most things.
- Wasmi - WebAssembly interpreter
- Polkavm - RISC-V based virtual machine
- SCALE-codec - serialization format
- Contributions to other libraries - libp2p, etc
- TLDR - Safe and fast code.
- Easily compile to WebAssembly (and other targets)
- A good option for sensitive applications
- Strict and extensible type system (static & strongly typed)
- The Rust compiler yells at you.. a lot (in a good way!)
- Ownership in Rust - avoiding common mem managment
fn take_my_value(str: String) {
println!("{str}");
}
fn main() {
let my_name = String::from("Bader");
take_my_value(my_name);
println!("{my_name}");
}error[E0382]: borrow of moved value: `my_name`
--> src/main.rs:9:14
|
7 | let my_name = String::from("Bader");
| ------- move occurs because `my_name` has type `String`, which does not implement the `Copy` trait
8 | take_my_value(my_name);
| ------- value moved here
9 | println!("{my_name}");
| ^^^^^^^^^ value borrowed here after move
...- Rust can compile to WebAssembly, allowing interop between languages
- Once you know it, it's easy to prototype with
- Bottomline: protects against human error
- Shameless shill: badery.co built with Rust (wasm-based blog)

We will quickly go over some Rust concepts.
Note that this is just what's important for us right now, there is obviously a lot more in Rust
- Strongly and statically typed
- Focuses on memory safety
- Combines concepts of object-oriented and functional programming
- Powerful memory managment system w/o a garbage collector
- A Rust project / package is called a crate
- A workspace is multiple crates as a single project
- Cargo is our build tool
- Immutable by default, must explicitly declare mutability
let age: u32 = 22;
❌ age += 1;
// A mutable variable (also, note shadowing)
let mut age: u64 = 22;
✅ age += 1;Vec= Growable arrays (handy macro)HashMap= Sets/Dictionary (std::collections::HashMap)String= a&stryou can modify
let vector = vec![1, 2, 3];
// or
let mut vector: Vec<u32> = Vec::new();
vector.push(1);
let mut hash_map: HashMap<u32, u32> = HashMap::new();
hash_map.insert(1, 50);
let string = String::from("Hey!");
// No parameters or return type
fn a_function() {}
// Just parameters
fn print_age(age: u32) {
// (btw, this is how you print stuff)
println!("{age}");
}
// Both (note return syntax)
fn increase_age(age: u32) -> u32 {
age + 1
}struct Person {
name: String,
age: u32
}- One is more like a constructor
- Using
&selfrequires the struct to be instantiated first
impl Person {
fn new(name: &str, age: u32) -> Self {
Person { String::from(name), age }
}
fn say_name(&self) {
println!("yo my name is {}", self.name);
}
}
// .. later
Person::new("Bader", 22);
Person.say_name();- Define common behavior for an object
trait Sound {
fn make_sound(&self);
}
impl Sound for Person {
fn make_sound(&self) {
println!("hello, {}", self.name)
}
}
// .. later
person.make_sound(); // "hello, name"- Common traits to print, clone, etc
- #[derive] macro used to avoid boilerplate
#[derive(Clone, Debug)]
struct Person {
name: String,
age: u32
}
// ..
println!("{:?}", person);
// .. or
let new_person = person.clone();- Placeholder types
Calculator<u32>Calculator<i32>Calculator<String>
struct Calculator<T> {
// T is just a placeholder
// for a type we put in the future
result: T
}- Another type of generic
- Can be used to configure traits
trait Factory {
// Product is an associated type
type Product: Clone + Debug;
fn make_product() -> Self::Product;
}
// .. later:
type Product = Table;- Associated Types - Use when you only need to implement a trait once.
pub struct EvmRuntime;
impl RuntimeConfig for EvmRuntime {
type Balance = u128;
type AccountId = [u8; 64];
type ExecutableCode = String;
}- Generics - Use for when you need to implement a trait for multiple types.
// 32 Bit Signed Integer
let squared_i32 = square::<i32>(10);
// 32 Bit Unsigned Integer
let squared_u32 = square::<u32>(10);
// Floating Point Number
let squared_f32 = square::<f32>(10.0);- Bound placeholder types (generics) with traits
- In this case, we could pass in both
Dogor aPersonand it doesn't matter
fn make_lots_of_noise<T: Sound>(thing: T, thing2: T) {
//.. other stuff
thing.make_sound();
//.. more stuff
}- Generics & associated types being used 😲
// Output is an associated type of Mul
fn square<T: Mul<Output = T> + Copy>(x: T) -> T {
x * x
}- Restrict it only to numbers that "can count"
struct Calculator<T: CanCount> {
result: T
}- Code the generates code
- Elimiates boilerplate
println!();
// or...
::std::io::_print(format_args!("\n"));macro_rules! println {
() => { ... };
($($arg:tt)*) => { ... };
}- If you installed cargo-expand..
cargo expandin any crate that has macros will give you the output of the macro
- Now, to put it together
- Generic Programming: Make assumptions about behavior, but not specific implemention
- Generic traits - creating reusable APIs
- Doesn't make assumptions about what you implement
- Uses traits and generics to tell you how to implement. (Blocks, extrinsics, fork choice rules..)
- Traits used as a guide for how to implement common components (i.e., a block, a transaction, a pallet)
- Pallets are building blocks for your blockchain
- Each pallet's configuration is a trait, called
Config, which has associated types. - The runtime itself has a configuration trait
/// Configure the pallet by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config {
/// The max amount of messages per user per conversation
#[pallet::constant]
type MaxMessageAmount: Get<u32>;
}
- Later, all of these are combined into one big runtime, which is essentially the business logic of our blockchain
- But don't worry about that right now!
impl pallet_uke::Config for Runtime {
type MaxMessageAmount = ConstU32<16>;
}
- Our goal: provide a way to create factories for multiple processes of the same kind
- Raw Materials -> Processes -> Products
- Example: IronOre -> Process It -> Steel
Later, we'll import this as a module inside of main.rs
- The generic
Pprocess - We can access associated types from our trait!
- This is only one type of process per factory (homogenous factory)
pub struct Factory<P: Process> {
factory_id: u32,
processes: Vec<P>,
completed_products: HashMap<usize, P::Product>,
}
impl<P: Process> Factory<P> {
// Our "constructor"
pub fn new(factory_id: u32) -> Self {
Factory { factory_id, processes: vec![], completed_products: HashMap::new() }
}
pub fn process_all(&mut self) {
// loop thru and run "push_along_belt", then push the products to completed products.
for (id, process) in self.processes.iter().enumerate() {
println!("Processing {}...", id);
let product: <P as Process>::Product = process.push_along_the_belt(process.material());
self.completed_products.insert(id, product.clone());
println!("Process successful, product produced! {:?}...", product);
}
}
pub fn add_new_process(&mut self, process: P) {
self.processes.push(process)
}
// Run the factory
pub fn run(&mut self) {
// run all belts
self.process_all();
}
}
- Remember: we want to define how a process works, not just a process itself
- A blueprint for infinitely many processes!
- Note: we can also use trait bounds on associated types.
trait Process {
// Note the use of associated functions as a means to "configure" our belt
type RawMaterial;
type Product: Debug + Clone;
/// Processes the item from a raw material to a product.
fn push_along_the_belt(&self, material: Self::RawMaterial) -> Self::Product;
// The raw material to process on this belt
fn material(&self) -> Self::RawMaterial;
}
pub struct IronOre {
pub amount: u32
}
#[derive(Debug, Clone)]
pub struct Steel {
pub amount: u32,
}
pub struct OreProcess {
pub amount: u32
}
impl Process for OreProcess {
type RawMaterial = IronOre;
type Product = Steel;
fn push_along_the_belt(&self, material: Self::RawMaterial) -> Self::Product {
Steel { amount: material.amount / 2 }
}
fn material(&self) -> Self::RawMaterial {
IronOre { amount: self.amount }
}
}
- Go to
scrachpad/src/main.rsand replace the main function with:
/// Do factory stuff.
fn main() {
let ore_process = OreProcess { amount: 20 };
let mut metal_factory: Factory<OreProcess> = Factory::new(0);
metal_factory.add_new_process(ore_process);
metal_factory.run();
}
- We defined how a factory process operates via a trait
- We successfully created a new process, and ran our factory!
- Lots of innovation can happen (multithreading for process, multi-process Factory, a Factory trait...)
- Usually this is with the node-template, but this specific repo is just the pallet on its own
https://github.com/Uke-Messaging/uke-pallet
-
https://paritytech.github.io/polkadot-sdk/master/frame_system/pallet/trait.Config.html
-
The main purpose of this trait is to act as an interface between this pallet and the runtime in which it is embedded in. A type, function, or constant in this trait is essentially left to be configured by the runtime that includes this pallet.
- The Rust Book
- Rust State Machine by Shawn Tabrizi
- Substrate Node Template
- Substate in a nutshell
- On generics and associated types
