Execution Kernel, Stage 1: the MachineInterface seam, contract plus a differential replay harness proven on real blocks
The kernel's first stage is not new infrastructure, it is a written contract for the state-transition seam (pre_state, block) -> (post_state, receipts) through StateView only, plus a harness that proves two independent clients agree on the state root of real chain-2800 blocks, in both directions per the D-150 rule (what was lost, not only what was added). Run and measured 2026-08-15, from the two public endpoints (Besu at rpc.aere.network, Nethermind at client2.aere.network): 24 blocks, 168 fields, zero divergences. All four negative-control plants (value differs, block only on one side, a lost field, a corrupted side) make the gate go red exactly where planted. Installs nothing, three read-only RPC methods, runnable by anyone. This makes "a second execution machine tomorrow" a mechanical slot, not a promise.
This commit is contained in:
parent
4ce1928d9e
commit
cea943ff57
294
execution-kernel/MACHINE-INTERFACE-CONTRACT.md
Normal file
294
execution-kernel/MACHINE-INTERFACE-CONTRACT.md
Normal file
@ -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<Receipt> 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.
|
||||
497
execution-kernel/mi-replay-diferential.mjs
Normal file
497
execution-kernel/mi-replay-diferential.mjs
Normal file
@ -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-<range>-<ts>.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<number, header|null>. 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:<field>`);
|
||||
}
|
||||
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);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user