diff --git a/infra/fast-time/vote-or-burn.mjs b/infra/fast-time/vote-or-burn.mjs index 733e8470e..378c21fd8 100644 --- a/infra/fast-time/vote-or-burn.mjs +++ b/infra/fast-time/vote-or-burn.mjs @@ -98,7 +98,7 @@ log(`case ${CASE}: finality ${FINALITY}, rule ${RULE}, expect ${EXPECT} (produce class Node { constructor(i, connect = null, overrideFile = override) { - this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2; + this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2; this.evmPort = BASE + i * 10 + 3; this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`; this.overrideFile = overrideFile; } get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; } @@ -106,7 +106,7 @@ class Node { mkdirSync(this.dir, { recursive: true }); const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex', `--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`, - `--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${this.overrideFile}`, '--loglevel=info', '--yes']; + `--listen=127.0.0.1:${this.p2pPort}`, `--evm-rpclisten=127.0.0.1:${this.evmPort}`, `--override-params-file=${this.overrideFile}`, '--loglevel=info', '--yes']; if (this.connect) a.push(`--addpeer=127.0.0.1:${this.connect}`); else a.push('--outpeers=0'); const out = openSync(this.logFile, 'a'); this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out] }); @@ -171,7 +171,7 @@ for (let i = 0; i < TAIL; i++) { const parentSubsidy = +(parent.verboseData?.transactions?.[0]?.payload ? 0 : 0); void parentSubsidy; if (outs.length >= 2) { - const row = { daa: +blk.header.daaScore, producer: outs[0], pool: outs[1], parent_daa: +parent.header.daaScore }; + const row = { daa: +blk.header.daaScore, producer: outs[0], pool: outs[1], parent_daa: +parent.header.daaScore, chain_block: cur, parent_key: pkey }; if (pkey === keyA) pairs.a.push(row); else if (pkey === keyB) pairs.b.push(row); lastDaa = lastDaa ?? row.daa; firstDaa = row.daa; } @@ -207,6 +207,38 @@ if (REPLAY) { log(`replay: ${synced ? `C synced in ${replay.seconds_to_sync} s` : 'C did NOT sync'}: C ${replay.c_blocks} blocks sink ${replay.c_sink}, A ${replay.a_blocks} sink ${replay.a_sink}; ${ruleErrors.length} rule error lines on C; ${refused} digest refusal lines`); } +// The bridge identity (ledger N7, the testnet lane's finding, 7 October 2026): for every pair row the execution layer's +// record of the merging chain block credits the paid block's miner exactly what the UTXO coinbase paid it (sompi times +// 10^10 wei; the harness's blocks carry no fees). The miner's EVM address is learnt from a chain block its key built +// (block_json's "miner"). With the bonus on, a silent key's credit must be the 72 percent on both ledgers. +const evmCall = async (port, method, params) => { + const r = await fetch(`http://127.0.0.1:${port}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) }); + return (await r.json()).result; +}; +const WEI_PER_SOMPI = 10n ** 10n; +const bridge = { rows: 0, agree: 0, disagree: [], address: {} }; +try { + const allRows = [...pairs.a, ...pairs.b]; + // the EVM address of each key: the "miner" of a chain block built by that key + for (const row of allRows) { + const blk = await evmCall(a.evmPort, 'eth_getBlockByHash', [`0x${row.chain_block}`, false]); + if (!blk) continue; + const bkey = String((await a.rpc.call('getBlock', { hash: row.chain_block, includeTransactions: false })).block.header.voteKeyHash); + if (blk.miner && !bridge.address[bkey]) bridge.address[bkey] = String(blk.miner).toLowerCase(); + } + for (const row of allRows) { + const blk = await evmCall(a.evmPort, 'eth_getBlockByHash', [`0x${row.chain_block}`, false]); + const addr = bridge.address[row.parent_key]; + if (!blk || !addr) continue; + const entry = (blk.igneum?.rewards || []).find(e => String(e.miner).toLowerCase() === addr); + if (!entry) continue; + bridge.rows++; + const wei = BigInt(entry.wei), want = BigInt(row.producer) * WEI_PER_SOMPI; + if (wei === want) bridge.agree++; else bridge.disagree.push({ chain_block: row.chain_block.slice(0, 16), utxo_sompi: row.producer, evm_wei: wei.toString(), want_wei: want.toString() }); + } +} catch (e) { bridge.error = String(e); } +log(`bridge: ${bridge.agree} of ${bridge.rows} pair rows credit the paid block's miner on the EVM side exactly what the UTXO coinbase paid${bridge.disagree.length ? `; first disagreement ${JSON.stringify(bridge.disagree[0])}` : ''}${bridge.error ? `; error ${bridge.error}` : ''}`); + const median = (xs) => { if (!xs.length) return null; const s = [...xs].sort((x, y) => x - y); return s[Math.floor(s.length / 2)]; }; const medA = median(pairs.a.map(r => r.producer)), medB = median(pairs.b.map(r => r.producer)); const poolA = median(pairs.a.map(r => r.pool)), poolB = median(pairs.b.map(r => r.pool)); @@ -219,13 +251,15 @@ const checks = { producer_ratio_matches: ratio != null && Math.abs(ratio - want.producer) <= TOL, pool_reading_matches: poolReading != null && Math.abs(poolReading - want.pool_over_full_producer) <= TOL, a_pool_is_a_quarter: poolA != null && medA && Math.abs(poolA / medA - 0.25) <= TOL, + // ledger N7: both ledgers agree on every pair row (A's and B's blocks alike; with the bonus on, B's silent 72 percent) + bridge_identity_holds: bridge.rows >= PAIRS && bridge.disagree.length === 0 && !bridge.error, }; if (REPLAY) { checks.replay_late_node_holds_every_block = !!replay?.synced; checks.replay_no_coinbase_rule_error = (replay?.rule_error_lines ?? 1) === 0; } const pass = Object.values(checks).every(Boolean); -const summary = { pass, expect: EXPECT, rule: RULE, case: CASE, replay, secs: SECS, tail: TAIL, keys: { a: keyA, b: keyB }, pairs_a: pairs.a.length, pairs_b: pairs.b.length, skipped_wide_mergesets: skippedWide, daa_range: [firstDaa, lastDaa], median_producer: { a: medA, b: medB }, median_pool: { a: poolA, b: poolB }, producer_ratio_b_over_a: ratio, pool_reading_b: poolReading, wanted: want, checks, rows: pairs, node: IGNEUMD, miner: CPU_MINER }; +const summary = { bridge: { rows: bridge.rows, agree: bridge.agree, disagree: bridge.disagree.slice(0, 5), error: bridge.error || null }, pass, expect: EXPECT, rule: RULE, case: CASE, replay, secs: SECS, tail: TAIL, keys: { a: keyA, b: keyB }, pairs_a: pairs.a.length, pairs_b: pairs.b.length, skipped_wide_mergesets: skippedWide, daa_range: [firstDaa, lastDaa], median_producer: { a: medA, b: medB }, median_pool: { a: poolA, b: poolB }, producer_ratio_b_over_a: ratio, pool_reading_b: poolReading, wanted: want, checks, rows: pairs, node: IGNEUMD, miner: CPU_MINER }; mkdirSync(OUT.replace(/\/[^/]+$/, ''), { recursive: true }); writeFileSync(OUT, JSON.stringify(summary, null, 2)); const fails = Object.entries(checks).filter(([, v]) => !v).map(([k]) => k);