diff --git a/execution-kernel/MACHINE-INTERFACE-CONTRACT.md b/execution-kernel/MACHINE-INTERFACE-CONTRACT.md new file mode 100644 index 0000000..9ce03ba --- /dev/null +++ b/execution-kernel/MACHINE-INTERFACE-CONTRACT.md @@ -0,0 +1,294 @@ +# MachineInterface (MI) contract, Execution Kernel treapta 1 + +Chain 2800 (AERE). Written 2026-08-15, the day the post-quantum consensus milestone went +live at block 14,050,000. This is the seam for interface **I3** in +`aerenew/strategie/EXECUTION-KERNEL-2026-08-15.md`. + +This document is a **contract**, not an implementation. Treapta 1 adds no new infrastructure. +Production Besu already ships `BlockProcessor` and `MainnetBlockProcessor` under +`ethereum/core/.../mainnet/`. Treapta 1 is (a) the written contract of what that seam guarantees +and forbids, and (b) a differential replay harness that proves two independent execution paths +derive the **same** post-state from the same input on **real live blocks**. The harness lives +next to this file: `mi-replay-diferential.mjs`. + +The rule of the house applies to every sentence here: **a claim asserts no more than the +measurement that backs it.** Anything below that the harness does not measure is marked +NOT MEASURED in the harness output, never asserted as proven. + +--- + +## 1. What MI is, in one line + + MI(pre_state, block) -> (post_state, receipts) + +MI is the pure function that a block-processing engine computes. Given the world state before +a block (`pre_state`) and the block itself (`block`), it produces the world state after the +block executed (`post_state`) and the execution output (`receipts`). Nothing else is an input. +Nothing else is an output. + +The value of treapta 1 is not a new virtual machine. It is that this function is **named, +isolated, and re-derivable from a stranger's machine**, so that "a second execution engine +tomorrow" becomes a mechanical slot instead of a promise. + +--- + +## 2. The interface + +``` +interface MachineInterface { + // Deterministic, side-effect-free block execution. + // Reads state ONLY through `view`. Writes nothing outside the returned post_state. + Result execute(StateView view, Block block); +} + +// The only channel through which MI may read prior state. +interface StateView { + Account account(Address a); // nonce, balance, codeHash, storageRoot + Bytes code(Address a); + Word storage(Address a, Word slot); + Hash blockHash(long number); // only within the EIP-2935 / BLOCKHASH window + Header header(); // the header of the block being executed + // NO clock, NO filesystem, NO network, NO RNG, NO ambient config read here. +} + +// The output of execute(). Fully determined by (pre_state, block). +struct Result { + Hash postStateRoot; // world-state trie root AFTER the block + List receipts; // one per transaction, in order + Hash receiptsRoot; // trie root of receipts + long gasUsed; + Bloom logsBloom; // OR of every receipt bloom +} +``` + +The three roots and `gasUsed`/`logsBloom` are exactly the header fields the live chain already +commits per block, so an MI result is checkable against the canonical header with no privileged +access. That is what the harness does. + +--- + +## 3. What MI guarantees + +1. **Determinism.** For a fixed `(pre_state, block)`, `execute` returns one and only one + `Result`, on every machine, in every process, at every wall-clock time. Two runs that + disagree are a contract violation, not a nondeterministic outcome to be retried. See section 5 + for the exact definition of "same" used here. + +2. **Purity of reads.** Every byte of prior state that influences the result is read through + `StateView`. If a value is not reachable through `StateView`, it may not change the result. + This is the load-bearing rule and it has its own section (section 4). + +3. **Isolation of writes.** `execute` mutates nothing observable except by returning + `post_state`. No global, no cache that a later block can read, no file, no counter. A cache + is permitted only if it is a pure function of `StateView` inputs and is invisible to the + result (an index that can be dropped and rebuilt with byte-identical output; see the D-153 / + log-filter-cache lesson in `CLAUDE.md`, "a computed index is deleted without losing history"). + +4. **Input closure.** The admissible inputs are exactly those in section 6. Anything outside + that set (a timestamp read from the OS clock, an environment variable, a random seed, the + identity of which validator won the round) is forbidden as an input. The base-fee-floor fork + is a function of `block.number` and system properties fixed per node, therefore part of the + ruleset, not an ambient input; see section 6. + +5. **Finality independence.** MI is an execution seam, not a consensus rule. It never decides + finality. Finality on 2800 is QBFT + Falcon (interfaces I1/I2). An MI implementation that is + slow, absent, or divergent must never be able to touch whether a block is final. This mirrors + the golden rule of I4: proofs never gate finality; MI never gates it either. + +--- + +## 4. The purity rule of StateView (the one that matters) + +**Everything that can change the result is read through `StateView`, and nothing else is.** + +Why it is the rule the whole treapta stands on: the failure mode of a second execution path +(Block-STM in treapta 4, or a second client today) is not a loud crash. It is a **silent state +divergence**, a `post_state` that differs by one slot with no error raised. The only way that +divergence stays impossible is if the complete set of inputs is closed and named, so that two +implementations reading the same `StateView` over the same `block` cannot legally reach two +answers. + +Concretely, MI is in violation if any of the following influence the result: + +- the wall-clock time, an OS timer, or process uptime; +- an environment variable, a config file, or any node-local setting **other than** the fixed + ruleset parameters of section 6; +- a random number, a map iteration order, a thread-scheduling order, or floating point; +- the contents of any cache that is not a pure function of `StateView` inputs; +- state at a height outside the window `StateView` exposes (the `blockHash` / EIP-2935 window); +- which node, which peer, or which validator produced or relayed the block. + +**Negative control, required.** The purity rule is not proven by reading code and agreeing it +looks pure. It is proven by **planting an impurity and watching the gate go red.** A conforming +treapta-1 test suite MUST include at least one planted violation of the list above (for example: +make `execute` branch on `System.currentTimeMillis()`), and MUST show the differential gate +turns that plant into a divergence. A gate that has never rejected an impure implementation is +applause, not a gate. The runnable analogue of this, at the header-root level, is the negative +control mode of `mi-replay-diferential.mjs` (section 8). + +--- + +## 5. What determinism means here + +"Same result" is defined by **byte-level equality of the committed commitments**, compared in +both directions (section 7). Two MI results for the same input are equal when, and only when: + +| field | equality | +|---|---| +| `postStateRoot` | identical 32-byte hash | +| `receiptsRoot` | identical 32-byte hash | +| `transactionsRoot` (input identity) | identical 32-byte hash | +| `logsBloom` | identical 256 bytes | +| `gasUsed` | identical integer value (compared by value, so `0x1f4` == `0x01f4`) | +| block `hash`, `parentHash` | identical 32-byte hash | + +Determinism is a claim about these commitments, not about internal representation. Two clients +may lay out their trie nodes differently in memory, name their threads differently, and store +`extraData` differently for the same block (QBFT lets each node assemble the final header +locally, and the block hash does not cover the seals; see `CLAUDE.md`). None of that is an MI +input or output. Only the commitments above are. + +**Determinism is measured across implementations, not asserted from one.** A single client +agreeing with itself proves nothing about determinism. The measurement that means something is +two independent engines, built by different teams in different languages, deriving the same +commitments from the same input. Today that is Besu (serial, live producer) versus Nethermind +(independent second client, live at `client2.aere.network`). Tomorrow it is Block-STM through +the same seam (treapta 4), measured by the same harness. + +**Honesty boundary, carried from `cross-client-determinism/`.** A matching header root served +over RPC proves, on its own, only that both endpoints **serve the same bytes** for a block that +travels over devp2p. The load-bearing fact underneath is that Nethermind **validates on +processing and rejects on disagreement**: this exact client has rejected live blocks it computed +differently (`WithdrawalsEmpty` at 2,075,341, `HeaderGasUsedMismatch` at 2,149,971). A block +that sits in the follower's canonical chain below its processed head is therefore a block whose +`post_state` the follower's own engine reproduced. The harness states this boundary on every run +and never claims more than it. + +--- + +## 6. Admissible inputs + +`execute(StateView view, Block block)` admits exactly: + +1. **`block`**: header + ordered transaction list. From the header, MI may use only the fields + that are consensus inputs to execution: `number`, `parentHash`, `timestamp` (as the block's + own declared timestamp, an input carried IN the block, never read from the OS clock), + `gasLimit`, `baseFeePerGas`, `coinbase`/`miner`, `prevRandao`/`mixHash`, `withdrawals`, + and the blob fields where active. `extraData`/seals are consensus material, not execution + input, and MI must not branch execution on them. + +2. **`view`**: the pre-state, reachable only through `StateView` as defined in section 4. + +3. **The fixed ruleset**, which is a pure function of `block.number` and the per-node system + properties that define the fork schedule, and is identical on every honest node: + - the EVM fork rules by height/timestamp (Cancun/Prague/Osaka milestones); + - `aere.basefee.floor.forkBlock = 10141734`, floor 1 Gwei, delivered via `BESU_OPTS`. This is + the live AERE fork. It is a **rule keyed on block number**, not an ambient input: every node + computes the same base-fee floor for the same height. An engine that lacks it computes a + different fee, therefore a different state root, and is a different MI, not the same one with + a different environment. This is exactly the D-150 lesson: a binary missing the floor fork + froze a real node at a state-root mismatch while every shape check stayed green. + +Everything not in this list is inadmissible as an input. In particular: no OS clock, no RNG, no +environment read at execution time, no network, no cross-block mutable cache. + +--- + +## 7. Both directions (the D-150 rule) + +The most expensive lesson on this project (D-150, 2026-08-07): **we verified what we added, +never what we lost.** Every gate asked "does the new thing contain what we put in?" and none +asked "did it keep what was already there?" A binary missing three production subsystems passed +hundreds of green tests. + +Applied to MI differential replay, "both directions" is not optional and not cosmetic: + +- **Forward (Besu -> Nethermind):** for every field Besu commits, Nethermind must commit the + same value. Catches a field Nethermind got **wrong**. +- **Reverse (Nethermind -> Besu):** for every field Nethermind commits, Besu must commit the + same value. Catches a field Nethermind has that Besu **lacks**, and a block Nethermind holds + that Besu does not. + +The comparison therefore takes the **union of the field keys on both sides** and the **union of +the block numbers on both sides**, and flags three distinct kinds of divergence: + +1. present on one side, **absent** on the other (a lost field, or a block only one side holds); +2. present on both, **different** value; +3. and it does this symmetrically, so "only on them" is caught with the same weight as + "only on us". "Only on them" is the direction that kills. + +A one-directional loop (iterate Besu's blocks, ask Nethermind for Besu's fields) is exactly the +D-150 mistake in miniature: it can never see a field or a block that exists only on the other +side. The harness does not do that. + +--- + +## 8. The negative control (why a green run means anything) + +A harness that has never gone red is decoration. Before any green run counts, the harness must +be shown to turn a planted divergence into a red verdict, using the **same comparison code** the +real run uses, not a copy of it. + +`mi-replay-diferential.mjs` has a built-in negative-control mode driven by the environment +variable `AERE_MI_INJECT`. When it is set, the harness fetches **real** headers from both live +clients, plants exactly one synthetic divergence into the fetched data, runs the **real** +`compareInterval` over it, and then **inverts its success condition**: the run passes only if the +gate caught the exact planted divergence and turned red, and fails if the plant slipped through +green. This is the runnable form of "plant a fault, require red". + +The plant can target either direction, so both arms of section 7 are exercised: + +| `AERE_MI_INJECT` value | what it plants | which direction it proves | +|---|---|---| +| `stateRoot` (or `1`) | flips one nibble of one block's `stateRoot` on the Nethermind side | forward, value differs | +| `receiptsRoot@5` | corrupts `receiptsRoot` at interval index 5 | forward, value differs | +| `gasUsed` | bumps `gasUsed` by 1 on Nethermind | forward, value differs, numeric | +| `drop` | makes Nethermind "not have" one block | reverse, block only on Besu | +| `missing:stateRoot` | deletes the `stateRoot` field from one Nethermind header | reverse, a **lost** field | +| `stateRoot:besu` | plants the corruption on the Besu side instead | reverse, value differs | + +The default `execute`-level purity control (planting an impure read and watching the gate go red) +is the Java-side obligation of section 4; the header-root control above is its runnable analogue +that anyone can execute against the live chain with no build tree. + +--- + +## 9. The gate + +Treapta 1 is CLOSED only when all of the following hold, each measured, none asserted: + +1. For N real live blocks, every MI field in section 5 is **identical in both directions** + between the Besu path and the second path. Coverage (N and the exact range) is printed, and + any block that could not be compared is counted and attributed, never silently dropped. +2. The per-client pre-state linkage holds: within each client, `parentHash(n) == hash(n-1)` + across the interval, so the `(pre_state, block) -> (post_state)` tuple is pinned as a chain + and not just a set of isolated roots. +3. The negative control (section 8) turns a planted divergence red, hitting the exact field and + block it planted, in each direction it is asked to. +4. The Java-side purity control (section 4) plants an impure read and the differential gate turns + it red. + +Items 1 through 3 are runnable today by anyone, against public endpoints, with no dependencies, +via `mi-replay-diferential.mjs`. Item 4 is the obligation on the treapta-1 test suite inside the +fork build. + +--- + +## 10. What this contract does NOT claim + +Printed as NOT MEASURED by the harness, never as a pass: + +- **That consensus is post-quantum.** It is not. Consensus finality is classical secp256k1 ECDSA + QBFT plus a Falcon certificate. MI is an execution seam and says nothing about consensus. +- **That equal header roots prove independent execution by themselves.** See the honesty boundary + in section 5. The load-bearing fact is Nethermind's reject-on-disagreement behaviour, not the + served bytes. +- **That the second path re-executed history it never reached.** Only blocks both clients hold + and serve in the compared interval are covered; the rest is counted as uncovered. +- **Anything about performance.** MI determinism is not throughput. This contract makes no TPS + claim and the harness runs no benchmark. +- **Who operates the validator or client keys.** Address independence is not operator + independence, and nothing measurable from a public endpoint settles it. +- **State older than roughly the last 512 blocks** on the public endpoints. The harness reads + headers and bodies pinned to explicit block numbers, never a deep state query, precisely to + stay inside what these endpoints can answer honestly. diff --git a/execution-kernel/mi-replay-diferential.mjs b/execution-kernel/mi-replay-diferential.mjs new file mode 100644 index 0000000..47745f0 --- /dev/null +++ b/execution-kernel/mi-replay-diferential.mjs @@ -0,0 +1,497 @@ +#!/usr/bin/env node +// mi-replay-diferential.mjs +// +// MachineInterface (MI) differential replay harness, Execution Kernel treapta 1. +// Chain 2800 (AERE). See MACHINE-INTERFACE-CONTRACT.md next to this file. +// +// WHAT IT PROVES +// MI is the pure function MI(pre_state, block) -> (post_state, receipts). +// Two independent execution engines each committed a post-state and receipts for the same +// canonical blocks: Besu (the live serial producer) and Nethermind (the independent second +// client). For a range of REAL live blocks this harness fetches, from BOTH clients, the header +// commitments that fully determine an MI result: +// +// stateRoot post-state world trie root (the headline MI output) +// receiptsRoot trie root of receipts (MI output) +// logsBloom OR of all receipt blooms (MI output) +// gasUsed total gas (MI output, compared by value) +// transactionsRoot the block's tx list (MI input identity) +// hash, parentHash block identity + pre-state linkage +// +// If both engines committed the same values for the same input, two independent MI +// implementations derived the same (post_state, receipts). That is the MI determinism claim. +// +// BOTH DIRECTIONS (the D-150 rule) +// The comparison is SYMMETRIC. It takes the union of block numbers on both sides and the union +// of field keys on both sides, and flags three kinds of divergence: +// - present on one side, ABSENT on the other (a lost field, or a block only one side holds) +// - present on both, DIFFERENT value +// "Only on them" is caught with the same weight as "only on us". D-150: we verify what we lost, +// not only what we added. +// +// NEGATIVE CONTROL (why a green run means anything) +// Set the env var AERE_MI_INJECT to plant exactly one synthetic divergence into the REAL +// fetched data, then the harness runs the SAME comparison code and REQUIRES it to go red on the +// exact planted field/block. In inject mode the success condition is inverted: exit 0 only if +// the plant was caught. See the table below and section 8 of the contract. +// +// READ-ONLY BY CONSTRUCTION +// The only JSON-RPC methods it will send are eth_chainId, eth_blockNumber, eth_getBlockByNumber. +// Any other method is refused before it leaves this process. Nothing is signed, nothing is +// broadcast, no validator is touched, no state is written. +// +// NO DEPENDENCIES +// Node 18+ and its built-in http/https only. No npm, no ethers. Runnable by anyone. +// +// USAGE +// node mi-replay-diferential.mjs +// AERE_COUNT=32 node mi-replay-diferential.mjs +// AERE_START=14000000 AERE_END=14000050 node mi-replay-diferential.mjs +// AERE_BESU_RPC=https://rpc.aere.network AERE_NETH_RPC=https://client2.aere.network \ +// node mi-replay-diferential.mjs +// +// Negative control (plant a divergence, require red): +// AERE_MI_INJECT=stateRoot node mi-replay-diferential.mjs # forward, value differs +// AERE_MI_INJECT=drop node mi-replay-diferential.mjs # reverse, block only on Besu +// AERE_MI_INJECT=missing:stateRoot node mi-replay-diferential.mjs # reverse, a LOST field +// AERE_MI_INJECT=stateRoot:besu node mi-replay-diferential.mjs # reverse, corrupt Besu side +// +// ENV VARS +// AERE_BESU_RPC default https://rpc.aere.network (also reads BESU_RPC) +// AERE_NETH_RPC default https://client2.aere.network (also reads NETHERMIND_RPC) +// AERE_START, AERE_END explicit inclusive range (decimal). AERE_END may be "head". +// AERE_COUNT blocks ending near head when START/END unset (default 16) +// AERE_SAFETY blocks to stay behind the min head, avoids racing the tip (default 4) +// AERE_CONCURRENCY parallel header fetches (default 6) +// AERE_MI_INJECT negative-control plant (see table above). Unset = normal differential run. +// AERE_OUT results JSON path (default results/mi-replay--.json) +// +// EXIT CODES +// normal run: 0 AGREE identical in both directions, coverage > 0 +// 1 MISMATCH a field differed or a block was one-sided +// 2 NOT MEASURED no coverage / unreachable endpoint / chain-id mismatch +// inject run: 0 the gate caught the planted divergence (control PASSED) +// 1 the plant slipped through green (control FAILED) OR fatal + +import http from 'node:http'; +import https from 'node:https'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); + +const BESU_RPC = process.env.AERE_BESU_RPC || process.env.BESU_RPC || 'https://rpc.aere.network'; +const NETH_RPC = process.env.AERE_NETH_RPC || process.env.NETHERMIND_RPC || 'https://client2.aere.network'; +const CONCURRENCY = Math.max(1, parseInt(process.env.AERE_CONCURRENCY || '6', 10)); +const DEFAULT_COUNT = 16; +const SAFETY = Math.max(0, parseInt(process.env.AERE_SAFETY || '4', 10)); +const INJECT = process.env.AERE_MI_INJECT || ''; + +// The commitments that fully determine / identify an MI result. See contract section 5. +// kind 'hash' compared as lowercased hex; kind 'num' compared by integer value. +const MI_FIELDS = [ + { name: 'hash', kind: 'hash', role: 'block identity' }, + { name: 'parentHash', kind: 'hash', role: 'pre-state linkage' }, + { name: 'stateRoot', kind: 'hash', role: 'MI output: post-state' }, + { name: 'transactionsRoot', kind: 'hash', role: 'MI input: tx list' }, + { name: 'receiptsRoot', kind: 'hash', role: 'MI output: receipts' }, + { name: 'gasUsed', kind: 'num', role: 'MI output: gas' }, + { name: 'logsBloom', kind: 'hash', role: 'MI output: logs bloom' }, +]; +const FIELD_BY_NAME = new Map(MI_FIELDS.map((f) => [f.name, f])); + +// ---- read-only JSON-RPC over the Node built-ins, no dependencies ------------------------------- + +const ALLOWED_METHODS = new Set(['eth_chainId', 'eth_blockNumber', 'eth_getBlockByNumber']); + +function rpc(endpoint, method, params, tries = 4) { + if (!ALLOWED_METHODS.has(method)) { + return Promise.reject(new Error(`method ${method} is not in the read-only allowlist`)); + } + const u = new URL(endpoint); + const lib = u.protocol === 'http:' ? http : https; + const port = u.port ? parseInt(u.port, 10) : (u.protocol === 'http:' ? 80 : 443); + const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }); + const attempt = () => new Promise((resolve, reject) => { + const req = lib.request({ + hostname: u.hostname, + port, + path: u.pathname === '/' && u.search ? u.pathname + u.search : (u.pathname || '/'), + method: 'POST', + headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) }, + timeout: 15000, + }, (res) => { + let d = ''; + res.on('data', (c) => { d += c; }); + res.on('end', () => { + if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode} from ${u.hostname}`)); + let j; + try { j = JSON.parse(d); } catch (_) { return reject(new Error(`non-JSON reply from ${u.hostname}: ${d.slice(0, 100)}`)); } + if (j.error) return reject(new Error(`RPC error ${JSON.stringify(j.error)}`)); + resolve(j.result); + }); + }); + req.on('timeout', () => req.destroy(new Error('timeout'))); + req.on('error', reject); + req.write(body); + req.end(); + }); + return (async () => { + let lastErr; + for (let t = 0; t < tries; t++) { + try { return await attempt(); } catch (e) { lastErr = e; await sleep(250 * (t + 1)); } + } + throw lastErr; + })(); +} + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const hx = (n) => '0x' + BigInt(n).toString(16); + +async function chainIdOf(endpoint) { return parseInt(await rpc(endpoint, 'eth_chainId', []), 16); } +async function headOf(endpoint) { return parseInt(await rpc(endpoint, 'eth_blockNumber', []), 16); } + +// Fetch a header, no tx bodies. Returns a plain object with the MI fields, or null if the block +// is absent (the endpoint does not hold it). Pinned to an explicit number, never "latest". +async function getHeader(endpoint, number) { + const b = await rpc(endpoint, 'eth_getBlockByNumber', [hx(number), false]); + if (!b) return null; + const out = { number: parseInt(b.number, 16) }; + for (const f of MI_FIELDS) if (b[f.name] !== undefined) out[f.name] = b[f.name]; + return out; +} + +async function mapPool(items, limit, worker) { + const results = new Array(items.length); + let next = 0; + async function run() { + for (;;) { + const i = next++; + if (i >= items.length) return; + results[i] = await worker(items[i], i); + } + } + await Promise.all(Array.from({ length: Math.min(limit, items.length) }, run)); + return results; +} + +// ---- the core comparison: symmetric, both directions -------------------------------------------- + +function eqField(f, a, b) { + if (a == null || b == null) return a == null && b == null; + if (f.kind === 'num') { try { return BigInt(a) === BigInt(b); } catch (_) { return false; } } + return String(a).toLowerCase() === String(b).toLowerCase(); +} + +// besuByNum / nethByNum: Map. numbers: the interval. +// Returns { divergences:[...], directions:{...}, fieldsCompared }. +// Each divergence has { block, field|null, kind, besu, nethermind }. +// kind 'block-only-besu' block held by Besu, absent on Nethermind (reverse direction) +// kind 'block-only-nethermind'block held by Nethermind, absent on Besu (forward would miss it) +// kind 'field-only-besu' field present on Besu, absent on Nethermind (a LOST field) +// kind 'field-only-nethermind'field present on Nethermind, absent on Besu +// kind 'field-differs' present on both, different value +function compareInterval(besuByNum, nethByNum, numbers) { + const divergences = []; + const directions = { forwardDiff: 0, reverseOnlyBesu: 0, reverseOnlyNeth: 0, fieldsCompared: 0 }; + for (const n of numbers) { + const a = besuByNum.get(n); + const b = nethByNum.get(n); + if (a == null && b == null) continue; // neither holds it; out of coverage + if (a != null && b == null) { + divergences.push({ block: n, field: null, kind: 'block-only-besu', besu: 'present', nethermind: 'absent' }); + directions.reverseOnlyBesu++; + continue; + } + if (a == null && b != null) { + divergences.push({ block: n, field: null, kind: 'block-only-nethermind', besu: 'absent', nethermind: 'present' }); + directions.reverseOnlyNeth++; + continue; + } + // both hold the block: compare the UNION of field keys, so a one-sided field is caught. + const keys = new Set([...Object.keys(a), ...Object.keys(b)].filter((k) => k !== 'number' && FIELD_BY_NAME.has(k))); + for (const k of keys) { + const f = FIELD_BY_NAME.get(k); + const av = a[k]; + const bv = b[k]; + const aHas = av !== undefined && av !== null; + const bHas = bv !== undefined && bv !== null; + if (aHas && !bHas) { divergences.push({ block: n, field: k, kind: 'field-only-besu', besu: av, nethermind: null }); directions.forwardDiff++; continue; } + if (!aHas && bHas) { divergences.push({ block: n, field: k, kind: 'field-only-nethermind', besu: null, nethermind: bv }); directions.forwardDiff++; continue; } + if (!aHas && !bHas) continue; + directions.fieldsCompared++; + if (!eqField(f, av, bv)) divergences.push({ block: n, field: k, kind: 'field-differs', besu: av, nethermind: bv }); + } + } + return { divergences, directions }; +} + +// Per-client pre-state linkage: parentHash(n) == hash(n-1) across the interval. Pins the +// (pre_state, block) -> (post_state) tuple as a chain, not just isolated roots. See contract sec 9. +function preStateLinkage(byNum, numbers) { + const issues = []; + let checked = 0; + for (const n of numbers) { + const cur = byNum.get(n); + const prev = byNum.get(n - 1); + if (!cur || !prev) continue; + checked++; + if (String(cur.parentHash).toLowerCase() !== String(prev.hash).toLowerCase()) { + issues.push(`block ${n} parentHash ${cur.parentHash} != hash(${n - 1}) ${prev.hash}`); + } + } + return { checked, issues }; +} + +// ---- negative control: plant exactly one synthetic divergence ----------------------------------- + +// spec grammar: field[@index][:side] | drop[@index][:side] | missing:field[@index][:side] +// bare "1"/"on"/"true" => stateRoot at the middle index on the nethermind side. +function parseInject(spec, numbers) { + let s = spec.trim(); + if (s === '1' || s === 'on' || s === 'true') s = 'stateRoot'; + let side = 'nethermind'; + const sideM = s.match(/:(besu|nethermind|neth)$/i); + if (sideM) { side = sideM[1].toLowerCase() === 'besu' ? 'besu' : 'nethermind'; s = s.slice(0, sideM.index); } + let index = Math.floor(numbers.length / 2); + const idxM = s.match(/@(\d+)$/); + if (idxM) { index = Math.min(numbers.length - 1, parseInt(idxM[1], 10)); s = s.slice(0, idxM.index); } + let op = 'corrupt'; + let field = s; + if (s === 'drop') { op = 'drop'; field = null; } + else if (s.startsWith('missing:')) { op = 'missing'; field = s.slice('missing:'.length); } + if (op === 'corrupt' && !FIELD_BY_NAME.has(field)) { + throw new Error(`AERE_MI_INJECT: unknown field "${field}". Use one of: ${MI_FIELDS.map((f) => f.name).join(', ')}, or drop, or missing:`); + } + const block = numbers[index]; + return { op, field, side, index, block }; +} + +function flipHashNibble(v) { + // XOR the last hex nibble with 1, so a valid-shaped but different 32-byte hash is produced. + const last = v[v.length - 1]; + const flipped = (parseInt(last, 16) ^ 1).toString(16); + return v.slice(0, -1) + flipped; +} + +// Mutate `byNum` in place (a Map). Returns the expected divergence the gate MUST report. +function applyInject(byNumBesu, byNumNeth, plan) { + const target = plan.side === 'besu' ? byNumBesu : byNumNeth; + const h = target.get(plan.block); + if (!h) throw new Error(`inject target block ${plan.block} not present on ${plan.side}`); + if (plan.op === 'drop') { + target.set(plan.block, null); + return { block: plan.block, field: null, expectKind: plan.side === 'besu' ? 'block-only-nethermind' : 'block-only-besu' }; + } + if (plan.op === 'missing') { + delete h[plan.field]; + // deleting on nethermind => field present on besu only => field-only-besu, and vice versa. + return { block: plan.block, field: plan.field, expectKind: plan.side === 'besu' ? 'field-only-nethermind' : 'field-only-besu' }; + } + // corrupt: change the value but keep the shape + const f = FIELD_BY_NAME.get(plan.field); + if (f.kind === 'num') h[plan.field] = hx(BigInt(h[plan.field]) + 1n); + else h[plan.field] = flipHashNibble(String(h[plan.field])); + return { block: plan.block, field: plan.field, expectKind: 'field-differs' }; +} + +// ---- range resolution --------------------------------------------------------------------------- + +function resolveRange(head) { + let start; + let end; + if (process.env.AERE_START || process.env.AERE_END) { + end = process.env.AERE_END && process.env.AERE_END !== 'head' ? parseInt(process.env.AERE_END, 10) : head; + start = process.env.AERE_START ? parseInt(process.env.AERE_START, 10) : end; + } else { + const count = parseInt(process.env.AERE_COUNT || String(DEFAULT_COUNT), 10); + end = head; + start = Math.max(0, head - count + 1); + } + if (start > end) [start, end] = [end, start]; + return { start, end }; +} + +// ---- main --------------------------------------------------------------------------------------- + +async function main() { + const report = { + harness: 'execution-kernel/mi-replay-diferential.mjs', + contract: 'MACHINE-INTERFACE-CONTRACT.md', + generatedAt: new Date().toISOString(), + besuRpc: BESU_RPC, + nethermindRpc: NETH_RPC, + mode: INJECT ? 'negative-control' : 'differential', + inject: INJECT || null, + chainId: null, + range: null, + heads: null, + blocksCovered: 0, + blocksUncovered: 0, + directions: null, + preStateLinkage: null, + divergences: [], + honesty: 'A matching header root served over RPC proves both endpoints serve the same bytes; ' + + 'the load-bearing fact is that Nethermind validates on processing and rejects on ' + + 'disagreement. Consensus is classical secp256k1 ECDSA QBFT plus Falcon; MI is execution ' + + 'only and this is not a post-quantum-consensus claim. Not a benchmark.', + verdict: null, + }; + + console.log('MachineInterface (MI) differential replay harness, Execution Kernel treapta 1'); + console.log(` contract: ${report.contract}`); + console.log(` Besu RPC: ${BESU_RPC}`); + console.log(` Nethermind: ${NETH_RPC}`); + console.log(` mode: ${report.mode}${INJECT ? ` (AERE_MI_INJECT=${INJECT})` : ''}`); + + // chain-id gate on both endpoints + const [besuChain, nethChain] = await Promise.all([chainIdOf(BESU_RPC), chainIdOf(NETH_RPC)]); + report.chainId = { besu: besuChain, nethermind: nethChain }; + if (besuChain !== 2800) console.warn(` WARNING: Besu chainId ${besuChain}, expected 2800`); + if (nethChain !== besuChain) { + report.verdict = `NOT MEASURED: chainId mismatch (Besu ${besuChain} vs Nethermind ${nethChain})`; + return finish(report, 2); + } + + // heads: compare only up to the lower head, minus a safety margin off the tip + const [besuHead, nethHead] = await Promise.all([headOf(BESU_RPC), headOf(NETH_RPC)]); + report.heads = { besu: besuHead, nethermind: nethHead }; + const safeHead = Math.max(0, Math.min(besuHead, nethHead) - SAFETY); + console.log(` heads: Besu ${besuHead}, Nethermind ${nethHead} (compare at/below ${safeHead})`); + + let { start, end } = resolveRange(safeHead); + if (end > safeHead) end = safeHead; + if (start > end) { + report.verdict = 'NOT MEASURED: no blocks in range are held by both clients'; + return finish(report, 2); + } + report.range = { start, end }; + const numbers = []; + for (let n = start; n <= end; n++) numbers.push(n); + // one extra predecessor for the pre-state linkage check, if available + const linkNumbers = start > 0 ? [start - 1, ...numbers] : numbers.slice(); + console.log(` range: [${start} .. ${end}] (${numbers.length} blocks)\n`); + + // fetch both sides + const [besuArr, nethArr] = await Promise.all([ + mapPool(linkNumbers, CONCURRENCY, (n) => getHeader(BESU_RPC, n).catch(() => null)), + mapPool(linkNumbers, CONCURRENCY, (n) => getHeader(NETH_RPC, n).catch(() => null)), + ]); + const besuByNum = new Map(); + const nethByNum = new Map(); + linkNumbers.forEach((n, i) => { besuByNum.set(n, besuArr[i]); nethByNum.set(n, nethArr[i]); }); + + // negative control: plant exactly one divergence into the REAL fetched data + let expected = null; + if (INJECT) { + const plan = parseInject(INJECT, numbers); + expected = applyInject(besuByNum, nethByNum, plan); + console.log(' NEGATIVE CONTROL: planted a synthetic divergence into fetched data'); + console.log(` plant: op=${plan.op} field=${plan.field ?? '(whole block)'} side=${plan.side} block=${plan.block}`); + console.log(` require: the gate reports kind "${expected.expectKind}" at block ${expected.block}` + + `${expected.field ? ` field ${expected.field}` : ''}\n`); + } + + // coverage accounting (before injection semantics): a block held by neither is uncovered + for (const n of numbers) { + const held = (besuByNum.get(n) != null) || (nethByNum.get(n) != null); + if (held) report.blocksCovered++; else report.blocksUncovered++; + } + + // the core comparison (SAME code for real run and negative control) + const { divergences, directions } = compareInterval(besuByNum, nethByNum, numbers); + report.divergences = divergences; + report.directions = directions; + + // pre-state linkage, per client + report.preStateLinkage = { + besu: preStateLinkage(besuByNum, numbers), + nethermind: preStateLinkage(nethByNum, numbers), + }; + + // sample line so the roots are visibly real, not asserted + const sampleN = numbers[0]; + const sB = besuByNum.get(sampleN); + const sN = nethByNum.get(sampleN); + if (sB && sN) { + console.log('Sample (both clients, block ' + sampleN + '):'); + console.log(` besu stateRoot ${sB.stateRoot}`); + console.log(` nethermind stateRoot ${sN.stateRoot}`); + console.log(''); + } + + console.log('Directions (both, per D-150):'); + console.log(` fields compared on blocks held by both: ${directions.fieldsCompared}`); + console.log(` forward (value differs / one-sided field): ${directions.forwardDiff}`); + console.log(` reverse (block only on Besu): ${directions.reverseOnlyBesu}`); + console.log(` reverse (block only on Nethermind): ${directions.reverseOnlyNeth}`); + const link = report.preStateLinkage; + console.log(` pre-state linkage: besu ${link.besu.issues.length ? 'FAIL' : 'ok'} (${link.besu.checked} links), ` + + `nethermind ${link.nethermind.issues.length ? 'FAIL' : 'ok'} (${link.nethermind.checked} links)`); + console.log(''); + + // verdicts + if (INJECT) { + const caught = divergences.find((d) => d.block === expected.block && d.kind === expected.expectKind && + (expected.field == null || d.field === expected.field)); + if (caught) { + report.verdict = `NEGATIVE CONTROL PASSED: the gate went red on the planted ${expected.expectKind}` + + ` at block ${expected.block}${expected.field ? ` field ${expected.field}` : ''}.`; + console.log('RESULT: ' + report.verdict); + console.log(' (a green run therefore means something: this gate can and did reject.)'); + return finish(report, 0); + } + report.verdict = `NEGATIVE CONTROL FAILED: the planted ${expected.expectKind} at block ${expected.block}` + + `${expected.field ? ` field ${expected.field}` : ''} was NOT caught. The gate is decoration.`; + console.log('RESULT: ' + report.verdict); + return finish(report, 1); + } + + const linkFail = link.besu.issues.length > 0 || link.nethermind.issues.length > 0; + if (report.blocksCovered === 0) { + report.verdict = 'NOT MEASURED: no block in the range was held by either client'; + console.log('RESULT: ' + report.verdict); + return finish(report, 2); + } + if (divergences.length === 0 && !linkFail) { + report.verdict = `AGREE: Besu and Nethermind committed identical MI fields ` + + `(stateRoot, receiptsRoot, transactionsRoot, logsBloom, gasUsed, hash, parentHash) in BOTH ` + + `directions for all ${report.blocksCovered} covered blocks, and the pre-state chain links on ` + + `both clients. Two independent execution paths derived the same post-state.`; + console.log('RESULT: AGREE across ' + report.blocksCovered + ' blocks (both directions, 0 divergences).'); + finish(report, 0); + } else { + report.verdict = `MISMATCH: ${divergences.length} divergence(s)` + + (linkFail ? ' plus a pre-state linkage failure' : '') + ` across ${report.blocksCovered} blocks.`; + console.log(`RESULT: MISMATCH, ${divergences.length} divergence(s):`); + for (const d of divergences.slice(0, 12)) { + console.log(` block ${d.block} ${d.field ? `field ${d.field} ` : ''}[${d.kind}] besu=${trunc(d.besu)} neth=${trunc(d.nethermind)}`); + } + if (linkFail) { + [...link.besu.issues, ...link.nethermind.issues].slice(0, 6).forEach((s) => console.log(' linkage: ' + s)); + } + finish(report, 1); + } +} + +function trunc(v) { const s = String(v); return s.length > 22 ? s.slice(0, 22) + '..' : s; } + +function finish(report, code) { + try { + const outDir = path.join(HERE, 'results'); + fs.mkdirSync(outDir, { recursive: true }); + const tag = report.range ? `${report.range.start}-${report.range.end}` : 'norange'; + const outPath = process.env.AERE_OUT || path.join(outDir, `mi-replay-${report.mode}-${tag}-${Date.now()}.json`); + fs.writeFileSync(outPath, JSON.stringify(report, null, 2)); + console.log(`\nReport written: ${outPath}`); + } catch (e) { + console.warn('could not write report: ' + e.message); + } + process.exit(code); +} + +main().catch((e) => { + console.error('FATAL: ' + e.message); + process.exit(2); +});