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.
EthExecutorruns revm. Used by default. - Custom impl. Override
execute_blockto 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 changesExecutorBuilder— 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:
| Override | Why |
|---|---|
| Custom precompile list | OP adds a few precompiles (e.g., for L1 block hash access) |
| Deposit transaction handling | Deposit txs skip signature verification (they're authenticated by L1) |
| L1 cost calculation | Every OP tx pays both L2 gas AND an L1 data cost (calldata posting) |
| Pre-execution hooks | Update 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:
- Before each tx, look up the current L1 base fee and blob gas price from a known storage slot
- Compute
l1_cost = calldata_gas * l1_base_fee + blob_overhead - Deduct from the sender's balance in addition to the L2 gas charge
- 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/:
- Find the
ConfigureEvmimpl for OP - List every precompile address that's NOT on Ethereum mainnet
- Find the function that adds the L1 cost charge
- 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.