An executable formal specification of the EVM · Lean 4
A Yellow Paper you can run.1
The Yellow Paper defined Ethereum in mathematics you could only read. Its living replacement is a program you can only run. Jaune is both at once: forty thousand lines of Lean 4 that state the execution layer precisely enough to prove theorems about — and compile into an interpreter that passes the pinned mainnet fixture corpus, fork by fork, case by case, and speaks the ecosystem’s standard t8n transition-tool interface.
Fig. 1 — historical mainnet gate output for tests@v20.0.1, verbatim. 5,100 fixture files, 34,005 cases, spanning Prague, Osaka, BPO1, BPO2, and the transitions between them — with zero expected failures on that run. The current release and counts are in Table 1.3
Abstract. Jaune restates the Ethereum execution layer as a single Lean 4 artifact that is simultaneously a specification, an interpreter, and a proof subject. It mirrors execution-specs at a pinned commit and implements Prague, Osaka, BPO1, and BPO2 as first-class rule sets selected from block context, passing 5,006 of 5,006 supported files (34,205 cases) of the current mainnet corpus; Amsterdam is a fifth rule set, tracked at a pinned Glamsterdam devnet snapshot with its own fixture lane. All eighteen precompiles — ecrecover to BLS12-381 to P256VERIFY — are pure Lean, with no FFI and nothing axiomatized; SHA-256 moreover carries a machine-checked equivalence to a transcription of its FIPS 180-4 standard. The interpreter is total: the fuel that drives it is proven sufficient from the frame’s own gas, with the tight bound, at the definition site. Its semantic errors are typed rather than stringly, its checked entry points bind an executable snapshot to canonical validated state, and a shrink-only CI budget holds the library at zero panics. The interpreter also answers as a standard t8n transition tool, gated against its pinned target’s complete output with every normalization and declared deviation explicit. Its canonical execution-derivation type, adequacy theorem, and symbolic instruction rules ship in the library itself, with checked examples, a task-organized API guide, and exact axiom pins that the ordinary build enforces. The semantics is demonstrated strong enough for real verification: the sibling Blanc language has carried contract invariants through it — a WETH’s solvency across scheduled fork activations, Registry integrity for a port of Lido’s deployed CircuitBreaker from an official-parameters deployment to every reachable state of a valid configured chain, and the eth2 deposit contract’s Merkle root through twelve SHA-256 precompile crossings to every admitted future — through a machine-checked compiler, with an axiom audit in CI. MIT-licensed.
§ 1 The gap this closes
One artifact instead of two.
Formal models of the EVM are usually written for the prover — small, idealised, and quietly different from anything that runs. The distance between the model and the implementation is where the bugs live, and nothing in the proofs covers it. Jaune removes the distance by refusing to have two artifacts.
The spec is the executable
The Lean definitions that theorems quantify over — exec, stateTransitionUsing, addBlockToChainUsing — are the definitions Lean compiles into the binary that runs the fixture corpus. There is no reference model to drift out of sync, because each rule of the protocol is written down exactly once. The one place the binary runs something else is declared: a calldata-sharing execFueledCached replaces execFueled in compiled code, through an @[csimp] theorem proving the two equal; the sharing applies on the pre-Amsterdam forks.
All the way down to the curves
Every precompile is Lean source: ecrecover, MODEXP, BN254 pairing, the full EIP-2537 BLS12-381 set, KZG point evaluation, and Osaka’s secp256r1 P256VERIFY. There is not one @[extern] in either repository — no linked C, no “assume this crypto is correct” frontier under the theorems.
Trust is checked, not asserted
A no-toolchain CI gate fails the build on any stray sorry — and a trust-surface gate holds the whole library to zero un-allowlisted axiom, opaque, @[extern], @[implemented_by], @[csimp], partial, unsafe, and native_decide occurrences: ten patterns, one hit, one allowlist row — the proved @[csimp] equation of §1.1, justified in writing. The canonical proof surface has its exact axiom sets pinned — 53 rows, each propext, Classical.choice, Quot.sound and nothing else — computed by a from-scratch walker rather than #print axioms, and the ordinary build fails on any mismatch.
A specification substrate has to be sound before it is fast
An internal review found seven ways this artifact could still have been a good interpreter and a bad specification: a chain that paired a tip with an unrelated state, a hand-built transaction indistinguishable from a wire-decoded one, an unimplemented era silently answered with another fork’s rules, error strings doing the work of a datatype. All seven are closed, and the gate that keeps them closed is the interesting part.
-- The VM's whole error carrier. An internal defect cannot be -- mistaken for an expected halt, because it is a different -- constructor rather than a different string. inductive EvmError : Type | halt (reason : ExceptionalHalt) | revert | crypto (reason : CryptoError) | internal (reason : InternalError) -- What a settled frame may store. `crypto` and `internal` -- are unrepresentable here by construction. inductive SettledHalt : Type | halt (reason : ExceptionalHalt) | revert -- Checked wire ingress: a private constructor reachable only -- through the strict decoder, carrying its own round-trip -- evidence, so a hand-built Block cannot be certified. def CanonicalBlock.ofRlp? (raw : Bytes) : Option CanonicalBlock
Fig. 2 — sixteen typed error ADTs replaced the strings that used to discriminate semantic outcomes. String survives only at the external parsing, rendering, and fixture-compatibility boundary — 58 exact-match allowlist rows across the whole gate, with the pending-defect budget held at zero and shrink-only.
scripts/check-integrity.sh inventories every panic, raw bang operation, and stringly-typed semantic carrier in Jaune.lean’s import closure — computed transitively from the module graph, never hardcoded — and demands an exact allowlist row for each. A new occurrence fails the gate. Known defects carry an owning step and count against a declared budget that may only decrease, so a step cannot discharge one and quietly introduce another. The library-wide panic count is 0; the pending budget is 0.
CheckedBlockChain binds an executable snapshot to canonical state, hash-linked retained history sufficient for BLOCKHASH, and tip-state-root agreement; ConfiguredChain adds a validated activation schedule and chain-ID agreement, once. Repeated execution reuses those witnesses instead of recomputing a trie root per call — the checked path measured 2.3% faster than the unchecked one it replaced, so the guarantees are not paid for at runtime.
Lineage, stated plainly: the Yellow Paper has not kept pace with the protocol, and Ethereum’s canonical specification today is execution-specs — executable Python, current, but not a thing you can state a theorem in. Jaune mirrors it at a pinned commit, tests against its fixtures, and adds the property neither ancestor has: the text of the specification is itself a mathematical object inside a proof assistant.
§ 2 Coverage
The protocol, present tense.
A specification earns the name only while it describes the protocol that actually exists. Jaune is fork-parameterised: Prague, Osaka, BPO1, BPO2, and Amsterdam are values of one ForkRules record, and a single interpreter reads the record and nothing else. A chain’s configuration schedules activations; each block’s timestamp selects its rules. Every machine carries its fork, so rule validity is proved once, for all five, rather than assumed at each use.
inductive Fork : Type | prague | osaka | bpo1 | bpo2 | amsterdam -- BPO1 is Osaka with its blob schedule replaced — enforced by -- construction, so no BPO fork can silently acquire an -- execution rule of its own. def bpo1Rules : ForkRules := { osakaRules with fork := .bpo1, blob := bpo1BlobSchedule } -- The single place a fork identity becomes rule data. Total: -- a Fork is the selection of one of five rule records, never -- an identity that may fail to resolve or fall back to -- another fork's rules. def Fork.ruleSet : Fork → ForkRules | .prague => pragueRules | .osaka => osakaRules | .bpo1 => bpo1Rules | .bpo2 => bpo2Rules | .amsterdam => amsterdamRules theorem Fork.ruleSet_valid (f : Fork) : f.ruleSet.Valid theorem Fork.supported_eq_all : Fork.supported = Fork.all
Fig. 3 — forks as data. ForkRules centralises the blob schedule, code, transaction and block limits, MODEXP pricing, fork-gated opcodes, the shared-formula gas schedule, Amsterdam’s optional state-gas dimension, the fork-dependent header fields, the request-producing system contracts, the optional block-level access list, and the precompile activation set; ChainConfig maps timestamps to forks and rejects ambiguous schedules. scripts/check-fork-constants.sh compares every number in every record against the pinned execution-specs revision.
Every executable EIP in the Prague→Osaka delta of execution-specs is implemented and exercised by a strict all-PASS gate — 2,467 of 2,467 files, 17,423 cases:
Amsterdam has no mainnet activation at the pinned upstream revision, so Jaune tracks it at the tests-glamsterdam-devnet@v8.1.4 release and the execution-specs commit it was filled from, 7341820. amsterdamRules is a complete record, written as an update of BPO2’s so every field it does not move is inherited by construction:
The two-reservoir metering is proved total like the rest: the fuel bound is restated over a gas measure that counts spilled state gas and equals gasLeft on every earlier fork. The EIP-8024 immediates are proved to leave the jump-destination set unchanged. What this is not: final Amsterdam. The devnet pin moves when upstream does, and nothing a Prague-through-BPO2 fixture observes changed to admit it.
Fixtures like OsakaToBPO1AtTime15k run through the configured block-import API: each block’s timestamp chooses its rules mid-stream. The mainnet transitions suite — Prague→Osaka, Osaka→BPO1, BPO1→BPO2 — is a strict all-PASS gate. The devnet’s BPO2ToAmsterdamAtTime15k suite passes 38 of its 42 files; on the other four Jaune agrees the block is invalid but names a different exception than the fixture, and those are recorded as identity findings rather than aliased away. Selectors that would match zero fixtures are refused, not reported as vacuously green.
§ 3 Conformance
Tested like a client.
Three fixture lanes. The canonical lane is the execution-specs tests@v20.0.2 release under strict generated manifests: activating a fork means its whole suite, exclusions are machine-generated with per-fork reasons, and unknown labels fail loudly. A separate devnet lane runs Amsterdam against its pinned prerelease corpus under the same rules, sharing nothing with the mainnet lane’s pin, manifest, or install. The frozen ethereum/tests lane is retained as an independently-filled regression instrument with committed per-file baselines. Its five non-passing files are diagnosed, and none is a defect in Jaune.
| suite | files | cases | result | wall | --jobs auto |
|---|---|---|---|---|---|
| --suite smoke | 16 | 155 | all pass | 0.5 s | 0.5 s |
| --suite prague | 2,526 | 16,673 | all pass | ~12 min | 341.7 s |
| --suite osaka | 2,467 | 17,423 | all pass | ~8 min | 231.0 s |
| --suite transitions | 13 | 109 | all pass | 16 s | 18.0 s |
| --suite full | 5,006 | 34,205 | all pass | ~18.9 min | 618.9 s |
Table 1 — the current-mainnet lane (scripts/check-mainnet.sh), blockchain_tests from tests@v20.0.2, verified by SHA-256 at bootstrap. This lane has no expected-failure baseline: every manifested case at a supported fork passed in the full-lane run of 2026-09-21.3 The timing columns were measured on the tests@v20.0.1 corpus, not the updated one: the --jobs auto column on 2026-09-21, after the Lean 4.34 migration; the sequential column earlier. Both harnesses take --jobs auto. Uncontended per-file timings require sequential runs.
| suite — --lane amsterdam | files | cases | result |
|---|---|---|---|
| --suite amsterdam-smoke | 16 | 60 | all pass |
| --suite amsterdam | 3,159 | 24,901 | all pass |
| --suite amsterdam-transitions | 42 | 127 | 38 pass · 4 recorded identity findings |
| --suite amsterdam-full | 3,201 | 25,028 | all pass but the same 4 |
Table 2 — the Glamsterdam devnet lane (scripts/check-mainnet.sh --lane amsterdam), over tests-glamsterdam-devnet@v8.1.4, with zero exclusions inside the Amsterdam trees. The four findings are the invalid pre- and post-fork blocks of the SLOTNUM and block-level-access-list transition modules: their headers carry 22 RLP fields, which Jaune rejects as a structural encoding error where the fixtures name a block-format, access-list-hash, or block-hash exception — same verdict, a different name for it. The union ran in 463.6 s at --jobs auto on 2026-09-21, red on exactly those four files.
| tier — check-legacy.sh | files | committed baseline | what it covers |
|---|---|---|---|
| --depth | 67 | all pass | recursion & call-depth stress |
| --smoke | 174 | 173 pass · 1 expected fail | curated cross-section — the routine gate |
| --bls | 29 | all pass | EIP-2537 BLS12-381 + KZG point evaluation |
| --patch | 10 | all pass | fixed historical failures — un-rebaseable |
| --rlp4 | 4 | all pass | invalid-RLP / header targets — un-rebaseable |
| --full | 2,983 | 2,978 pass · 5 diagnosed non-passes (none a Jaune defect) | the frozen BlockchainTests corpus (~7.7 min at --jobs auto) |
Table 3 — the frozen legacy lane (scripts/check-legacy.sh). Each gate passes iff every file’s PASS/FAIL matches its committed baseline — a regression gate, not an all-green banner. None of the five baselined FAILs is a defect in Jaune: two fixtures carry no case in the supported fork range, and the other three are multiply-invalid cases whose expected-exception alternatives reflect another client’s check ordering and omit the identity Jaune reports — the same one the frozen oracle reports. Within this corpus the GeneralStateTests/ subtree passes 2,633 of 2,634.
Beneath the fixture lanes sit three oracles. Two are differential against a frozen execution-specs Python interpreter — 21,593 word-arithmetic and hash cases, and 240 blob-fee taylor_exponential cases. The third checks the elliptic-curve layer on 573 pinned, differential, and algebraic-identity cases, with no skip and no unknown outcome. Alongside them, 53 precompile vector files — 1,990 cases, including 782 for P256VERIFY — run under a strict manifest that fails on a missing or unexpected file, and on a file that runs fewer cases than it declares. A vector file that quietly lost half its cases would otherwise pass while testing half as much. And since 2026-08 the interpreter also answers as a standard t8n transition tool: scripts/check-t8n.sh compares its complete output on 39 cases — 30 of them Amsterdam’s, block-level access list included — against goldens from the pinned execution-specs, with every normalization and deviation explicit and each case run twice for determinism. A three-way acceptance run against go-ethereum in 2026-08 found that in every registered divergence, Jaune matches its pinned conformance target. Read the case study: what a full-output reader sees in t8n.
Jaune is not proved equivalent to the Yellow Paper or to execution-specs. That correspondence is established the way every client establishes it — by differential testing against pinned corpora — and published as per-file classifications rather than asserted as a theorem. The formal guarantee is relative to Jaune’s semantics; the empirical guarantee is the corpus. Amsterdam’s evidence is a devnet corpus, so its support is a snapshot of a moving target, not final Amsterdam. Confusing any of these is how verification gets oversold, and this project declines to.
§ 4 Totality
A total interpreter, proven so.
Executable semantics in a proof assistant usually carry a fuel parameter, and “out of fuel” leaks into every downstream statement as a side condition. Jaune closed that door: the EVM’s own gas bounds its recursion, and the bound is a theorem, not a convention.
-- Fuel strictly above the gas measure can never exhaust. -- The measure is gasLeft plus any spilled Amsterdam state -- gas; on every earlier fork it is exactly gasLeft. theorem execFueled_ne_exhausted (evm : Evm) (fuel : Nat) (h : evm.dyna.gasMeasure < fuel) : execFueled evm fuel ≠ Fueled.exhausted -- The additive constant is 1 — the tight bound. def sufficientFuel (gas : Nat) : Nat := gas + 1 -- The total interpreter. Fuel is an implementation detail, -- discharged by proof at the definition site. def exec (evm : Evm) : Except (EvmError × Devm) Devm
Fig. 4 — the sufficiency result, closed 2026-07 and restated in 2026-09 over the two-reservoir gas measure Amsterdam’s metering needs. A gas-decrease corpus over every instruction constructor (82 when the arc closed; Amsterdam’s four new opcodes joined it) feeds a settlement-and-monotonicity argument; exec and the frame-level API (runFrame, executeCode, processMessage) are total, and the "RecursionLimit" observable is deleted from the semantics.
Adequacy between the relational semantics proofs use and the interpreter that runs becomes a clean equivalence — no fuel threshold, no ∃ fuel, ∀ fuel′ > fuel, … plumbing in any statement. The relational type and this equivalence now live in Jaune itself (Jaune/Exec.lean), so a proof author needs no second package:
lemma exec_iff_exec_eq (pc : Nat) (sevm : Sevm) (devm : Devm) (exn : Execution) : Nonempty (Exec pc sevm devm exn) ↔ exec ⟨pc, sevm, devm⟩ = exn
And the seeding is provably an implementation detail: execFueled_run_mono shows more fuel never changes a reached result, so no semantic choice is hiding in the constant.
Proof-engineering telemetry: the combinator layer discharged 43 of 69 per-instruction obligations in one dispatch; the arc deleted Blanc’s entire fuel-threshold machinery, −113 Fueled mentions across its proof files, with the then-protected four theorem statements byte-identical before and after.
§ 5 Performance
Fast enough to check.
A specification nobody can afford to run is a specification nobody checks. Two measured optimization arcs — elliptic curves, then keccak — took the four-family benchmark aggregate from 105.81 s to 11.5 s on identical instruments and hardware, a 9.2× speedup, without changing a single baseline classification, gas rule, or public signature. A third then took the single slowest fixture in the entire corpus from 12½ minutes to 77 seconds.
Fig. 5 — same four fixture families, same machine, summed per-file seconds, each bar from its arc’s committed report; the last is the best of three runs. The v4.32.1 toolchain migration, the totality arc, and the semantic-integrity arc each re-measured and landed at parity or better — the BLS tier 20.6% faster after migration, the checked-entry-point path 2.3% faster than the code it replaced. The last bar is the most recent committed run of this instrument; it predates the Lean 4.34 migration and Amsterdam’s metering.
BLAKE2b: 9.72× on the corpus’s worst case
Blake2.g ended in four Array.set! calls. Array α stores boxed elements, so a BLAKE2b round — eight mixes — cost 32 heap allocations and 32 frees, in a loop that does no other memory work. Keccak had solved exactly this one file away, holding its 25 lanes as unboxed scalar fields. Applying it to BLAKE2b took CALLBlake2f_MaxRounds from 753.24 s to 77.45 s — 175.4 to 18.0 ns per round over 4.29 billion rounds — with byte-identical runner output before and after.
The flat round is not trusted, it is proved: two theorems relate it to the retained reference round, with axioms exactly propext and Quot.sound. The old definitions stay in the file — not as dead code, but as the specification the equivalence mentions. Keccak’s flat permutation carries the same standing: f1600_eq proves it equal to its retained reference transcription.
The legacy corpus is latency-bound: its wall time is set by the single longest indivisible fixture, not by the core count. BLAKE2F held that position at 711 s — 41% of the serial total — so no amount of parallelism moved the gate. Unboxing the round handed the bound to loopMul and dropped the whole corpus from ~15 min to 8 min at --jobs auto.
The generalisable part is the diagnostic, not the remedy. The vector suite was latency-bound too, and its dominant file was a batch of 106 independent cases — so it was sharded eight ways, with a checker proving the shards are an exact partition of the source. CALLBlake2f_MaxRounds was one contiguous computation with no independent units inside it. Sharding had nothing to offer it; only making it faster did.
| microbenchmark | before | after | factor |
|---|---|---|---|
| keccak, 64-byte input | 53,721 ns | 1,359 ns | 39.5× |
| secp256k1 signature recovery | 49,188,387 ns | 2,140,882 ns | 22.9× |
| BLS12-381 G2 scalar multiplication | 217,632,365 ns | 13,028,177 ns | 16.7× |
| secp256k1 scalar multiplication | 16,389,080 ns | 1,580,029 ns | 10.4× |
| BLAKE2F, 12 rounds | 5,065 ns | 3,119 ns | 1.62× |
Table 4 — committed benchmark instruments, five-run medians. All of it pure Lean before and after: the speedups came from representation and algorithm, never from calling out to C. The last row is reported at its honest magnitude: twelve rounds behind a fixed per-call cost is setup-dominated, so it dilutes a ~10× round loop to 1.62× and is a corroborating signal rather than the headline. The fixture-level measurement is.
§ 6 Case study — Blanc & the Lido CircuitBreaker
What the semantics is sufficient for: a deployed guardian’s registry, coherent at every reachable future.
The claim that a semantics “supports verification” is cheap until someone pays it. Blanc (fr. “white”, as in whitepaper) is the payment: a deliberately minimal contract language — four constructors, a compiler into EVM bytecode, a machine-checked compiler-correctness theorem — that carries real invariants through Jaune’s semantics at every altitude. Its deepest single exercise of the semantics is a port of the CircuitBreaker that Lido deployed in 2026: an emergency brake whose admin grants pauser addresses time-limited authority over registered protocol contracts. The invariant is Registry integrity: the guardian’s book of authorizations stays coherent, whatever any contract it talks to does.
From any checkpoint whose state has the CircuitBreaker’s exact compiled runtime installed and Registry-coherent storage, every state reachable along a valid configured chain whose schedule selects only covered forks still does — with no premise about the bytecode at any other address, no non-reentrancy condition, and no honesty assumed of the contracts it pauses.
theorem chainUsing_preserves_registryStable (dp : DeployParams) (ca : Adr) (cfg : ChainConfig) (checkpoint future : BlockChain) (reach : BlockChain.ReachUsing cfg checkpoint future) (stable : RegistryStable dp ca checkpoint.state) (hcov : ∀ t f, cfg.forkAt t = .ok f → CoveredFork f) : RegistryStable dp ca future.state
Read the quantifiers — ReachUsing is the same configured-chain reachability the WETH solvency family climbed first: whole valid blocks, every transaction anyone sent, arbitrary contracts doing arbitrary things, under the model’s explicit per-step bound on total ether plus withdrawals. RegistryStable packages exactly two facts — the exact compiled runtime is installed, and the contract’s actual storage admits a coherent Registry witness. CoveredFork is exactly Prague, Osaka, BPO1, and BPO2: Blanc now builds on a Jaune that implements Amsterdam, and states its results for the four forks its proofs cover rather than for Amsterdam blocks it has not yet reasoned about. What the theorem does not ask for is the point: a pause hands control to an arbitrary callee mid-transaction, and no hypothesis constrains it. The checkpoint is not assumed either: a deployment theorem walks the official constructor input through Jaune’s real creation-message, transaction, and Prague-block pipeline to establish it.
Each layer discharges the assumption the one above it would have to make
The checkpoint, derived rather than assumed. From the exact official constructor input, one successful strict singleton type-2 Prague block through Jaune’s configured transition establishes the deployment root — receipt success, the exact installed runtime, an empty coherent Registry.
Every reachable state. Induction over configured reachability — no block sequence of any length under a valid schedule of covered forks breaks coherence.
One whole block, through Jaune’s real transition. Every transaction, plus withdrawals, requests, and fee settlement — with the explicit per-step bound carried rather than hidden.
One message call, at arbitrary depth. Arbitrary callee bytecode, same-instance re-entry included — coherence crosses the CALL and STATICCALL boundaries with no premise about what runs on the other side.
Source to bytecode, backward simulation. Every behaviour of the compiled byte string is a behaviour of the Blanc program — so a property proved about source is a fact about what the EVM executes.
Jaune’s executable semantics. The definitions every rung above is stated over — the same ones the fixture runner of §3 compiles. Blanc pins Jaune at c326e3f, whose Lean sources are the ones this page describes.
WETH was the first payment on this claim — 244 lines of Blanc compiling to 988 proved bytes, its solvency (the contract always holds enough ether to honour every balance) carried to the same configured-chain altitude, across scheduled fork activations. The frontier then moved through a full 27-selector WETH10, whose future-redeemability flagship runs the Prague pipeline from a proved creation transaction onward, to the CircuitBreaker — and on: the eth2 deposit contract, whose proofs cross Jaune’s SHA-256 precompile at twelve source-shaped sites and read the Merkle root off every admitted future of a derived deployment; Lido’s withdrawals gateway and upgradeable proxy, carrying the first delegatecall and two-contract composition theorems; a pro-rata pricing étude built on Jaune’s new integer-arithmetic layer — and on since, as the Blanc page records.
Scope, unchanged since the first port: these are reimplementations, never verification of the code at the reference addresses. Each port’s observable differences are catalogued in its own deviation registry, and every theorem is about the bytecode Blanc’s compiler emits — transferring one to a deployed original would need a separate argument that nobody here has made.
Blanc’s CI audits 1,372 named theorems — 290 of them the CircuitBreaker family’s, the history rungs among them — each against its own pinned expected axiom set, failing on an extra or missing axiom and on anything beyond propext, Classical.choice, Quot.sound, each closure computed by Jaune’s from-scratch axiom walker. The history family’s own gate probes all 104 of its public theorems and admits no exception table, and the compile witness lidoCircuitBreakerCode_compile pins the exact runtime bytes the family is about. No sorry, no native_decide, nowhere in the trusted path.
Blanc pins Jaune by immutable commit in its lakefile, so the pair builds reproducibly from two clones — and “which semantics was this proved against” always has a one-line answer. Since 2026-09-24 Blanc builds on Jaune’s canonical execution layer instead of its own copy of it, and its build runs Jaune’s own axiom pins as well.
The division of labour is exact: Jaune owns what the machine does; Blanc owns programs, the compiler, and everything proved about particular contracts. The full portfolio — ports from the WETH to the eth2 deposit contract and Lido’s CircuitBreaker, gateway, and proxy, plus arithmetic études, each with an evidence page of its own — lives at skbaek.github.io/blanc.
§ 7 Method
Pre-registered gates, and the discipline to lose.
Every number on this page comes from a committed report, baseline, or benchmark instrument. Optimization arcs declare their measurement method and GO/NO-GO threshold before the experiment, and a failed gate cancels the work no matter how much was already invested. Below is the part most projects never publish: the ledger of ideas that lost.
-
Fixed-base precomputation & joint-wNAF for ecrecover gate: ≥15% inclusive in 3 families — measured 8.78–10.42%; deleting recovery entirely capped the win at 1.106×no-go
-
Projective-coordinate Miller loop for BN254 & BLS12-381 pairings gate: ≥20% inclusive in affine double/add — measured 4.14% and 3.42%no-go
-
Byte-layer reroute of the keccak input path gate: ≥15% marshalling residual in ≥2 families — measured 5.1%, 5.5%, 5.5%no-go
-
Flat U256 representation, first attempt archived: word ops measured 0.0–1.1% of the profile at the time; reopens only under a new, separately measured rationalearchived
The gates cut the other way too, and the same rule applies: the BLAKE2b arc predeclared GO at ≥2.0×, HALT below — with the recorded response to a low number being “report and stop,” not “reach for a faster form until the number looks better.” It measured 9.72× and shipped. Both halves of that arc were checked with deliberately broken negative controls, run and reverted without ever entering a commit: one wrong word index in the flat round, to confirm the equivalence proof actually fails; one flipped PASS in a baseline, to confirm the timing-refresh mode writes nothing and exits non-zero. A gate nobody has watched fail is not yet evidence.
Regression gates, not green theatre
Committed baselines record the expected PASS/FAIL of every file; a gate passes only on an exact match. Two target tiers are explicitly un-rebaseable, and rebasing any other requires stated scope and justification in the plan — a gate is never weakened to land a change. When an optimization made a baseline’s timings stale, the fix was not to reach for --rebase: that flag was split into two verbs, so “the code got faster” can no longer be spelled the same way as “accept a changed classification.” The safe verb re-derives the classifications from the bytes it is about to write and refuses if any moved.
Every external input is pinned
Fixture corpora never enter the repository: bootstrap scripts provision them against a machine-readable manifest — Git commits for the legacy suites, SHA-256 for release archives, a frozen Python interpreter for the differential oracle — and a read-only environment doctor reports drift without touching anything.
The trusted path is checked mechanically
A no-toolchain CI job fails the build on any sorry or stray dbg_trace outside a justified allowlist; the integrity gate — run locally, like the corpus suites — holds panics, raw bang operations, and stringly-typed semantic carriers to a shrink-only budget over the import closure; the ordinary build fails on any drift in the 53 pinned axiom sets; Blanc’s job audits 1,372 theorems against exact pinned axiom sets. Per push, two conformance gates that need nothing but the binary run beside them — t8n against its pinned goldens, and the word and hash primitives — in about a second, and a separate job installs Jaune into a fresh package through an exact Git dependency and verifies what it installed. The gates extend to the tooling itself: an elaboration-budget gate holds the compile time of every Lean file the default build elaborates against a host-local baseline, a memory probe bounds a 1,024-deep Prague call path carrying a megabyte of calldata, and a CLI gate pins the runner’s refusal behaviours, so green never means a filter silently selected nothing. Generated constants and vectors come from named generators, never hand transcription.
§ 8 Trajectory
Recently landed, currently open.
The multi-fork architecture
Fork-parameterised rules, the complete Osaka execution delta, BPO1/BPO2 as schedule data, configured transitions, and the strict-manifest current-mainnet lane — 5,006/5,006 files. The generic transition proof landed in the fork arc; configured reachability and raw-import corollaries are now restored and protected by Blanc’s exact-axiom audit.
Toolchain migration & the totality arc
Lean and mathlib to v4.32.1 with every classification unchanged; then the sufficiency proof — a total interpreter with the tight fuel bound, RecursionLimit deleted, and Blanc’s adequacy bridge restated as an equivalence.2 Then the BLAKE2b unboxing, which moved the corpus’s latency bound off a precompile for the first time.
Semantic-integrity hardening
Seven priority findings from a full internal review, each now a checked invariant rather than a convention — strengthening Jaune specifically as a specification and verification substrate, the role everything else here depends on:
- typed error ADTs throughout the core semantics, with String confined to a shrink-only allowlist at the external boundary;
- checked entry points binding an executable snapshot to canonical validated state, and a wire ingress that a hand-built block cannot reach;
- configured execution that fails closed before a chain’s earliest implemented era, instead of assuming every schedule starts at timestamp zero;
- zero panics and zero partial definitions in the library, mechanically enforced — one of the removals fixed a real bug in twist-coefficient extraction, not merely a totality gap.
A machine interface: t8n
lake exe jaune t8n speaks the ecosystem’s standard transition-tool interface: alloc/env/txs in, result and post-state out, with the same fail-closed fork lane as the fixture runner (Prague through BPO2 then, Amsterdam added since) — no default fork, no silent fallback, and unclaimed modes refused rather than ignored. Conformance checks the complete output against goldens from the pinned execution-specs, with every normalization and deviation explicit and each case run twice for determinism. A three-way acceptance run against go-ethereum registered only divergences in which Jaune matches its pinned conformance target. The frontend lives outside the proof-facing import closure, exactly as the spike prescribed.
Arithmetic layers, a canonical vocabulary, and a resource-aware harness
Two additive layers for the sibling’s arithmetic études, each a new module and nothing changed beneath it: MulDiv, the floor and ceiling a·b/d facts — bounds, monotonicity, round-trip dust, the perturbation lever a donation pulls — with their word-level bridges; and RPow, the scale-parametric rounded multiply and the reference-shaped square-and-multiply loop with its exact telescoping error band, every statement brute-forced by an independent falsifier before proof and all 127 declarations audited to the standard axioms. The instruction vocabulary moved to the canonical EVM mnemonics before any outside consumer, with a gate that refuses the retired spellings. And the fixture harness became resource-aware: unobservable calldata is released rather than retained across frames, --jobs auto sizes itself from memory as well as cores, and a live resource collector records what a run actually cost.
Amsterdam, a fifth fork
Four merged steps took Amsterdam from a declared name to a runnable rule set at the Glamsterdam devnet-8 pin. First the identity, the rule data and a separate fixture lane, with every fork constant compared against the pinned execution-specs by a gate. Then EIP-8037’s metering: a frame with two gas reservoirs, and the totality proof restated over a gas measure that covers both. Then the block level — the block-level access list, the new request contracts, SLOTNUM and DUPN/SWAPN/EXCHANGE, the raised code limits — and finally the BPO2-to-Amsterdam transition suites and a t8n handshake that advertises exactly the lane the binary runs. The static suite passes whole; the transition suite’s four identity findings are recorded, not aliased.
Rules by construction, and a bounded memory closure
Every machine now carries its Fork, and its rules are a total function of it, so rule validity is one theorem over five records instead of a premise on every statement; a static gate refuses any interpreter site that reads a repriced number from a global instead of the selected rules. Separately, a call chain 1,024 frames deep no longer keeps one copy of a megabyte of calldata per frame: on the pre-Amsterdam forks, the four call opcodes share one evaluated slice through the proved @[csimp] equation of §1.1, and a memory-probe gate holds the peak under a fixed budget.
Lean 4.34, and the v20.0.2 mainnet release
Lean and mathlib moved to v4.34.0, with the gate catalogue re-run on the migrated tree; the current-mainnet lane moved to the tests@v20.0.2 release, whose manifest is Table 1; and a round of fixes landed for contract-creation collisions, original storage of freshly created accounts, and the block-level access list’s index accounting.
Jaune as a library you can prove against
The canonical execution-derivation layer — the relational Exec type, its adequacy with the interpreter, derivation, settlement and chronology — moved from Blanc into Jaune under Jaune’s own names, and Blanc now imports it instead of keeping a copy. Around it: symbolic PUSH and ADD/SUB/MUL rules, two checked examples that run symbolic bytecode to completion, a task-organized guide (API.md) that says which modules are supported, incidental, or experimental, and 53 exact axiom pins with a provenance manifest, computed by one from-scratch walker that Blanc’s audit reuses. CI installs Jaune into a fresh package from an exact Git commit and verifies what it installed.
What is stated as not yet done
- Amsterdam tracks a devnet; its pin, rules and lane move when upstream does, and its mainnet activation is written only once the pinned upstream revision schedules one — the constants gate turns red that day.
- The four transition-suite identity findings stay recorded until upstream declares alternative exceptions for those fixtures.
- The message-settlement theorems for revert and exceptional halt are proved for Prague through BPO2; no Amsterdam form is proved yet.
- Jump destinations: the Amsterdam walk is proved equal to the earlier one, but the bridge from either walk to the interpreter’s own scan is sampled on 64 KiB blobs, not proved.
- There is no running-prefix interface: an Exec derivation always runs to an outcome.
More contracts over the semantics
WETH was the existence proof, not the destination — and the sibling project has since moved the frontier repeatedly: Blanc’s WETH10 exercises Jaune’s pipeline end to end, from a proved creation transaction through configured-chain reachability with a future-redeemability flagship on top; its Lido CircuitBreaker port carries a deployed guardian’s Registry integrity from an official-parameters deployment to every reachable future, over arbitrary hostile callees; its deposit-contract port crosses the SHA-256 precompile — the one hash here that carries a FIPS 180-4 equivalence theorem — twelve times per deposit; and its proxy and gateway ports are the first theorems about delegation and about two verified contracts at once. Blanc’s results cover Prague through BPO2; Amsterdam coverage there is future work, as Blanc states. The open questions are which invariants justify their proof effort, how much block-level reasoning transfers between contracts, and where a proof-first language stops being expressive enough. Collaborators with a target property in mind are exactly who this page is for — and the Blanc page is the worked answer so far.
§ 9 Who this is for
Three ways in.
Proof engineers & PL researchers
A live, non-toy verification target: ~530,000 lines of Lean 4 across the sibling pair, spanning an executable semantics, a verified compiler, and contract invariants proved to chain altitude — with well-posed open problems in representation choice, proof-exposure budgeting, and fork-parameterised reasoning. The sufficiency arc alone is a worked case study in retrofitting totality onto a large fueled interpreter.
then: Jaune/Sufficiency.lean → exec
and: Blanc/LidoCircuitBreakerHistoryChain.lean → the theorems of §6
Protocol & client engineers
An independent implementation of the Prague-through-BPO2 rules, and of Amsterdam at its devnet pin, that reads as a specification and runs the real corpus — a differential oracle, a precise reference for when a fixture disagrees with your client, and a place where each rule is stated once, without the surrounding engineering.
scripts/check-mainnet.sh --suite smoke in 0.5 s
Security researchers & DeFi builders
Invariants that hold at chain level, not per function. A fuzzer searches for the counterexample; this is the other half — a machine-checked proof that, for a stated invariant and contract, no counterexample exists in any reachable state, under any schedule of the forks the proof covers.
skbaek.github.io/blanc — the sibling’s own evidence page
§ 10 Reproduce it
Two clones, a build, and a corpus.
The only prerequisite is elan, the Lean toolchain manager — exact Lean and mathlib versions are pinned in the repositories, and Blanc pins Jaune by immutable commit.
Jaune
run the spec against real fixturesThe corpus is not in the repository: the third line fetches the pinned execution-specs tests@v20.0.2 release, verifies its checksum before extraction, and installs it at ~/eest-mainnet-v20.0.2 — a 540 MB download and about 9.1 GB of extracted files on disk, so budget 13 GB free and run it before either of the last two lines. See scripts/vectors/SOURCES.md for pinned provenance and the read-only environment doctor. The blockchain_tests directory is the one this runner consumes; EEST fills the same test into state_tests and two engine trees under the same file name, and those describe a different consumer.
A bare run executes every case in the file whose network this build supports, each under its own rules; --network Prague narrows that to one and --help lists the labels. A run fails if no case survives its filters, so green never means nothing was checked. Every gate’s exact command, scale, runtime, and pass criterion is catalogued in scripts/GATES.md; the fixture harnesses take --jobs auto, which ran the whole current-mainnet corpus in about ten minutes on the catalogue host.
Blanc
check the proofs yourselfcheck.sh builds the library and runs the axiom audit — 1,372 audited theorems, the history rungs of § 6 among them, each checked against its own pinned expected axiom set; it exits non-zero on an extra or missing axiom, on sorryAx, ofReduceBool, or ofReduceNat. No fixture corpus is needed: this side is proof, not testing.
Both projects depend on mathlib, so both open with lake exe cache get — it downloads mathlib's prebuilt artifacts. Skip it and lake build compiles mathlib from source, which takes hours, and several GB land under .lake/ either way. Jaune imports only seven mathlib modules; its README names them, and fetching just those was measured at about 70 MB on 2026-09-24.
Your own proofs
depend on Jaune from LakePin a full commit: there are no version tags and no compatibility promise across revisions. API.md sorts the library by proof task and says which modules are supported, which are incidental, and which (the Amsterdam lane) are experimental; Examples/ holds the checked examples it cites, which compile with every build of the same revision; and scripts/external-consumer.py prepares and verifies a disposable package installed this way, as CI does for every push to main and every pull request.
- Successor in spirit, not in office. Jaune is an independent project with no affiliation to the Ethereum Foundation. Ethereum’s canonical specification is execution-specs, which Jaune mirrors at pinned commit 4198b9c5 (mainnet branch, 2025-09-19) and tests against. Amsterdam, which that commit predates, follows execution-specs 7341820, the commit the Glamsterdam devnet-8 fixtures were filled from, and every Amsterdam constant is checked against it. ↩
- Jaune was previously published under the name ELeVM; the repository was renamed in July 2026 and old links redirect.
- Figures on this page are drawn from committed reports, baselines, and benchmark instruments in the repositories, current as of 2026-09-25; a figure that carries its own date is the most recent committed measurement of it. Wall-clock numbers are same-machine, same-instrument measurements on a 10-core Apple M5; fixture counts are exact. The current-mainnet lane covers every blockchain_tests fixture of tests@v20.0.2 whose fork Jaune supports; exclusions for unsupported historical forks are machine-generated with per-fork reasons, and unknown labels fail the manifest check.