This document provides guidance for AI agents working with the derivation pipeline in the Optimism monorepo — the logic that reconstructs L2 state from L1 data. See go-dev.md for general Go build, test, and lint workflow and rust-dev.md for the Rust workflow.
The same derivation logic is implemented twice:
- Go (op-node): the reference consensus-layer node, under
op-node/rollup/. - Rust (kona): kona-node (
rust/kona/bin/node) is the Rust consensus-layer node; kona-client (rust/kona/bin/client) is the Rust fault proof program. Both drive the same derivation pipeline, implemented in the kona-derive crate (rust/kona/crates/protocol/derive).
The derivation pipeline lives primarily under op-node/rollup/ (Go) and
rust/kona/crates/protocol/derive (Rust). It reads deposits, batches, and channel data from
L1 and applies them to produce the L2 chain.
The pipeline is a series of stages, each pulling from the one below it. From L1 data to L2 blocks:
- L1 block traversal: for each L1 block, read the deposit transaction logs and the
SystemConfigupdate logs (from the receipts), and collect the batcher transactions addressed to the batch inbox — their payload is either calldata or blobs depending on the chain's data-availability mode. - Frame extraction: parse the batcher transactions into frames.
- Channel assembly: assemble frames into channels (a channel may span multiple frames and multiple L1 blocks).
- Channel decompression: decompress each complete channel into a stream of batches.
- Payload-attributes derivation: derive L2 payload attributes from the batches, prepending the deposit transactions and the L1-info/system-config deposit for the block.
- Consolidation vs. execution: if the existing unsafe chain already matches the derived
payload attributes (consolidation), the block is promoted to safe and derivation
progresses to the next batch / L1 block without re-executing. Otherwise the payload
attributes are sent to the execution layer to build and execute the block. In Go this
matching lives in
op-node/rollup/attributes/(AttributesMatchBlock).
Other recurring concerns:
- Safe head advancement: updating the safe L2 head as derivation progresses.
- Reorg handling: rewinding derivation state on L1 reorgs.
Both clients' derivation rules are configured by consensus parameters in rollup.Config (Go) /
RollupConfig (Rust), which can be loaded from the
superchain-registry. Adding a
field that comes from the registry means wiring it through every ingestion enumeration on
both clients — not just the config struct and the op-deployer/deploy-config
(DeployConfig.RollupConfig) path:
- op-node (Go): the TOML-decoded
superchain.HardforkConfig(op-core/superchain) and thesuperchain.ChainConfig→rollup.Configconversion inop-node/rollup/superchain.go(applyHardforks/rollupConfigFromRegistry). - kona (Rust):
HardForkConfig/ChainConfig::as_rollup_config(rust/kona/crates/protocol/genesis).
Two guards fail loudly on a field left unwired:
- Strict decoding rejects registry keys that no struct field models —
jsonutil.DecodeTOMLStrict(Go, used byop-core/superchain) and#[serde(deny_unknown_fields)](kona'sChainConfig/HardForkConfig). A registry bump that adds an unmodeled field fails to load until the struct consumes it. - A reflection completeness test (
TestRollupConfigFromRegistry_AllFieldsSet) asserts everyrollup.Configfield is populated from a fully-populatedChainConfig, catching a field that is modeled but never copied in the conversion.
- Deterministic derivation: the same L1 data always produces the same L2 chain. No randomness, no time-dependent behavior.
- Safe head bound: the safe head never advances past finalized L1 data. Safe head advancement must verify L1 finality status.
- Deposit ordering: all deposits are processed in their L1 inclusion order. Batch processing must preserve this ordering.
- Channel timeout: channel timeout is enforced to prevent memory exhaustion. Channel timeout values must not be modified without protocol review.
- Reorg unwinding: reorg handling must correctly unwind all derived state.
For post-Holocene derivation, transaction-list validation belongs to the batch stage, after a span
batch has been decomposed and its singular batches are streamed one at a time. In particular,
activation-gated transaction rules must use the streamed singular batch's timestamp. Do not inspect
transactions while decoding or constructing a SpanBatch, and keep post-Holocene whole-span
processing limited to prefix and extraction checks. A span may cross a fork boundary; only singular
batches emitted after the safe head are candidates for validation.
Add new per-block transaction rules to checkSingularBatch (op-node) and
SingleBatch::check_batch (kona), where singular batches from both wire formats converge. Rejecting
during DeriveSpanBatch can discard valid later elements before the batch stage has selected the
singular batches that actually apply. The legacy pre-Holocene batch queue has no singular-streaming
batch stage, so it still performs its historical full-span transaction checks. Keep those legacy
checks aligned between op-node and Kona.
Both clients decode the same batcher-controlled bytes, so their decoders must accept exactly the same byte set. A byte string that one client decodes and the other rejects splits derivation on identical L1 data.
- The spec is the reference; op-node is the incumbent. Decide what is correct from the specs. op-node additionally defines what OP Mainnet currently does, so where the spec is silent or ambiguous its behavior is the tie-breaker — but a decoder that contradicts the spec, or looks outright buggy, is a finding to raise rather than something to mirror into kona. Either way, take a spec/op-node disagreement to the user before encoding a choice in either client.
- Verify a codec's accept-set, don't infer it from the format name. Wire formats come in
families that differ on exactly the inputs a batcher controls. Span-batch
uvarintis a protobuf Base128 varint, whose non-minimal encodings are valid; theunsigned-varintcrate implements the minimal-only multiformats variant instead and rejects them. Span-batch fields go throughread_uvarint(rust/kona/crates/protocol/protocol/src/batch/varint.rs), a port of Go'sbinary.ReadUvarint. Before trusting any decoder on this path, diff its accept-set against op-node's over a generated corpus — a spec citation does not distinguish two families. - Pin every decision in both suites. When changing either decoder, add the same byte vector to the kona and op-node tests so the pair stays locked together.
- Unit tests for every pipeline stage.
- Reorg simulation tests for any change to reorg handling.
- End-to-end derivation tests with synthetic L1 data.
- Benchmark tests for batch-processing throughput.