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.
498 lines
24 KiB
JavaScript
498 lines
24 KiB
JavaScript
#!/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);
|
|
});
|