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 gate — nine cases when this study was written, thirty-nine now — 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 target has since been re-pinned twice, to 8ca2f4c and 7341820 (3 and 4 September 2026); this case’s goldens were regenerated at each and came out byte-identical, so the pinned target still emits both behaviors.
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, and both were still open on 25 September 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.
The exhibit is reproduced at the commits it was found on. Jaune df58966 carries the case, its goldens from EELS 9d6e6f83, and the three-way script. Its ordinary gate needs only a built Jaune binary: it reruns every case twice (nine at that commit), checks determinism, verifies every golden digest, and compares result, alloc, and body with all normalizations and deviations explicit. The three-way script additionally needs an EELS checkout at the same pin and a Geth evm binary. It makes the root divergence observable but, deliberately, does not compare body.
$ git clone https://github.com/skbaek/jaune.git $ cd jaune $ git checkout --detach df58966f01786adfdc8f6315d53580468225e9eb $ lake exe cache get && lake build $ scripts/check-t8n.sh --case reject-middle --red-test $ git clone https://github.com/ethereum/execution-specs.git ~/execution-specs-t8n-amsterdam $ git -C ~/execution-specs-t8n-amsterdam checkout --detach 9d6e6f8352a0f76e7e8803722d1a2798fa4f0a96 $ (cd ~/execution-specs-t8n-amsterdam && uv sync --no-default-groups --group test) $ JAUNE_T8N_TARGET=~/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'
Current Jaune still carries the case and its goldens unchanged, but its corpus has grown to thirty-nine cases, thirty of them Amsterdam’s, while the three-way divergence registry was recorded against the nine-case corpus; the three-way run is therefore reproduced at df58966 rather than at the tip. The body correction reproduces separately in the focused regression of PR #3361, pinned below to the head the pull request carried on 25 September 2026: rejection at input 1, an inline legacy transaction, a typed byte envelope, and exactly the accepted-only body — 204 bytes on this input. The pull request’s branch was rewritten on 22 August 2026, and the test moved under packages/testing; the head this page first cited, 0b79966, is no longer in the branch’s history, so fetching the pull request no longer brings it into a fresh clone.
$ git clone https://github.com/ethereum/execution-specs.git $ cd execution-specs $ git fetch origin pull/3361/head $ git checkout --detach 6d963d31e539fd82284ea52a10d3b331d6186b7a $ uv sync --frozen --no-default-groups --group test $ uv run --no-sync pytest -q \ packages/testing/src/execution_testing/evm_tools/tests/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, at its 25 September 2026 head
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.