Merge pool-finish-21 a9de830d into release-0.3.22 (pool/tools/dn3-split-read.mjs, the Devnet 3 read tool for the split's activation; docs and tools only)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
971012757e
1 changed files with 113 additions and 0 deletions
113
pool/tools/dn3-split-read.mjs
Normal file
113
pool/tools/dn3-split-read.mjs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
#!/usr/bin/env node
|
||||
// The Devnet 3 read for the pool split's activation (the node lane's 6.11 condition, 7 October 2026): over a window of
|
||||
// chain blocks on the hub node, the first and last chain block (number, hash, UTC time), the blocks whose coinbase
|
||||
// carries the share chain's commitment (IGNS) and the window's split (IGNP), and the executed state root of the window's
|
||||
// last block on every node named, so the node lane can set `pool_split_activation_daa` at the hour's last block + 7,200.
|
||||
// Reads chain-side facts only (getVirtualChainFromBlock, getBlock with transactions, igneum_getExecStatus and
|
||||
// eth_getBlockByNumber on the exec RPC); never a process name, never a reported rate.
|
||||
//
|
||||
// node pool/tools/dn3-split-read.mjs --hub ws://dn3-g1:28610 --hub-exec http://dn3-g1:26790 \
|
||||
// --node ws://<box-a>:28610=http://<box-a>:26790 --node ws://<box-b>:28610=http://<box-b>:26790 \
|
||||
// --from-daa <first DAA of the hour> [--to-daa <last DAA>] [--out <json file>]
|
||||
//
|
||||
// The hub's chain between the two DAA scores is walked from the pruning point or a block below `--from-daa` through
|
||||
// getVirtualChainFromBlock; every chain block and its mergeset blues are read with transactions; the coinbase payload
|
||||
// (hex) is scanned for the tags. Exec roots: igneum_getExecStatus on every node (executed tip, hash), then
|
||||
// eth_getBlockByNumber at the lowest executed tip across the nodes, stateRoot compared.
|
||||
|
||||
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const opt = (n, d) => { const i = args.indexOf(n); return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : d; };
|
||||
const all = (n) => args.map((a, i) => (a === n ? args[i + 1] : null)).filter(Boolean);
|
||||
const HUB = opt('--hub', 'ws://127.0.0.1:28610');
|
||||
const HUB_EXEC = opt('--hub-exec', 'http://127.0.0.1:26790');
|
||||
const NODES = all('--node').map(s => { const [ws, exec] = s.split('='); return { ws, exec }; });
|
||||
const FROM = +opt('--from-daa', 0);
|
||||
const TO = opt('--to-daa', null) == null ? null : +opt('--to-daa');
|
||||
const OUT = opt('--out', null);
|
||||
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
|
||||
|
||||
let rpcId = 0;
|
||||
async function execCall(url, method, params = []) {
|
||||
const r = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++rpcId, method, params }) });
|
||||
const j = await r.json(); if (j.error) throw new Error(`${method}: ${j.error.message || JSON.stringify(j.error)}`); return j.result;
|
||||
}
|
||||
const hexToBytes = (h) => Buffer.from(h.replace(/^0x/, ''), 'hex');
|
||||
const hasTag = (payloadHex, tag) => hexToBytes(payloadHex).includes(Buffer.from(tag, 'ascii'));
|
||||
// the split's entries, from IGNP || 4 hex count || count x (40 hex address || 8 hex weight) (consensus/core/src/evm.rs)
|
||||
function splitOf(payloadHex) {
|
||||
const b = hexToBytes(payloadHex); const i = b.indexOf(Buffer.from('IGNP', 'ascii')); if (i < 0) return null;
|
||||
const body = b.subarray(i + 4).toString('latin1'); const n = parseInt(body.slice(0, 4), 16); if (!(n > 0)) return null;
|
||||
const out = []; for (let k = 0; k < n; k++) { const e = body.slice(4 + k * 48, 4 + k * 48 + 48); if (e.length < 48) return null; out.push({ address: '0x' + e.slice(0, 40), weight: parseInt(e.slice(40, 48), 16) }); }
|
||||
return out;
|
||||
}
|
||||
|
||||
const hub = await connectRpc(HUB, { attempts: 10, waitMs: 1000 });
|
||||
if (!hub) { console.error(`no RPC at ${HUB}`); process.exit(2); }
|
||||
const info = await hub.call('getBlockDagInfo', {});
|
||||
log(`hub ${HUB}: network ${info.networkName || info.network}, virtual DAA ${info.virtualDaaScore}, pruning point ${String(info.pruningPointHash).slice(0, 16)}`);
|
||||
const toDaa = TO ?? Number(info.virtualDaaScore);
|
||||
// walk the selected chain from the pruning point (the hub's own view), keeping chain blocks within the DAA window
|
||||
let cursor = info.pruningPointHash;
|
||||
const chainBlocks = []; let steps = 0;
|
||||
while (steps++ < 2000) {
|
||||
const r = await hub.call('getVirtualChainFromBlock', { startHash: cursor, includeAcceptedTransactionIds: false });
|
||||
const added = r.addedChainBlockHashes || [];
|
||||
if (!added.length) break;
|
||||
for (const h of added) {
|
||||
const b = (await hub.call('getBlock', { hash: h, includeTransactions: true })).block;
|
||||
const daa = Number(b.header.daaScore);
|
||||
if (daa >= FROM && daa <= toDaa) chainBlocks.push(b);
|
||||
if (daa > toDaa) { steps = 1e9; break; }
|
||||
}
|
||||
cursor = added[added.length - 1];
|
||||
if (added.length < 2) break;
|
||||
}
|
||||
if (!chainBlocks.length) { console.error(`no chain blocks between DAA ${FROM} and ${toDaa} on the hub`); process.exit(3); }
|
||||
// every block the window pays: the chain blocks and their mergeset blues
|
||||
const seen = new Set(); const blocks = [];
|
||||
for (const cb of chainBlocks) {
|
||||
const blues = [cb.verboseData?.hash || cb.header.hash, ...(cb.verboseData?.mergeSetBluesHashes || [])];
|
||||
for (const h of blues) {
|
||||
if (seen.has(h)) continue; seen.add(h);
|
||||
const b = h === (cb.verboseData?.hash || cb.header.hash) ? cb : (await hub.call('getBlock', { hash: h, includeTransactions: true })).block;
|
||||
const cbTx = (b.transactions || [])[0];
|
||||
const payload = cbTx?.payload || '';
|
||||
const split = splitOf(payload);
|
||||
blocks.push({ hash: h, daa: Number(b.header.daaScore), ts: Number(b.header.timestamp), chain: b === cb, igns: hasTag(payload, 'IGNS'), ignp: !!split, igna: hasTag(payload, 'IGNA'), split_entries: split ? split.length : 0, vote_key_hash: b.header.voteKeyHash || b.header.vote_key_hash });
|
||||
}
|
||||
}
|
||||
const first = chainBlocks[0], last = chainBlocks[chainBlocks.length - 1];
|
||||
const iso = (ms) => new Date(Number(ms)).toISOString();
|
||||
const withBoth = blocks.filter(b => b.igns && b.ignp);
|
||||
const onlyOne = blocks.filter(b => b.igns !== b.ignp);
|
||||
log(`window: ${chainBlocks.length} chain blocks, ${blocks.length} blue blocks; first chain block DAA ${first.header.daaScore} ${String(first.verboseData?.hash || first.header.hash).slice(0, 16)} at ${iso(first.header.timestamp)}; last DAA ${last.header.daaScore} ${String(last.verboseData?.hash || last.header.hash).slice(0, 16)} at ${iso(last.header.timestamp)}`);
|
||||
log(`blocks carrying IGNS and IGNP: ${withBoth.length} (${withBoth.filter(b => b.chain).length} chain blocks); one tag without the other: ${onlyOne.length}; distinct vote keys among the split blocks: ${new Set(withBoth.map(b => b.vote_key_hash)).size}; split entries min/max: ${withBoth.length ? Math.min(...withBoth.map(b => b.split_entries)) : 0}/${withBoth.length ? Math.max(...withBoth.map(b => b.split_entries)) : 0}`);
|
||||
// exec roots across the nodes at one executed height
|
||||
const nodes = [{ name: 'hub', ws: HUB, exec: HUB_EXEC }, ...NODES.map((n, i) => ({ name: `node${i + 1}`, ...n }))];
|
||||
const statuses = [];
|
||||
for (const n of nodes) { try { const s = await execCall(n.exec, 'igneum_getExecStatus', []); statuses.push({ name: n.name, exec: n.exec, tip: Number(s.executedTip), tipHash: s.executedTipHash, tipDaa: Number(s.executedTipDaa) }); } catch (e) { statuses.push({ name: n.name, exec: n.exec, error: e.message }); } }
|
||||
const lowest = Math.min(...statuses.filter(s => s.tip != null).map(s => s.tip));
|
||||
const roots = [];
|
||||
for (const s of statuses) {
|
||||
if (s.tip == null) { roots.push({ name: s.name, error: s.error }); continue; }
|
||||
try { const b = await execCall(s.exec, 'eth_getBlockByNumber', ['0x' + lowest.toString(16), false]); roots.push({ name: s.name, number: lowest, hash: b?.hash, stateRoot: b?.stateRoot }); } catch (e) { roots.push({ name: s.name, number: lowest, error: e.message }); }
|
||||
}
|
||||
const distinctRoots = new Set(roots.filter(r => r.stateRoot).map(r => r.stateRoot));
|
||||
log(`exec: ${statuses.map(s => `${s.name} tip ${s.tip ?? 'ERR'}`).join(', ')}; state root at executed block ${lowest} on ${roots.filter(r => r.stateRoot).length} nodes: ${distinctRoots.size === 1 ? 'AGREE ' + [...distinctRoots][0].slice(0, 18) : 'DISAGREE ' + roots.map(r => `${r.name}=${String(r.stateRoot).slice(0, 12)}`).join(' ')}`);
|
||||
const result = {
|
||||
read_at: new Date().toISOString(), hub: HUB, network: info.networkName || info.network, from_daa: FROM, to_daa: toDaa,
|
||||
first_chain_block: { number_daa: Number(first.header.daaScore), blue_score: Number(first.header.blueScore), hash: first.verboseData?.hash || first.header.hash, time: iso(first.header.timestamp) },
|
||||
last_chain_block: { number_daa: Number(last.header.daaScore), blue_score: Number(last.header.blueScore), hash: last.verboseData?.hash || last.header.hash, time: iso(last.header.timestamp) },
|
||||
activation_daa_if_set_now: Number(last.header.daaScore) + 7200,
|
||||
chain_blocks: chainBlocks.length, blue_blocks: blocks.length, blocks_with_igns_and_ignp: withBoth.length, chain_blocks_with_split: withBoth.filter(b => b.chain).length, blocks_with_one_tag_only: onlyOne.length,
|
||||
distinct_vote_keys_in_split_blocks: new Set(withBoth.map(b => b.vote_key_hash)).size,
|
||||
exec: { statuses, roots_at: lowest, roots, agree: distinctRoots.size === 1 },
|
||||
split_blocks: withBoth.map(b => ({ hash: b.hash, daa: b.daa, time: iso(b.ts), chain: b.chain, entries: b.split_entries })),
|
||||
source: 'hub getBlockDagInfo, getVirtualChainFromBlock, getBlock(includeTransactions) coinbase payload tags; every node igneum_getExecStatus and eth_getBlockByNumber at the lowest executed tip',
|
||||
};
|
||||
if (OUT) { writeFileSync(OUT, JSON.stringify(result, null, 2)); log(`written ${OUT}`); }
|
||||
console.log(JSON.stringify({ ...result, split_blocks: result.split_blocks.length }, null, 1));
|
||||
process.exit(0);
|
||||
Loading…
Reference in a new issue