Skip to content

Latest commit

 

History

History
178 lines (112 loc) · 9.72 KB

File metadata and controls

178 lines (112 loc) · 9.72 KB

Prerequisites

Reading time: ~8 min.

This tutorial assumes you've written some Rust and have a vague idea what a blockchain is. It does not assume you've used Substrate before. This page lists what to read before lesson 1 if you want the rest to make sense, plus a glossary of Substrate-specific terms.

What to know before starting

Rust, at the level of:

  • You've read The Rust Book or equivalent.
  • You can write a function that uses traits, generics, and lifetimes without panicking.
  • You understand Result, Option, and the ? operator.
  • You've seen async/.await in a project and aren't allergic.

You don't need to be fluent in advanced macro-foo or trait-object internals — Substrate's heavy macro magic is something we'll point at, not write from scratch.

Blockchain basics, at the level of:

  • You know what a block is, and that blockchains link blocks via hashes.
  • You know there are two separate problems: who picks the next block (block production), and when is a block irreversible (finality).
  • You've heard of Proof-of-Work and Proof-of-Stake, even if you couldn't explain the math.
  • You know what "validator" means roughly: a node empowered to author blocks.

If those bullets are uncomfortable, read Polkadot Wiki — Consensus basics first. 30 minutes.

Async Rust, at the level of:

  • You can read async code without being startled by it.
  • You roughly understand "futures don't run unless awaited" and "Tokio is a runtime."
  • You won't be confused when you see async fn returning something that needs .awaiting.

We don't write much async ourselves — Substrate's node-side service code uses it heavily, but most of what we touch is configuration.

Substrate-specific concepts

The two pieces

A Substrate chain is two binaries wearing one binary name:

  • Runtime — a Rust crate compiled to WebAssembly. Defines all state transitions. Lives in runtime/. Every node in the network runs the same Wasm; that's how they agree.
  • Node (or "client") — a native Rust binary. Hosts the database, networking, consensus, RPC. Imports the runtime as a Wasm blob. Lives in node/.

Two questions follow naturally:

  • Where does the Wasm come from? A build script (runtime/build.rs) compiles the runtime to Wasm during cargo build, using substrate-wasm-builder. The compiled blob is embedded into the node binary.
  • How do the two communicate? Through runtime APIs — Wasm-exported functions the node calls. Defined by the impl_runtime_apis! macro in runtime/src/apis.rs. Examples: Core::version(), BlockBuilder::apply_extrinsic(tx), BabeApi::configuration().

Pallets and FRAME

A pallet is a self-contained piece of runtime logic. It defines:

  • Its own storage (key-value items, maps, double-maps)
  • Its own extrinsics (callable functions)
  • Its own events
  • Its own errors
  • A Config trait that the runtime fills in with concrete types

A runtime is built by listing pallets and providing each one's Config. The #[frame_support::runtime] macro generates the glue: RuntimeCall enum, RuntimeEvent enum, dispatch, metadata, storage prefixing.

FRAME is the framework that defines what a pallet is. "FRAME pallet" = "pallet built with FRAME conventions" — which is what virtually all substrate runtime code looks like.

Origins

When an extrinsic is dispatched, the runtime needs to know who's calling. The "origin" type carries that:

  • Origin::signed(account) — a signed transaction from account
  • Origin::root — root (admin) call, reachable via sudo or governance
  • Origin::none — unsigned call (for inherent extrinsics like timestamp, or for unsigned extrinsics like equivocation reports)

Each extrinsic specifies which origins it accepts. EnsureSigned, EnsureRoot, EnsureNone, etc., are the canonical guards.

Extrinsics

A unit of input to the chain. Three flavors:

  • Signed — most user transactions. Signed by an account, pays fees.
  • Unsigned — sent without a signature, but validated by per-pallet logic. Used by trustless reporters (equivocations, heartbeats).
  • Inherent — special unsigned data the block author must include. Timestamps, BABE slot info, etc.

Storage

Pallets declare storage items in their #[pallet::storage] blocks. Storage is key-value at the trie level; FRAME wraps that in typed maps and values. Reads and writes have weights (gas-equivalent costs); the chain limits per-block compute via BlockWeights.

Weights and fees

Substrate doesn't use "gas" the way EVM does. Instead, each extrinsic has a weight — pre-computed by frame-benchmarking or, for development, set to a default. Fees are computed from length × LengthToFee + weight × WeightToFee + tip. Block production has a weight budget; once exhausted, no more extrinsics fit in the block.

Our chain uses IdentityFee<Balance> — fee = weight directly. Production chains tune this to map to economic units.

The wasm-builder

substrate-wasm-builder is the build dependency that runs the runtime's build.rs. It:

  1. Creates a temporary Cargo project with just the runtime as a member.
  2. Compiles that project for the wasm32v1-none target (Rust ≥ 1.84) or wasm32-unknown-unknown (older).
  3. Optionally compresses the resulting Wasm.
  4. Writes a constant WASM_BINARY: &[u8] for the node to embed.

This is why building a runtime takes long: every release build rebuilds the Wasm blob.

Session keys

A validator runs the node with one or more session keys — private keys used to sign consensus messages (BABE block claims, GRANDPA finality votes, ImOnline heartbeats). Session keys are different from the validator's stash key (which holds bonded funds).

The set of session keys is declared in impl_opaque_keys! and rotated via pallet-session::set_keys.

Genesis

The state of the chain at block 0. Defined by the runtime's GenesisConfig (auto-generated from pallets' genesis_config declarations) and exposed through the sp_genesis_builder::GenesisBuilder runtime API.

A chain spec is a JSON file that selects which preset to use or provides the raw genesis state. Three flavors:

  • Plain chain spec — references a preset by name (e.g., "dev").
  • Raw chain spec — pre-computed Storage keys/values. What production chains ship.
  • Genesis state hash — just the root, used internally.

The polkadot-sdk monorepo

Polkadot-sdk is the monorepo containing Substrate, Polkadot, Cumulus, and adjacent code. We pin our chain to its stable2603 branch.

Layout you'll encounter often:

  • substrate/frame/<pallet>/ — runtime pallets (e.g., frame/balances, frame/staking).
  • substrate/primitives/<sp-crate>/ — runtime primitives (e.g., primitives/core, primitives/runtime).
  • substrate/client/<sc-crate>/ — node-side libraries (e.g., client/consensus/babe, client/network).
  • substrate/utils/<utility>/ — build helpers, CLIs (e.g., utils/wasm-builder).
  • templates/solochain/ — the minimal solochain template we forked from.
  • substrate/bin/node/ — the canonical full reference node (Polkadot-shaped).

When the tutorial says "check substrate/bin/node for the canonical pattern," it means: read the actual code in this monorepo at the same branch we pin. That's the source of truth.

What you'll touch

The work splits roughly:

  • Cargo.toml — workspace + dep pins. Most "add a pallet" PRs touch this.
  • runtime/Cargo.toml — runtime crate deps + [features].std list.
  • runtime/src/lib.rs — runtime declaration, session keys, constants, runtime types, offchain trait impls.
  • runtime/src/configs.rs — every impl Config for Runtime block.
  • runtime/src/apis.rs — every impl_runtime_apis! block.
  • runtime/src/genesis_config_presets.rs — dev and local-testnet genesis JSON patches.
  • node/src/service.rs — the heaviest node file. Wires consensus, networking, RPC, task management.
  • node/src/rpc/ — JSON-RPC method registration (a single file node/src/rpc.rs for most of the tutorial; it becomes a directory in the final lesson).
  • node/src/chain_spec.rs, cli.rs, command.rs, main.rs — small files, occasionally touched.

If you understand the role of each file, the rest of the tutorial is a story about which ones change at each step and why.

Setting up your environment

Outside the scope of the tutorial in detail, but the short version:

# 1. Install Rust (rustup will pin to the version in rust-toolchain.toml)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. Clone Substrate Tutorial
git clone <repo> substrate-tutorial && cd substrate-tutorial

# 3. Build
cargo build --release

# 4. Run the dev chain
./target/release/substrate-tutorial --dev

If the build fails with "libclang not found," install your distro's libclang-dev / clang package. If it fails with "wasm32 target not installed," rustup target add wasm32v1-none (the toolchain file should handle this, but doesn't always).

What this tutorial is not

  • A Rust tutorial. If you can't follow the Rust, learn Rust first.
  • A FRAME-from-scratch guide. We use pallets as a consumer. Writing your own pallet is its own (substantial) topic; we'll point at resources when it comes up.
  • A Polkadot/Kusama tutorial. Polkadot is a relay chain; this is a solochain. Most concepts transfer, but XCM, parachains, and bridging are out of scope unless we explicitly cover them.
  • A production deployment guide. We talk about deployment considerations, but we don't cover infrastructure, monitoring, key custody, validator operations, or chain governance practices.

Next up

Lesson 1 — A solochain that breathes: AURA + GRANDPA