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.
Matches the pinned conformance target deliberately; records each declared difference.
Rejects input 1, but its emitted transaction trie still contains it.
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.
Committed and pinned
Jaune df58966; EELS 9d6e6f83 on forks/amsterdam; Geth 1.15.6-stable-19d2b4c8.
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}.
| root | EELS and Jaune | Geth — 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.
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.
§ 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.
| surface | what the framework does | why 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.
Consensus execution rejects the whole invalid block. The finding is in the tool’s reject-and-continue composition.
The examined path recomputes txRoot, requires an error transaction to be last, and does not consume body; no fixture-corpus impact is known.
Jaune reproduces its frozen target by design. The cases remain regression detectors for a future target re-pin.
Rejected transactions produce inconsistent transaction and receipt roots
Minimized repro, direct trie reconstruction, Amsterdam block-access evidence, and two candidate designs for maintainer direction.
Upstream pull request · #3361 · filed 11 August 2026Encode accepted transactions in the t8n body
A focused implementation plus a legacy / rejected / type-2 regression test. The trie-indexing behavior is intentionally out of scope.
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
- Completion report and three-way ledger
- Committed middle-rejection case
- Pinned pre-fix 307-byte body
- Machine-readable divergence registry
- Golden provenance and digests
- Standalone issue repro archive
SHA-256 c47fdc1374f8a41a26edc0ac34b7227071c197877fb6d64a6634ac62712cb9a7 - Fixture path: exception-last, exclude, recompute
- Fixture output loader does not consume body
- EELS transaction-processing path
- EELS t8n reject-and-continue path
- PR #3361 body regression
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.