Differential case study ·

What a full-output reader sees in t8n.

When Jaune’s conformance work accounted for the complete transition-tool output surface, one rejected-transaction input run through three independent tools exposed two divergences in emitted block artifacts: one in transaction and receipt indexing, one in body encoding.

tool emission only no consensus impact no known fixture-corpus impact filed upstream
JauneLean 4
012

Matches the pinned conformance target deliberately; records each declared difference.

EELSreference t8n
012

Rejects input 1, but its emitted transaction trie still contains it.

Gethindependent client
01

Emits the two accepted transactions at contiguous block indices.

One three-transaction shape: accepted legacy transaction, rejected legacy transaction, accepted type-2 transaction. Jaune intentionally matches its declared target, with every exception registered; Geth and direct reconstruction classify the emitted artifacts here.

§ 1 The setup

One interface, three independently driven tools.

A t8n tool takes a pre-state, a block environment, and signed transactions, then emits the post-state and the artifacts needed to assemble a block. Jaune implements that interface over the same Lean definitions used by its fixture runner and proof stack. Its nine-case gate compares result, alloc, and body against goldens generated by a pinned EELS checkout, with every normalization and deviation explicit.

That gate asks whether Jaune reproduces its declared target. A separate acceptance run asks a different question: what happens when Jaune, the target, and an independent client receive identical signed inputs? On the eight comparable blockchain-mode cases, three were unanimous and five had registered divergences. Jaune agreed with its target in all five.

The root comparison below is part of that automated three-way run. The body is not one of the comparator’s result fields; it was decoded and compared separately on the same signed three-transaction input.

Discovery lane

Committed and pinned

Jaune df58966; EELS 9d6e6f83 on forks/amsterdam; Geth 1.15.6-stable-19d2b4c8.

Filing re-verification

Rechecked against upstream

Both findings were reproduced at EELS 5b2b22c7 before filing. The relevant code was unchanged at the branch tip on 12 August 2026.

§ 2 Finding one

A rejected transaction survives in one trie and leaves a hole in another.

EELS’s consensus transaction processor inserts a transaction into the shared transaction trie before admission checks. That is harmless in consensus execution: an invalid transaction invalidates the whole block, and no mutated output is used. The transition tool adds a non-consensus behavior around it—catch the admission exception and continue—without restoring that trie.

With input 1 rejected — its nonce is too high — the transaction trie contains original input keys {0, 1, 2}. Receipts exist only for the accepted transactions but retain their original keys {0, 2}. Geth instead emits both accepted transactions at contiguous keys {0, 1}.

EELS transaction trie
0 accepted1 rejected2 accepted
EELS receipt trie
0 accepted2 accepted
Geth block artifacts
0 ← input 01 ← input 2
Exact roots from the minimized middle-rejection case, on Amsterdam.
rootEELS and JauneGeth — accepted transactions, re-indexed {0, 1}
txRoot 0x33475b089f7e79c177de6104a67e662ad8959fe91e781cd2713a9f4de9be628a 0x0e9e7a492709d9dc5ab0cf24b1af81e0ba9f46b03bc48646858d827e7a75c1b1
receiptsRoot 0x98207864c436426d40ece7e546fd1dedf75d6d8627308705b0b9e9235e7641a2 0x59fe57186b64100200328863236de3773f63cad6ebd47226f7c34d8765ba34a3

Direct reconstruction reproduces all four values. No contiguously indexed accepted subset reproduces EELS’s pair, so the pair cannot describe a valid block. The post-state itself agrees.

§ 3 Finding two

The emitted body wraps encoded legacy transactions as byte strings.

EELS builds the body as rlp.encode([tx.rlp() for tx in txs]). A legacy transaction’s rlp() is already encoded, so the outer list treats those bytes as a byte string instead of placing the transaction’s RLP list inline. Typed EIP-2718 envelopes really are byte strings in a block body, which is why typed transactions happen to take the right shape.

Legacy memberas emitted — EELS / Jaune
b862 f860 800a… — 98 bytes, tagged as a byte string
Legacy membercanonical — Geth
f860 800a… — the same list, inline
Whole bodyemitted → canonical
0xf90130 three members, 307 bytes0xf8ca two members, 204 bytes

The first two rows carry the identical legacy transaction, beginning with its own f860 list prefix. EELS’s outer list tags those bytes as a string — b862, “the next 98 bytes are a byte string” — whereas canonical body encoding nests the list itself. The body is also assembled before execution, so it carries transactions the tool later reports as rejected; that is the second, independent reason the whole-body lengths in the third row differ.

The upstream regression case combines an accepted legacy transaction, a rejected legacy transaction, and an accepted type-2 transaction. The current body has three members and 307 bytes; the proposed output has the two accepted members, preserves the typed envelope, and is 204 bytes—byte-identical to Geth 1.15.6 on that case.

§ 4 Why the ordinary path did not expose them

Each affected surface sits behind a different blind spot.

The execution-spec-tests framework is not trusting these outputs naïvely. Its block-fixture path either replaces the value, constrains the input away from the failing shape, or does not consume the output. Those are sensible defenses. They also mean a general reader of the full transition-tool response exercises a surface the ordinary path does not.

Three affected outputs, three different protective blind spots.
surfacewhat the framework doeswhy this finding stays hidden
txRoot Excludes the tool’s transaction trie and recomputes the root from the input transaction list. The emitted trie is never used to build the fixture header.
receiptsRoot Requires an exception-producing transaction to be last in the block. A last-position rejection cannot leave a hole before a later accepted receipt.
body Requests the body file but does not load it into the fixture output. Neither legacy double-wrapping nor rejected-member inclusion affects generation.

§ 5 Scope and status

A transition-tool finding, with deliberately narrow claims.

No consensus impact

Consensus execution rejects the whole invalid block. The finding is in the tool’s reject-and-continue composition.

No known fixture-corpus impact

The examined path recomputes txRoot, requires an error transaction to be last, and does not consume body; no fixture-corpus impact is known.

Not a Jaune mismatch

Jaune reproduces its frozen target by design. The cases remain regression detectors for a future target re-pin.

Both filings were open when this page was prepared on 12 August 2026. This page does not treat an open filing as maintainer confirmation or claim that either change has landed.

§ 6 Reproduce it

The evidence is committed with the implementation.

Jaune’s ordinary gate needs only a built Jaune binary: it reruns all nine cases twice, checks determinism, verifies every golden digest, and compares result, alloc, and body with all normalizations and deviations explicit. The three-way script additionally needs the pinned EELS checkout and a Geth evm binary. It makes the root divergence observable but, deliberately, does not compare body.

$ lake build
$ scripts/check-t8n.sh --case reject-middle --red-test

$ JAUNE_T8N_TARGET=/path/to/execution-specs-t8n-amsterdam \
    python3 scripts/t8n-acceptance.py \
      --third /path/to/geth/evm \
      --third-name geth \
      --third-version 'evm version 1.15.6-stable-19d2b4c8'

The focused regression at PR #3361’s exact head reproduces the body correction separately: rejection at input 1, an inline legacy transaction, a typed byte envelope, and the exact accepted-only 204-byte body.

$ git clone https://github.com/ethereum/execution-specs.git
$ cd execution-specs
$ git fetch origin pull/3361/head
$ git checkout --detach 0b79966b037eb17f1a02f2f13e73de06bedf2342
$ uv sync --frozen --no-default-groups --group test
$ uv run --no-sync pytest -q tests/evm_tools/test_t8n_body.py
This is a bounded case study, not the promised generated-corpus campaign. It records one minimized exhibit completely: inputs, tools, pins, observed bytes, attribution, upstream filings, and impact boundaries. A broader campaign should add generated seeds, counts, and machine-readable results without retroactively enlarging this claim.

The broader lesson is simple: an interface may have many callers while parts of its output still have only one effective reader. Recomputing, constraining, or ignoring fields can protect a downstream pipeline, but it does not validate what the producer emits. Adding another full-output reader makes that otherwise quiet surface observable.