FABRKNT
Reth Expert — Production Engineering
Reth-based Chains — Reading the Extension Pattern
Lesson 21 of 25·CONTENT18 min45 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
21 / 25

Lesson 20 — Custom executor — swapping the execution layer

Question

Custom Executor = override how Reth processes txs. Standard = revm with mainnet rules. Custom = OP fee handling, custom precompiles, etc.

Principle (minimum model)

  • Executor trait. execute_block(block, state) -> ExecutionResult. Reth calls this per block.
  • Standard impl. EthExecutor runs revm. Used by default.
  • Custom impl. Override execute_block to use revm with custom precompiles + custom fee handling.
  • Wire via NodeBuilder. NodeBuilder::executor(YourExecutor::default()).
  • Test. Run a tx through; assert behaviour differs from default Eth.
  • Common customisations. OP L1 fee handling, Hyperliquid CLOB precompiles, Tempo merchant attestation, Berachain PoL.
  • Composes with other components. Custom Executor + Custom Pool + Custom Consensus = a fully custom L1.
  • Production parallel. All Reth-based L1s/L2s ship custom executors.

Worked example + steps

Custom executor — swapping the execution layer

The executor is "what actually runs the transactions and produces the post-state." For Ethereum mainnet, this is vanilla revm. For Optimism, it's revm plus deposit-tx handling, plus L1 cost computation, plus a slightly different precompile list. This lesson is about how Reth lets you swap that in.

1. The trait surface

The relevant traits (names may drift slightly across reth versions; verify in source):

  • ConfigureEvm — given a block context, produce a configured revm instance (with the right precompile set, gas schedule, etc.)
  • BlockExecutionStrategy (or similar) — the loop that pulls txs from a block and feeds them to revm, accumulating receipts and state changes
  • ExecutorBuilder — the NodeBuilder slot that produces an executor for the running node

A chain customizes the first two via trait impls in its own crate, then registers them via the third in its NodeBuilder.

2. What Optimism overrides

Reading crates/optimism/evm/ will show you roughly:

OverrideWhy
Custom precompile listOP adds a few precompiles (e.g., for L1 block hash access)
Deposit transaction handlingDeposit txs skip signature verification (they're authenticated by L1)
L1 cost calculationEvery OP tx pays both L2 gas AND an L1 data cost (calldata posting)
Pre-execution hooksUpdate the L1 block oracle storage slot before the first tx in a block

The first one is config. The other three are execution-strategy-level — they live in the block executor's main loop.

3. The custom-precompile story

You wrote a custom precompile earlier. Now ask: where does that precompile get plugged into a chain?

Answer: ConfigureEvm impls hand revm a precompile set. A chain's ConfigureEvm impl extends the default set with its custom precompiles, gated by the chain's hardfork schedule.

So the wiring is:

ChainSpec  ──[which fork is active?]──▶  EVM config  ──[active precompile set]──▶  revm

The EVM config crate is where the precompile registration code physically lives.

4. The L1 cost computation (and why it's a great example)

OP Stack charges every transaction an L1 data cost — the amortized cost of posting the transaction's calldata to L1. This is a hard requirement: every node must compute the exact same L1 cost or block validation fails.

It's implemented inside the executor by:

  1. Before each tx, look up the current L1 base fee and blob gas price from a known storage slot
  2. Compute l1_cost = calldata_gas * l1_base_fee + blob_overhead
  3. Deduct from the sender's balance in addition to the L2 gas charge
  4. Credit it to the fee vault

This is a clean example of consensus-critical logic that you cannot put in a precompile — it has to be in the executor itself.

5. The execution loop, in pseudo-code

for tx in block.body:
    if is_deposit_tx(tx) and current_fork.allows_deposits():
        skip_signature_verify()
    else:
        verify_signature(tx)?

    db = state_provider.load_relevant_accounts(tx)
    cfg = configure_evm(chainspec, block, db)   // sets precompiles, gas schedule
    result = revm.transact(cfg, tx)
    apply_l1_cost(tx, result, db)               // L2-specific
    state.commit(result.state_changes)
    receipts.push(result.receipt)
return post_state_root(state), receipts

For Ethereum mainnet, lines 3 and 9 disappear. Everything else is identical. That's the whole point of the extension model.

6. For Tempo, what to expect

Tempo is an L1, so:

  • No "deposit tx" concept (no parent chain to be deposited from)
  • No L1 cost charge

But likely YES:

  • Custom precompiles for payment primitives (FX, settlement attestations, ...)
  • Pre-execution hooks if Tempo has a built-in "current FX rate" oracle slot, by analogy with OP's L1 block hash slot
  • A different fee market structure (Tempo is stablecoin-native; the fee-asset choice is interesting)

Tempo's executor is now public — find it in tempoxyz/tempo; the equivalent file is where you'd verify each of the above hypotheses against actual code.

7. Practice

In crates/optimism/evm/:

  1. Find the ConfigureEvm impl for OP
  2. List every precompile address that's NOT on Ethereum mainnet
  3. Find the function that adds the L1 cost charge
  4. Trace how a deposit transaction bypasses signature verification

Final check: name the two things that must be in the executor (not in a precompile, not in the mempool) for a Reth-based chain, and explain why each must live there. If you can't, re-read sections 4 and 5.

Summary (3 lines)

  • Custom Executor = override Reth's tx-processing. Trait impl + NodeBuilder wiring.
  • Common customisations: OP L1 fee, Hyperliquid CLOB precompiles, Tempo merchant attestation, Berachain PoL.
  • Composes with custom Pool + Consensus. Production: all Reth-based L1/L2s ship custom executors.