fast-time: the vote-or-burn harness checks the bridge identity per pair row (ledger N7: the exec record of the merging chain block credits the paid block's miner what the UTXO coinbase paid; --evm-rpclisten per node)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 08:10:02 +00:00
parent 5b4f76b64f
commit 05526a6e88

View file

@ -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);