7.2: the pipelined relay (fork bb397f99) at 10 bps and a 3-s propagation delay holds 10.01 blocks/s with the difficulty 7.7 percent over the hash-implied value, the hub taking 7,746 blocks via relay against 4,015 via submit; the same run at a batch of 1 (the 0.3.17 flow) takes 445 against 801 and reads 0.351. N3 closed in the ledger, network.md 5.11 and CLAUDE.md's gate line. The 1 bps, 30-s case did not merge on either flow (3 blocks via relay in 40 minutes, no IBD, no error, one reconnect per spoke): at a 30-s delay the blocks in flight exceed the k bound, and its cause is owed its own reading (7.3). 7.7: the N4 node half's gate (peer-drop.mjs): a still sink dropped its silent peer at 720 s and re-dialled at 750 s; a moving sink kept the same peer through 155 sinks; the harness's failed shape fails. 7.8 and ledger N6: the stale-block nuisance guard (fork 319da644, f2b25fdf, a22cb261), the shipper's record (310 to 551 connections in an hour from four stale datadirs), the two halves and the knob; the gate (nuisance-peer.mjs, a node mining stub-engine blocks with proof of work off and restarting on the shared rule with its datadir): the sender half flat with the skip line, the known-failed shape climbing (3 connections, 2 refusals in 300 s), its mirror failing, the receiver half flat with one refusal and the ten-minute ban. Also recorded: the vote-or-burn harness's devnet finality object (--finality devnet) for the replay running to about 05:00Z. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
145 lines
10 KiB
JavaScript
145 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
// Ledger N4, the node half (fork 96619b7d): a peer that delivered no block for PEER_STALL (600 s) while our own sink stood
|
|
// still over the same span is dropped by the ping loop, so the connection manager dials another. The record: a
|
|
// desktop node kept one same-digest peer for 53 minutes that relayed nothing, its tip frozen the whole time.
|
|
//
|
|
// --case still node A with NO miner (its sink stands still at genesis), one peer B (addpeer) that mines nothing and
|
|
// relays nothing. PASS = A's log carries `P2P, peer ...: no block from this peer for N s while our sink
|
|
// stood at ... (ledger N4); dropping it to dial another peer` within PEER_STALL + one ping interval
|
|
// (600 + 120 s, read with 60 s of margin: 780 s), and A's connection count drops and comes back (the
|
|
// addpeer is re-dialled). `--expect drop`.
|
|
// --case moving node A WITH a CPU miner (its sink moves), the same silent peer B, watched for the same 780 s.
|
|
// PASS = no drop line, the peer stays connected throughout. `--expect quiet`. `--case moving --expect drop`
|
|
// is the harness's own failed shape and must FAIL.
|
|
//
|
|
// node infra/fast-time/peer-drop.mjs --case still|moving [--expect drop|quiet] [--secs 780]
|
|
// IGNEUMD, IGNEUM_MINER name the binaries (defaults: vendor/igneum-node-0316/target-0316/release).
|
|
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
|
|
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
|
|
|
|
const ROOT = new URL('../../', import.meta.url).pathname;
|
|
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
|
|
const BIN = process.env.IGNEUM_0316_BIN || `${ROOT}vendor/igneum-node-0316/target-0316/release`;
|
|
const IGNEUMD = process.env.IGNEUMD || `${BIN}/igneumd`;
|
|
const CPU_MINER = process.env.IGNEUM_MINER || `${BIN}/igneum-miner`;
|
|
const TMP = '/tmp/igneum-fast-time-pdrop';
|
|
const BASE = 30390, SUFFIX = 993; // 30390 to 30402: clear of every other fast-time harness
|
|
const NEVER = '18446744073709551615';
|
|
const args = process.argv.slice(2);
|
|
const flag = (name, dflt) => { const i = args.indexOf(`--${name}`); return i >= 0 ? Number(args[i + 1]) : dflt; };
|
|
const sflag = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : null; };
|
|
const CASE = sflag('case') || 'still';
|
|
const EXPECT = sflag('expect') || (CASE === 'still' ? 'drop' : 'quiet');
|
|
const SECS = flag('secs', 780);
|
|
const PEER_STALL = 600, PING_INTERVAL = 120;
|
|
const OUT = sflag('out') || `${ROOT}docs/plans/counter-asic-3-gate/peer-drop-${CASE}-expect-${EXPECT}.json`;
|
|
if (!['still', 'moving'].includes(CASE) || !['drop', 'quiet'].includes(EXPECT)) { console.error('usage: --case still|moving [--expect drop|quiet]'); process.exit(2); }
|
|
const started = [];
|
|
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
|
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
for (const b of [IGNEUMD, CPU_MINER]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
|
|
try { spawnSync('pkill', ['-f', `devnet-suffix=${SUFFIX}`]); } catch { }
|
|
await sleep(1500);
|
|
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
|
|
|
const baseText = readFileSync(FILE, 'utf8');
|
|
const field = (name) => { const m = new RegExp(`"${name}":\\s*([0-9]+)`).exec(baseText); return m ? +m[1] : undefined; };
|
|
export function mergeOverrideText(text, fields) {
|
|
let out = text;
|
|
for (const k of Object.keys(fields)) out = out.replace(new RegExp(`\\s*"${k}":\\s*[^,}\\n]+,?`), '');
|
|
const extra = Object.entries(fields).map(([k, v]) => `"${k}": ${typeof v === 'string' && !/^\d+$/.test(v) ? JSON.stringify(v) : v}`).join(', ');
|
|
return out.replace(/,?\s*}\s*$/, `,\n ${extra}\n}\n`);
|
|
}
|
|
const DAY_MS = field('pow_day_ms');
|
|
const override = `${TMP}/override.json`;
|
|
writeFileSync(override, mergeOverrideText(baseText, { genesis_bits: 0x1f010000, skip_proof_of_work: false, program_class_v3_activation_daa: NEVER, program_class_v4_activation_daa: NEVER }));
|
|
log(`case ${CASE}: expect ${EXPECT}, PEER_STALL ${PEER_STALL} s, ping interval ${PING_INTERVAL} s, watch ${SECS} s`);
|
|
|
|
class Node {
|
|
constructor(i, connect = null) {
|
|
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2;
|
|
this.connect = connect; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`;
|
|
}
|
|
get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; }
|
|
async start() {
|
|
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=${override}`, '--loglevel=info', '--yes'];
|
|
// --addpeer: a dropped peer is dialled again, which is the re-dial the case watches for
|
|
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] });
|
|
started.push(this.proc);
|
|
await sleep(1500);
|
|
if (this.proc.exitCode != null) throw new Error(`n${this.i} exited ${this.proc.exitCode}: ${this.grepLog(/ERROR|Error|error|refused|invalid/).slice(-3).join(' | ')}`);
|
|
for (let i = 0; i < 20; i++) {
|
|
try { this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`); await this.rpc.call('getBlockDagInfo'); break; } catch { this.rpc = null; await sleep(500); }
|
|
}
|
|
if (!this.rpc) throw new Error(`n${this.i}: the RPC did not answer within 10 s`);
|
|
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} p2p ${this.p2pPort}${this.connect ? ` addpeer ${this.connect}` : ''}`);
|
|
return this;
|
|
}
|
|
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
|
|
async peers() { try { const r = await this.rpc.call('getConnectedPeerInfo'); const arr = Object.values(r).find(Array.isArray); return arr ? arr.length : null; } catch { return null; } }
|
|
}
|
|
function miner(name, grpc) {
|
|
const out = openSync(`${TMP}/${name}.log`, 'a');
|
|
const p = spawn(CPU_MINER, ['mine', grpc, '1', String(SECS + 60), name, '--engine', 'igneum-pow', '--payout-label', name, '--status-secs', '60', '--no-vote', '--stall-secs', '0'], { stdio: ['ignore', out, out], env: { ...process.env, IGNEUM_POW_DAY_MS: String(DAY_MS) } });
|
|
started.push(p);
|
|
return p;
|
|
}
|
|
async function stopAll() {
|
|
for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch { } }
|
|
await sleep(1500);
|
|
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
|
|
}
|
|
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
|
|
process.on('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
|
|
|
|
// B first (the silent peer, no miner), then A dialling it
|
|
const b = await new Node(1).start();
|
|
const a = await new Node(0, b.p2pPort).start();
|
|
if (CASE === 'moving') miner('mover', a.grpc);
|
|
const t0 = Date.now();
|
|
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
|
|
const samples = [];
|
|
let dropAt = null, redialAt = null, minPeers = null, lastReport = 0;
|
|
const DROP_RE = /no block from this peer for \d+ s while our sink stood at \S+ for \d+ s \(ledger N4\); dropping it/;
|
|
while (Date.now() - t0 < SECS * 1000) {
|
|
await sleep(5000);
|
|
const peers = await a.peers();
|
|
const d = await a.rpc.call('getBlockDagInfo').catch(() => null);
|
|
const drops = a.grepLog(DROP_RE);
|
|
const connects = a.grepLog(/P2P Connected to outgoing peer/).length;
|
|
if (drops.length && dropAt == null) { dropAt = +since(); log(`DROP at ${since()} s: ${drops[0].replace(/^.*?P2P, /, 'P2P, ').slice(0, 220)}`); }
|
|
if (dropAt != null && redialAt == null && connects >= 2) { redialAt = +since(); log(`re-dialled at ${since()} s (${connects} outgoing connections so far)`); }
|
|
if (peers != null) minPeers = minPeers == null ? peers : Math.min(minPeers, peers);
|
|
samples.push({ t: +since(), peers, blocks: d?.blockCount ?? null, sink: d ? String(d.sink).slice(0, 8) : null, drops: drops.length, connects });
|
|
if (Date.now() - lastReport > 60000) { lastReport = Date.now(); log(`t=${since()} s A peers ${peers} blocks ${d?.blockCount} sink ${samples.at(-1).sink} drops ${drops.length} outgoing connections ${connects}`); }
|
|
}
|
|
const drops = a.grepLog(DROP_RE);
|
|
const connects = a.grepLog(/P2P Connected to outgoing peer/).length;
|
|
const sinks = new Set(samples.map(s => s.sink).filter(Boolean));
|
|
const checks = {
|
|
drop_line_on_a: drops.length > 0,
|
|
drop_within_stall_plus_ping: dropAt != null && dropAt <= PEER_STALL + PING_INTERVAL + 60,
|
|
redialled_after_drop: redialAt != null,
|
|
sink_stood_still: sinks.size <= 1,
|
|
peer_kept_throughout: drops.length === 0 && samples.every(s => s.peers == null || s.peers >= 1),
|
|
sink_moved: sinks.size > 5,
|
|
};
|
|
const good = EXPECT === 'drop'
|
|
? checks.drop_line_on_a && checks.drop_within_stall_plus_ping && checks.redialled_after_drop
|
|
: checks.peer_kept_throughout;
|
|
const summary = { pass: good, case: CASE, expect: EXPECT, secs: SECS, peer_stall: PEER_STALL, ping_interval: PING_INTERVAL, drop_at_s: dropAt, redial_at_s: redialAt, drop_lines: drops.length, drop_example: drops[0]?.replace(/^.*?P2P, /, 'P2P, ').slice(0, 300) ?? null, outgoing_connections: connects, min_peers: minPeers, distinct_sinks: sinks.size, checks, samples, node: IGNEUMD, miner: CPU_MINER };
|
|
mkdirSync(OUT.replace(/\/[^/]+$/, ''), { recursive: true });
|
|
writeFileSync(OUT, JSON.stringify(summary, null, 2));
|
|
const judged = EXPECT === 'drop' ? ['drop_line_on_a', 'drop_within_stall_plus_ping', 'redialled_after_drop'] : ['peer_kept_throughout'];
|
|
const fails = judged.filter(k => !checks[k]);
|
|
log(`SUMMARY ${good ? 'PASS' : 'FAIL'} (case ${CASE}, expect ${EXPECT}): ${drops.length} drop lines${dropAt != null ? ` (first at ${dropAt} s)` : ''}, ${redialAt != null ? `re-dialled at ${redialAt} s` : 'no re-dial'}, ${connects} outgoing connections, min peers ${minPeers}, ${sinks.size} distinct sinks over ${SECS} s${fails.length ? `; FAILED CHECK ${fails.join(', ')}` : ''}`);
|
|
log(`summary: ${OUT}`);
|
|
await stopAll();
|
|
process.exit(good ? 0 : 1);
|