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.
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.
rustup show
rustup target add wasm32v1-noneThe toolchain file pins Rust 1.93.0 — the version polkadot-sdk stable2603 was
built and tested against.
cargo build --release./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.
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.)
| 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 |
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.
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.
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.
- BABE / GRANDPA equivocation — provable double-signing by a validator;
reported through the
sp_consensus_*runtime APIs, prosecuted bypallet_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.
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.
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.
- Identity — on-chain identity claims with registrar-issued judgements, optional sub-accounts, and a username system.
- Utility —
batch/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.
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.
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.
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 standardstate_callpath.- 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 aBabeConsensusDataProvidersoeth_callagainst"pending"resolvesblock.coinbaseto a real authority.
-
No sudo, ever. Administrative calls (
force_new_era,cancel_deferred_slash,set_code) are gated onEnsureRootor 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.setIdentitydirectly, or add Substrate Tutorial to a local fork ofapps-config. -
SS58 prefix is advertised but unused. Addresses are 20-byte hex strings, not SS58. The
42prefix lingers in the chain spec for tooling that still expects it but means nothing for user-facing serialisation. -
chain_id = 1337is a placeholder. Pick a real, registered EVM chain id before any public deployment to avoid MetaMask replay-attack collisions. -
EVM
block.coinbaseis only consistent at finalised heights. During pending-block simulation it's the slot author fromBabeConsensusDataProvider, 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).