#!/usr/bin/env node 'use strict'; /* * claim-chain-detector.mjs - Execution Kernel Stage 2: the claim-chain gap detector. * * The kernel treats execution proofs as an audit layer OVER the chain, never under it * (finality stays QBFT plus Falcon). Each ExecutionClaim covers a window [h1,h2] and carries * prevStateRoot, postStateRoot, txRoot, vkey, proof. For the layer to mean anything, three * properties must hold continuously, and this tool measures all three from the live chain: * * 1. LINKAGE - claim[n].prevStateRoot == claim[n-1].postStateRoot, and the first claim's * prevStateRoot equals the chain's state root at h1-1. No forged jump. * 2. NO GAPS - windows tile the covered range with no hole and no overlap. * 3. ANCHORING - each claim's postStateRoot equals the header.stateRoot the chain published * at that height. The proof proves the state the real chain committed, not a * parallel state. * * And it reports LAG as a first-class metric: how far the newest claim trails the chain head. * Lag is not a failure, it is the cost curve of proving. Hiding it is the failure. * * There is no on-chain proof registry with real traffic yet (aggregator V1 is deprecated and * fenced), so claims are read from a claims file (--claims path) whose shape mirrors the * on-chain ProvenBlock record. When a public claim endpoint exists, point --claims at it. * A SYNTHETIC mode (--synthesize N) builds a correct claim chain of N windows from the live * header state roots, so the detector's own logic can be exercised, and NEGATIVE CONTROLS * (--inject) plant exactly one defect and require the detector to go red. * * Read-only. Only eth_getBlockByNumber / eth_blockNumber against a public RPC. * * node claim-chain-detector.mjs --synthesize 8 --window 4 * node claim-chain-detector.mjs --synthesize 8 --window 4 --inject gap * node claim-chain-detector.mjs --synthesize 8 --window 4 --inject linkage * node claim-chain-detector.mjs --synthesize 8 --window 4 --inject anchor * node claim-chain-detector.mjs --claims claims.json */ import fs from 'node:fs'; const RPC = process.env.AERE_RPC || 'https://rpc.aere.network'; const args = process.argv.slice(2); const opt = (name, def) => { const i = args.indexOf(name); return i >= 0 && i + 1 < args.length ? args[i + 1] : def; }; const WINDOW = parseInt(opt('--window', '4'), 10); const SYNTH = opt('--synthesize', null); const CLAIMS_FILE = opt('--claims', null); const INJECT = opt('--inject', null); const SAFETY = parseInt(opt('--safety', '6'), 10); let rpcCalls = 0; async function rpc(method, params) { rpcCalls++; const r = await fetch(RPC, { method: 'POST', headers: { 'content-type': 'application/json', connection: 'close' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: AbortSignal.timeout(20000) }); if (!r.ok) throw new Error(method + ' -> HTTP ' + r.status); const j = await r.json(); if (j.error) throw new Error(method + ' -> ' + JSON.stringify(j.error)); return j.result; } const stateRootAt = async (h) => { const b = await rpc('eth_getBlockByNumber', ['0x' + h.toString(16), false]); return b ? b.stateRoot : null; }; // Build a correct claim chain of `count` windows of `WINDOW` blocks each, ending near head. async function synthesizeChain(count) { const head = parseInt(await rpc('eth_blockNumber', []), 16) - SAFETY; const h2 = head; const h1 = h2 - count * WINDOW + 1; const claims = []; for (let w = 0; w < count; w++) { const a = h1 + w * WINDOW; const b = a + WINDOW - 1; claims.push({ h1: a, h2: b, prevStateRoot: await stateRootAt(a - 1), postStateRoot: await stateRootAt(b), chainId: 2800, }); } return { firstPrevAt: h1 - 1, firstPrev: await stateRootAt(h1 - 1), claims }; } function plant(chain, kind) { const c = chain.claims; const mid = Math.floor(c.length / 2); if (kind === 'gap') { c[mid].h1 += 1; return 'window ' + mid + ' start shifted +1 (a one-block hole appears before it)'; } if (kind === 'overlap') { c[mid].h1 -= 1; return 'window ' + mid + ' start shifted -1 (it overlaps the previous window)'; } if (kind === 'linkage') { c[mid].prevStateRoot = '0x' + 'ba'.repeat(32); return 'window ' + mid + ' prevStateRoot corrupted (chain link broken)'; } if (kind === 'anchor') { // corrupt the LAST window's postStateRoot: no next claim links to it, so this is caught // ONLY by the anchoring check (postStateRoot == header.stateRoot), not by linkage. const last = c.length - 1; c[last].postStateRoot = '0x' + 'ad'.repeat(32); return 'last window postStateRoot != header.stateRoot (proves a parallel state, isolated from linkage)'; } throw new Error('unknown inject: ' + kind); } (async () => { console.log('Execution Kernel Stage 2: claim-chain gap detector'); console.log('RPC ' + RPC + ', window ' + WINDOW + '\n'); let chain; if (SYNTH) chain = await synthesizeChain(parseInt(SYNTH, 10)); else if (CLAIMS_FILE) chain = JSON.parse(fs.readFileSync(CLAIMS_FILE, 'utf8')); else { console.error('give --synthesize N or --claims path'); process.exit(2); } let plantedNote = null; if (INJECT) { plantedNote = plant(chain, INJECT); console.log('NEGATIVE CONTROL: planted ' + INJECT + ' -> ' + plantedNote + '\n'); } const problems = []; const c = chain.claims; // 1. linkage: first claim vs chain, then claim to claim if (chain.firstPrev && c[0].prevStateRoot !== chain.firstPrev) problems.push('LINKAGE: first claim prevStateRoot != chain state root at ' + chain.firstPrevAt); for (let i = 1; i < c.length; i++) if (c[i].prevStateRoot !== c[i - 1].postStateRoot) problems.push('LINKAGE: claim[' + i + '].prevStateRoot != claim[' + (i - 1) + '].postStateRoot'); // 2. gaps / overlaps for (let i = 1; i < c.length; i++) { if (c[i].h1 > c[i - 1].h2 + 1) problems.push('GAP: hole between block ' + c[i - 1].h2 + ' and ' + c[i].h1); if (c[i].h1 <= c[i - 1].h2) problems.push('OVERLAP: window ' + i + ' starts at ' + c[i].h1 + ', inside window ' + (i - 1)); } // 3. anchoring: each postStateRoot == header.stateRoot at h2 for (let i = 0; i < c.length; i++) { const real = await stateRootAt(c[i].h2); if (real && c[i].postStateRoot !== real) problems.push('ANCHOR: claim[' + i + '] postStateRoot != header.stateRoot at ' + c[i].h2); } // lag as a first-class metric const head = parseInt(await rpc('eth_blockNumber', []), 16); const newest = c[c.length - 1].h2; const lag = head - newest; console.log('claims: ' + c.length + ' windows, covering blocks ' + c[0].h1 + '..' + newest); console.log('LAG (first-class metric): newest claim trails head by ' + lag + ' blocks (' + (lag * 0.53).toFixed(1) + ' s at 0.53 s/block)'); console.log('RPC calls: ' + rpcCalls + '\n'); if (INJECT) { if (problems.length) { console.log('NEGATIVE CONTROL PASSED: detector went red -> ' + problems[0]); process.exit(0); } console.log('NEGATIVE CONTROL FAILED: the planted ' + INJECT + ' was NOT caught. The detector is theatre.'); process.exit(1); } if (problems.length) { console.log('RESULT: BROKEN CLAIM CHAIN'); problems.forEach((p) => console.log(' * ' + p)); process.exit(1); } console.log('RESULT: claim chain is continuous, linked, and anchored across ' + c.length + ' windows. Lag published above.'); process.exit(0); })().catch((e) => { console.error('ERROR: ' + e.message); process.exit(2); });