#!/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', 'web3_clientVersion']); 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. Both endpoints are ' + 'operated by the same party (the trust root does not move; what is removed is the class ' + 'of lies needing only one client implementation). State older than the ~512-block RPC ' + 'state window is not probed. Engine identity of each endpoint is recorded below.', engines: null, 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})` : ''}`); // endpoint-identity gate: one endpoint agreeing with itself measures nothing if (BESU_RPC.replace(/\/+$/, '') === NETH_RPC.replace(/\/+$/, '')) { report.verdict = 'NOT MEASURED: both endpoints are the same URL; agreement of an endpoint with itself is not a measurement'; console.log('RESULT: ' + report.verdict); return finish(report, 2); } // engine identity, recorded so "who answered" travels with every report try { const [ebesu, eneth] = await Promise.all([ rpc(BESU_RPC, 'web3_clientVersion', []), rpc(NETH_RPC, 'web3_clientVersion', [])]); report.engines = { besu: ebesu, nethermind: eneth }; console.log(' engines: ' + ebesu + ' | ' + eneth); if (String(ebesu).toLowerCase().split('/')[0] === String(eneth).toLowerCase().split('/')[0]) { console.log(' WARNING: both endpoints report the SAME engine family; the cross-client claim does not hold for this run'); } } catch (e) { report.engines = { error: String(e.message) }; } // NOT MEASURED, printed on every run (the contract's non-claims, stated by the tool itself): console.log(' NOT MEASURED by this harness: post-quantum consensus; independent derivation'); console.log(' (proven only via the follower validate-on-processing behavior); throughput or'); console.log(' latency; who operates the endpoints (same party today); state older than the'); console.log(' ~512-block RPC window; gas accounting beyond the gasUsed commitment.'); // 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. What this proves: both endpoints serve identical canonical MI commitments ` + `(the same bytes) for every covered block. Independent derivation is NOT proven by this ` + `run alone; it rests on the follower's validate-on-processing behavior, stated in the ` + `honesty field, and on the recorded engine identities.`; 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); });