FABRKNT
Reth Expert — Production Engineering
Reth-based Chains — Reading the Extension Pattern
Lesson 20 of 25·CONTENT14 min40 XP

Treat this page as a workbench, not a blog post. The goal is to extract a reusable mental model from the source and carry it into the rest of the Fabrknt stack.

Course
Reth Expert — Production Engineering
Lesson role
CONTENT
Sequence
20 / 25

Lesson 19 — Custom ChainSpec — forks, genesis, and the precompile schedule

Question

ChainSpec = genesis + forks + precompile schedule + chain ID. Customise per chain. Reth's NodeBuilder takes one as input.

Principle (minimum model)

  • Genesis state. Pre-deployed contracts + initial balances + nonces. Defined in JSON.
  • Hardfork schedule. Block-number-keyed list: "at block 100, activate Berlin. At block 200, activate London."
  • Precompile schedule. Which precompiles at which block. Mainnet has 9; custom chains may add more at specific blocks.
  • Chain ID. Unique per chain. 1 = mainnet, 137 = polygon, etc. Custom = pick unused.
  • Validation. Reth's ChainSpec::validate checks for consistency. Conflicts fail fast.
  • Testing. Spin up a node with the chainspec; mine some blocks; assert hardforks activate.
  • Production examples. Sepolia testnet, OP Mainnet, Hyperliquid, Tempo. Each has its own chainspec.

Worked example + steps

Custom ChainSpec — forks, genesis, and the precompile schedule

The block validates on mainnet but rejects on your chain. Same block, same client binary, same Revm — different result. Why? Because something inside ChainSpec said "at this height, the rules are different here." A wrong fork height, a wrong precompile schedule entry, a wrong base-fee parameter — any one of them, and your chain forks itself off the network in one block.

ChainSpec is the Rust struct that owns "what makes this chain different from mainnet Ethereum at the protocol level" — chain ID, fork activation, base fee curve, genesis allocation, precompile schedule. If you're going to read or build a Reth-based chain, this is the type you read first.

1. What ChainSpec is

In reth-chainspec, ChainSpec is a struct (with various extensions in chain-specific crates) that captures:

CategoryWhat it controls
Chain IDEIP-155 replay protection key
Hardfork activationBlock-height- or timestamp-based switches for protocol upgrades
Base fee paramsEIP-1559 parameters (elasticity, change denominator)
GenesisInitial allocations, state root, gas limit
Precompile scheduleWhich precompile addresses are active at each fork
Misc legacy paramsBlock gas limits, DAO fork, mining difficulty (legacy)

For Reth-based L2s, the chain provides an extended ChainSpec — e.g., the OP chain spec wraps the base ChainSpec and adds OP-specific fork tracking (Bedrock, Canyon, Ecotone, Fjord, ...).

2. The hardfork list as the chain's history

Reading the hardfork enum out loud is the fastest way to understand a chain.

For OP Stack you'll find an enum roughly like:

pub enum OptimismHardfork {
    Bedrock,
    Regolith,
    Canyon,
    Ecotone,
    Fjord,
    Granite,
    Holocene,
    // ...
}

Each variant comes with activation logic (block height on mainnet, separate timestamp on each network like Sepolia, Base, etc.). Reading this enum + its activation table = reading the chain's entire protocol history.

For Tempo, you can verify the same shape directly in tempoxyz/tempo — different fork names, same enum + activation-table structure.

3. The precompile schedule

A precompile is a "native function" living at a reserved address (0x00..01 through 0x00..0a on mainnet, plus optional extras). Each chain decides which precompiles exist at which fork.

OP Stack inherits most of Ethereum's precompiles and adds a few of its own. Future hardforks add more. The precompile schedule is essentially:

At fork F, address A maps to native function impl I

You'll find this in the chain's EVM config crate (covered in the next lesson), but the activation gating lives in ChainSpec — because activation is a consensus rule.

4. Genesis encoding

Genesis is just "the state at block 0." A custom chain ships:

  • A genesis JSON file (allocations, gas limit, initial difficulty/seal)
  • A Genesis Rust struct in the chainspec crate, often loadable from the JSON
  • A computed genesis state root that all nodes must agree on

If you're auditing a chain, verify the genesis state root in code matches the network. Disagreement here means every node disagrees on block 1.

5. What's special about an L2 chainspec

L2 chainspecs (Optimism, Base, ...) also track:

  • L1 chain ID the L2 is anchored to (for cross-domain message verification)
  • L1 block oracle address on the L2 (the contract that records the current L1 block hash)
  • Sequencer address (for sequencer-signed batch validation)
  • Withdrawal config (the time delay for L2→L1 withdrawals)

These don't apply to a Tempo-style L1, but illustrate the kind of thing that lives in an extended ChainSpec.

6. Reading exercise

In crates/optimism/chainspec/ (or wherever the OP chainspec lives in your reth checkout):

  1. Find the struct that represents an OP chain spec
  2. Read its hardfork list out loud
  3. Locate the function that answers "is fork F active at block height H, timestamp T?"
  4. Find where Bedrock's activation block is hard-coded for OP mainnet vs Base

Now do the same for any other chain in awesome-reth's "Layer 2" section.

Final check: if I asked you "what fork activation rule does chain X use at block N?", what files would you need to read, and in what order? If your answer is more than 2 files, you're over-complicating it — ChainSpec + the activation table is the whole story.

Summary (3 lines)

  • ChainSpec = genesis + hardfork schedule + precompile schedule + chain ID. JSON config.
  • Validation via ChainSpec::validate. Test by spinning up node + mining.
  • Production: each L1/L2 has its own chainspec. Reth: pluggable via NodeBuilder. Next: custom Executor.