Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Substrate Tutorial

A Substrate-based solochain with BABE block production, GRANDPA finality, NPoS validator economics, OpenGov governance, a sovereign treasury, the institutional-accounts layer (identity, utility, multisig, proxy), an H160-native account model, and full Frontier EVM compatibility (pallet-evm + pallet-ethereum + full eth_* JSON-RPC). Built against the polkadot-sdk stable2603 branch and frontier stable2603.

Tutorial

This repository is a lesson-by-lesson tutorial: the chain is built up one capability at a time, and each commit is a single, self-contained step. The companion writeup in tutorial/ walks through every step — what it adds, the design decision behind it, and the alternatives not taken — starting from a bare AURA + GRANDPA node and ending with the full governed, H160-native, EVM-capable chain described above.

Start at the introduction.

Toolchain

rustup show
rustup target add wasm32v1-none

The toolchain file pins Rust 1.93.0 — the version polkadot-sdk stable2603 was built and tested against.

Build

cargo build --release

Run a dev node

./target/release/substrate-tutorial --dev

--dev selects the development chain spec: a single-validator (Alith) chain with the six Moonbeam test accounts (Alith, Baltathar, Charleth, Dorothy, Ethan, Faith) endowed at genesis. Alith is bonded as the genesis validator with 100 UNIT and marked invulnerable. State is purged on restart unless --base-path is set.

Run a local two-node testnet

Use the local chain spec on both nodes (Alith + Baltathar as authorities):

./target/release/substrate-tutorial --chain local --alice --base-path /tmp/substrate-tutorial/alice
./target/release/substrate-tutorial --chain local --bob   --base-path /tmp/substrate-tutorial/bob \
  --bootnodes /ip4/127.0.0.1/tcp/30333/p2p/<alith-peer-id>

(The --alice / --bob CLI flags inject the standard sr25519/ed25519 session keys; Alith's stash account is bound to Alice's session keys in substrate-tutorial-keyring, and likewise Baltathar ↔ Bob.)

Chain parameters

Property Value
Block production BABE (PrimaryAndSecondaryPlainSlots)
Finality GRANDPA
Block / slot time 6 s
Epoch length 100 slots (≈ 10 min)
Sessions per era 144 (≈ 24 h)
Bonding duration 28 eras (≈ 28 days)
Slash defer duration 27 eras
Primary slot prob. 1/4
Reward curve Polkadot (2.5%–10%, target 50% staked)
Token symbol UNIT (12 decimals)
Existential deposit 1 mUNIT
Genesis validator bond 100 UNIT
Account model H160 (20-byte Ethereum addresses)
Signature scheme ECDSA (EthereumSignature)
SS58 prefix 42 (advertised but unused; addresses are H160)
Fee split 20% treasury / 80% author
Tip split 100% treasury
Treasury burn 1% per 24-day spend period
OpenGov tracks 7 (Root, StakingAdmin, GeneralAdmin, ReferendumCanceller, ReferendumKiller, Treasurer, BigSpender)
Proxy types 6 (Any, NonTransfer, Governance, Staking, IdentityJudgement, CancelProxy)
EVM chain id 1337
EVM block gas limit 75,000,000
Precompiles 6 (ECRecover, SHA256, RIPEMD160, Identity, Modexp, SHA3-FIPS @ 0x400)
Spec / impl version 107 / 1

Pallets

27 pallets, indices 0–26:

System, Babe, Timestamp, Authorship, Balances, TransactionPayment, Staking, Session, Historical, Grandpa, ImOnline, Offences, Scheduler, Preimage, ConvictionVoting, Referenda, Origins, Treasury, Identity, Utility, Multisig, Proxy, Ethereum, EVM, EVMChainId, BaseFee, DynamicFee.

Session keys

Three keys per validator: babe (sr25519), grandpa (ed25519), im_online (sr25519). Rotate with author_rotateKeys against a validator RPC port and submit the resulting blob via session.setKeys from the validator's stash account.

Validator economics

NPoS with on-chain Phragmén election. Validators bond UNIT, optionally nominators delegate stake to them (up to 16 nominations per nominator), and each era an on-chain election picks up to 100 validators by total backing. Rewards follow the Polkadot reward curve: ~2.5% min inflation, ~10% max, targeting 50% of total issuance staked. Reward dust (RewardRemainder) and slashes accumulate in the treasury.

Slashing

  • BABE / GRANDPA equivocation — provable double-signing by a validator; reported through the sp_consensus_* runtime APIs, prosecuted by pallet_offences.
  • Liveness (ImOnline) — validators send unsigned heartbeats each session; missing heartbeats trigger an UnresponsivenessOffence.

Slashed funds accumulate in the treasury. Slashes are deferred 27 eras before execution. Genesis validators are marked invulnerable.

Governance

OpenGov (Gov2). Anyone with bonded UNIT can submit a referendum into one of 7 tracks; each track has its own approval/support curves, deposits, and decision/confirm periods (mirroring Polkadot's curves). Conviction voting locks balances for 1×–32× voting weight. Approved referenda execute through pallet-scheduler after the enactment period. EnsureRoot is reachable via the Root track.

Treasury

Sovereign on-chain account fed by:

  • 20% of every transaction's base fee
  • 100% of every transaction's tip
  • staking slashes and reward remainders
  • forfeited referendum deposits (when killed/rejected)

Spending happens through OpenGov referenda on the Treasurer (unlimited) or BigSpender (1,000,000 UNIT cap) tracks. 1% of the unspent balance is burned each SpendPeriod.

Accounts as institutions

  • Identity — on-chain identity claims with registrar-issued judgements, optional sub-accounts, and a username system.
  • Utilitybatch / batch_all / as_derivative / with_weight. Atomic batching and deterministic sub-accounts.
  • Multisig — deterministic M-of-N accounts with on-chain approval flow.
  • Proxy — delegated keys with InstanceFilter-scoped permissions. Six proxy types: Any, NonTransfer, Governance, Staking, IdentityJudgement, CancelProxy.

Account model

H160 (20-byte) addresses end-to-end. Signature = EthereumSignature, AccountId = AccountId20, Address = AccountId, Lookup = IdentityLookup<AccountId>. Extrinsics are signed with secp256k1 over the SCALE-encoded payload — the same curve Ethereum uses, but not an Ethereum transaction, so MetaMask can't sign a native extrinsic. Polkadot.js Apps signs them through the Ethereum crypto path instead; MetaMask signs EVM transactions once Frontier is in place. The chain spec advertises isEthereum: true, which makes Polkadot.js Apps display Ethereum-style hex addresses and route signing through the Ethereum prompt path.

Genesis presets endow six well-known test accounts (Alith, Baltathar, Charleth, Dorothy, Ethan, Faith) whose private keys are published with the Moonbeam test keyring. Import any of them into MetaMask to sign EVM transactions. The substrate-tutorial-keyring crate exposes these identities for genesis, tests, and tooling.

Validator session keys (BABE sr25519, GRANDPA ed25519, ImOnline sr25519) are still substrate-flavored — EthereumSignature covers user extrinsics but block production and finality use the regular Substrate signature schemes. substrate-tutorial-keyring binds each H160 test account to a fixed sr25519/ed25519 pair (Alith ↔ Alice, Baltathar ↔ Bob, …) so test deployments have predictable session keys.

EVM

Full Frontier EVM compatibility on top of the H160 account model. Ethereum transactions ride a fp_self_contained::UncheckedExtrinsic envelope so they dispatch through pallet_ethereum::transact without an outer Substrate signature. Six Ethereum-mainnet-compatible precompiles are wired at the usual addresses (0x01-0x05) plus SHA3-FIPS at 0x400. block.coinbase resolves to the BABE primary author via a FindAuthorTruncated<Babe> adapter (sr25519 public key truncated to 20 bytes). Gas fees use pallet_base_fee (EIP-1559 style) atop pallet_dynamic_fee to track network demand. EVM chain_id is exposed via pallet_evm_chain_id and advertised in chain spec properties.

JSON-RPC

Beyond the standard chain_* / state_* / system_* namespaces:

  • babe_epochAuthorship (which authorities are eligible per slot of the current epoch — sc_consensus_babe_rpc)
  • grandpa_roundState, grandpa_proveFinality, grandpa_subscribeJustifications (sc_consensus_grandpa_rpc)
  • pallet_staking_runtime_api::StakingApi: nominations_quota, eras_stakers_page_count, pending_rewards — accessed by frontends through the standard state_call path.
  • Full Ethereum-compatibility RPC: eth_* (chainId, blockNumber, getBalance, getCode, call, estimateGas, sendRawTransaction, getTransactionByHash, getTransactionReceipt, …), eth_filter_* (newFilter, getFilterLogs, getLogs, …), eth_subscribe / eth_unsubscribe (newHeads, logs, pendingTxs), net_* (version, listening, peerCount), web3_* (clientVersion, sha3), debug_* (traceTransaction, traceBlock), and (with --features txpool) txpool_*. Pending-block simulation uses a BabeConsensusDataProvider so eth_call against "pending" resolves block.coinbase to a real authority.

Caveats

  • No sudo, ever. Administrative calls (force_new_era, cancel_deferred_slash, set_code) are gated on EnsureRoot or track-specific origins; reaching them requires a successful OpenGov referendum on the appropriate track. In production with the default Root-track curves, runtime upgrades and similar calls have a 28-day decision period.

  • No bags-list, no nomination-pools, no multi-phase election. On-chain Phragmén is fine up to ~1000 stakers. Beyond that, add those pallets.

  • No bounties / tips / recovery / vesting / assets. Spending happens exclusively through OpenGov spend tracks. Add the relevant pallets when the chain has a workflow that benefits from them.

  • Polkadot.js Apps hides the "Set on-chain identity" menu for unknown chains. Use Developer → Extrinsics → identity.setIdentity directly, or add Substrate Tutorial to a local fork of apps-config.

  • SS58 prefix is advertised but unused. Addresses are 20-byte hex strings, not SS58. The 42 prefix lingers in the chain spec for tooling that still expects it but means nothing for user-facing serialisation.

  • chain_id = 1337 is a placeholder. Pick a real, registered EVM chain id before any public deployment to avoid MetaMask replay-attack collisions.

  • EVM block.coinbase is only consistent at finalised heights. During pending-block simulation it's the slot author from BabeConsensusDataProvider, which is correct but speculative — fully accurate only once the block is imported.

  • Existing databases must be purged when crossing a runtime version boundary (spec_version changes).

About

Substrate Tutorial

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages