tools/finality-attacks: run.mjs drives a private igneum-devnet-800 network (ports 27800+, /tmp/igneum-fin-attacks, skip_proof_of_work) with the test-only hostile flags of igneum-miner (vmine, --equivocate, --sybil, --drop-votes, --pulse, fin-rpc-attack; worktree fin-attacks on master c6d47547..2a00ff55). Seven scenarios in priority order 3,2,1,6,4,8,5 with a spec 03 criterion and a measured result each; README carries the catalogue, what needs a finality-aware p2p probe, and a proposed diff for every FAIL. Results (six voters): S3 dishonest aggregators PASS (35/35/35 locks, 0 conflicts, 1,018 ms); S2 Sybil dust: weights PASS, aggregator sortition FAIL (per key, ledger F17); S1 equivocation PASS (2/2/2 stripped, 0 conflicts); S6A 3/3 partition FAIL (floor is time-bounded, one side crossed 56.7% at 84 s of a 90 s split, T* = 2F/13R, 9.2 days for a 50/50 split at mainnet scale); S6B 4/2 PASS; S4 vote-dropping producer PASS (0 ms added); S8 malformed votes over RPC PASS (9 cases, no crash); S5 pulse: no retarget amplification (ratio 0.999) PASS, lock-alone FAIL (a 20 s burst locked checkpoints 1 to 10 alone on a young window, ledger F1, spec 3.8 not implemented). S7 eclipse not run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
54 lines
2.6 KiB
JavaScript
54 lines
2.6 KiB
JavaScript
// wRPC JSON client for igneumd. Node 22, no dependencies. Method names are the lowerCamelCase of the node's
|
|
// RpcApiOps (getBlockDagInfo, getFinalityCheckpoints, getFinalityWeights, submitFinalityVote, ...).
|
|
|
|
export class Rpc {
|
|
constructor(url, { timeoutMs = 10_000 } = {}) {
|
|
this.url = url; this.id = 0; this.pending = new Map(); this.ws = null; this.open = false;
|
|
this.timeoutMs = timeoutMs; this.onNotification = () => { };
|
|
}
|
|
connect() {
|
|
return new Promise((resolve) => {
|
|
const ws = new WebSocket(this.url); this.ws = ws;
|
|
ws.onopen = () => { this.open = true; resolve(true); };
|
|
ws.onmessage = (e) => {
|
|
let m; try { m = JSON.parse(e.data); } catch { return; }
|
|
if (m.id !== undefined && m.id !== null && this.pending.has(m.id)) {
|
|
const p = this.pending.get(m.id); this.pending.delete(m.id);
|
|
m.error ? p.reject(new Error(typeof m.error === 'string' ? m.error : (m.error.message || JSON.stringify(m.error)))) : p.resolve(m.params);
|
|
} else if (m.method) this.onNotification(m.method, m.params);
|
|
};
|
|
ws.onerror = () => { if (!this.open) resolve(false); };
|
|
ws.onclose = () => {
|
|
const wasOpen = this.open; this.open = false;
|
|
for (const p of this.pending.values()) p.reject(new Error('rpc closed'));
|
|
this.pending.clear();
|
|
if (!wasOpen) resolve(false);
|
|
};
|
|
setTimeout(() => { if (!this.open) { try { ws.close(); } catch { } resolve(false); } }, 3000);
|
|
});
|
|
}
|
|
close() { try { this.ws && this.ws.close(); } catch { } }
|
|
call(method, params = {}, timeoutMs = this.timeoutMs) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!this.open) return reject(new Error('rpc not connected'));
|
|
const id = ++this.id; this.pending.set(id, { resolve, reject });
|
|
this.ws.send(JSON.stringify({ id, method, params }));
|
|
setTimeout(() => { if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(`${method} timed out`)); } }, timeoutMs);
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function connectRpc(url, { attempts = 60, waitMs = 500 } = {}) {
|
|
for (let i = 0; i < attempts; i++) {
|
|
const rpc = new Rpc(url);
|
|
if (await rpc.connect()) {
|
|
try { await rpc.call('getInfo'); return rpc; } catch { rpc.close(); }
|
|
}
|
|
await new Promise(r => setTimeout(r, waitMs));
|
|
}
|
|
throw new Error(`could not reach ${url}`);
|
|
}
|
|
|
|
// Convenience readers (camelCase fields from the node's RPC model).
|
|
export async function checkpoints(rpc, last = 40) { return rpc.call('getFinalityCheckpoints', { last }); }
|
|
export async function weights(rpc) { return rpc.call('getFinalityWeights', {}); }
|