Counter ASIC 3.0 gates (node): the 0.3.15 digest rule's compat and refusal cases on real nodes (digest-compat.mjs: a fixed node and a 0.3.14 node on the thirteen-field file peer on one digest; the sixteen-field node is refused), the P3 row, the sixteen-field digest 1dddfa55... on the devnet id
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
1cf76c0f8a
commit
58fdfda87a
3 changed files with 150 additions and 0 deletions
64
docs/plans/counter-asic-3-gate/digest-compat-20261006.json
Normal file
64
docs/plans/counter-asic-3-gate/digest-compat-20261006.json
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"pass": true,
|
||||
"checks": {
|
||||
"new_and_old_on_13_fields_print_one_digest": true,
|
||||
"the_16_field_digest_differs": true,
|
||||
"n0_peers_with_n1_and_n2": true,
|
||||
"old_node_peers_with_new": true,
|
||||
"n3_has_no_peer": true,
|
||||
"a_digest_refusal_line_exists": true
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"node": "n0",
|
||||
"binary": "new",
|
||||
"fields": 13,
|
||||
"digest": "a89be8a7e64b7d5a0d89d19c8837b3a8c2323fc277717e94c508eeb51a5e71e6",
|
||||
"peers": 2,
|
||||
"refusal": [
|
||||
"Application directory: /tmp/igneum-digest-compat/n0",
|
||||
"Data directory: /tmp/igneum-digest-compat/n0/igneum-devnet-995/datadir"
|
||||
]
|
||||
},
|
||||
{
|
||||
"node": "n1",
|
||||
"binary": "new",
|
||||
"fields": 13,
|
||||
"digest": "a89be8a7e64b7d5a0d89d19c8837b3a8c2323fc277717e94c508eeb51a5e71e6",
|
||||
"peers": 1,
|
||||
"refusal": [
|
||||
"Application directory: /tmp/igneum-digest-compat/n1",
|
||||
"Data directory: /tmp/igneum-digest-compat/n1/igneum-devnet-995/datadir"
|
||||
]
|
||||
},
|
||||
{
|
||||
"node": "n2",
|
||||
"binary": "old",
|
||||
"fields": 13,
|
||||
"digest": "a89be8a7e64b7d5a0d89d19c8837b3a8c2323fc277717e94c508eeb51a5e71e6",
|
||||
"peers": 1,
|
||||
"refusal": [
|
||||
"Application directory: /tmp/igneum-digest-compat/n2",
|
||||
"Data directory: /tmp/igneum-digest-compat/n2/igneum-devnet-995/datadir"
|
||||
]
|
||||
},
|
||||
{
|
||||
"node": "n3",
|
||||
"binary": "new",
|
||||
"fields": 16,
|
||||
"digest": "db9a85f9ff08f72f11f7ba496b3060189c32bdf6cb53e94c810a4cc25585487a",
|
||||
"peers": 0,
|
||||
"refusal": [
|
||||
"Application directory: /tmp/igneum-digest-compat/n3",
|
||||
"Data directory: /tmp/igneum-digest-compat/n3/igneum-devnet-995/datadir"
|
||||
]
|
||||
}
|
||||
],
|
||||
"new_binary": "/Users/joshm/Projects/igneum-wt-ca3-v4-node/vendor/igneum-node-0315fix/../igneum-node-ca3v4/target-ca3v4/release/igneumd",
|
||||
"old_binary": "/Users/joshm/Projects/igneum/vendor/igneum-node/target-0314/release/igneumd",
|
||||
"secs": 45,
|
||||
"refusal_lines_rechecked": [
|
||||
"Refusing peer 127.0.0.1:51123: consensus params digest mismatch, local a89be8a7e64b7d5a0d89d19c8837b3a8c2323fc277717e94c508eeb51a5e71e6 remote db9a85f9ff08f72f11f7ba496b3060189c32bdf6cb53e94c810a4cc25585487a (the peer's override file, environment or build differs)",
|
||||
"Refusing peer 127.0.0.1:51393: consensus params digest mismatch, local a89be8a7e64b7d5a0d89d19c8837b3a8c2323fc277717e94c508eeb51a5e71e6 remote db9a85f9ff08f72f11f7ba496b3060189c32bdf6cb53e94c810a4cc25585487a (the peer's override file, environment or build differs)"
|
||||
]
|
||||
}
|
||||
|
|
@ -17,4 +17,6 @@
|
|||
| P2 | miner-signalled class activation, implemented behind the override, the fast-time gate with three cases and the failed case | `docs/plans/counter-asic-3-node.md` section 6; `infra/fast-time/class-v4-signal.mjs` | GREEN on the Mac: two of three signalling never flips (7 epochs, 6,166 to 7,583 bps), all three flips at epoch 3 (the first full window, 10,000 bps, the same line on 3 of 3 nodes, the id assertion), nobody signalling flips at the floor epoch 5 and not before; the known-failed case fails on eight checks. Rows and summary files: node doc section 6.4; `class-v4-signal-{no-flip,flip,floor,failed-case}.json` |
|
||||
| G6 (signalling) | the fork change on PC 2 with the igneum-pow feature | the job ids and their lines | GREEN on fork 0562a7f2 (main f39a8eb), three PC 2 jobs under one clearance ("PC 2 open for G6", 17:27Z), the mkdir lock taken 17:28:15Z and released 17:38:58Z, then 17:39:41Z to 17:55:31Z; prover on, app untouched. Job 1 `build-20261006-173017` (the six crates and the app): every build stage ok, the app tests 112 + 26 + 8, but kaspa-consensus 97 passed and 1 FAILED on 2.0's known flake `ban_is_decided_by_the_carrying_block_so_nodes_agree_on_every_voter_list` (`UnexpectedDifficulty(.., 487111630, 487128802)` in `mine_on_all`, the 0.3.10 cut's section 11 case, which also passed on the Mac and in this morning's job 155958), and cargo stopped the unit there. Treated as 2.0 did: job 2 `build-20261006-174027` (kaspa-consensus alone, 17:40 to 17:46Z): `RESULT test node [kaspa-consensus] exit 0 19 s`, `done exit 0 after 345 s ... every stage ok`. Job 3 `build-20261006-174823` (the five crates and the app, 17:48 to 17:55Z): `RESULT test node [kaspa-consensus-core igneum-exec kaspa-pow igneum-miner kaspa-p2p-flows] exit 0 35 s` and `RESULT test app/igneum-app [igneum-app] exit 0 5 s`: kaspa-consensus-core 110 + 7 (`override_params_carry_the_program_class_v4_activation ... ok`, the signal window test inside it), igneum-exec 18, kaspa-pow 15 (`program_class_signal_rule ... ok` is consensus-core's; kaspa-pow's `program_class_v4_seeds_hash_the_shadow_program_over_the_v3_day_cache ... ok`, the feature on), igneum-miner 18, kaspa-p2p-flows 33, igneum-app 112 + 26 + 8; the job's own closing line reads `failed ... upload incomplete` because the relay's blob service refused one of the nine uploads (`igneum-app.exe.zst FAILED: blob PUT: no url in the reply: service_unavailable`, the shipper's 17:25Z fault) AFTER both test stages had closed with exit 0, so the STAGE and RESULT lines are the evidence, as the shipper's warning said. Where the runs went: these three on PC 2 under the clearance given before the build-server rule arrived (18:0xZ); no further Linux build or suite is needed by this brief; the next one from this lane goes to igneum-build-1 through tools/build-remote.sh |
|
||||
|
||||
| P3 | the 0.3.15 digest rule (main's ruling, 20:1x UK): an absent class v4 field contributes nothing to the digest, so publish 1 (the binary) splits no handshake and publish 2 (the sixteen-field file) is the one sweep | the compat case and the refusal case on real nodes; the pinned fixtures | GREEN on fork ca3-v4-order-fix 713ef876 (with the order-test race fix 791ff22c; both handed to the shipper for release-0.3.15-node): `infra/fast-time/digest-compat.mjs` (18:58Z, suffix 995, the live thirteen-field file copied, never written): the fixed node and a 0.3.14 node (`target-0314/release/igneumd`) on the thirteen-field file print ONE digest `a89be8a7e64b7d5a...` and peer (n0 has 2 peers, the old node 1: the compat case); the fixed node on the sixteen-field file (the exec pin, the floor 219,600, the window 86,400) prints `db9a85f9ff08f72f...` and has no peer, the handshake's `Refusing peer ...: consensus params digest mismatch` line in the log (the refusal case, the rule's known-failed case: present fields move the digest). On the devnet network id the sixteen-field object digests to `1dddfa5574d5536109a5ae68dc415b55e954bd11208608440845e19670505871` (the fixed Mac binary's start line = the pinned test value), the thirteen-field live file to `b18ed271f75dd464...` (0.3.14's value), fourteen fields `23e76936...`; no file `c562d70e...` (0.3.11 to 0.3.15 unchanged). Box line on 713ef876: kaspa-consensus 98, consensus-core 111 + 7, rc 0. The rehearsal object sets both fields, so its `a15db4f0` on devnet-400 stands. Summary `digest-compat-20261006.json` |
|
||||
|
||||
Summary files in this directory: `class-v4-20261006-1553Z-cpu.json` (G4 run 1), `class-v4-20261006-never-failed-case.json` (G4 run 2, the failed case), `class-v4-20261006-metal.json` (G4b), `class-v4-20261006-id-rerun.json` (G4 on the program-id fix, with the id assertion), `class-v4-20261006-id-failed-case.json` (the id assertion's failed case).
|
||||
|
|
|
|||
84
infra/fast-time/digest-compat.mjs
Normal file
84
infra/fast-time/digest-compat.mjs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#!/usr/bin/env node
|
||||
// The 0.3.15 digest rule's two cases (docs/plans/counter-asic-3-node.md section 6.2; main's ruling of 6 October 2026,
|
||||
// 20:1x UK): an absent class v4 field contributes nothing to the consensus digest. Four nodes on a private network
|
||||
// (suffix 995, ports 29800 and up, data /tmp/igneum-digest-compat), all on the same thirteen-field live devnet object
|
||||
// COPIED from /tmp/igneum-devnet/override-v3.json (the live file is never written) except the last:
|
||||
// n0 the fixed (0.3.15) node, thirteen fields the hub every other node connects to
|
||||
// n1 the fixed node, thirteen fields must peer with n0 (same binary, same file)
|
||||
// n2 the OLD node (0.3.13 or 0.3.14), thirteen fields the COMPAT case: must peer with n0 (same digest across binaries)
|
||||
// n3 the fixed node, SIXTEEN fields (the exec pin, the floor, the window) the REFUSAL case: must NOT peer (its digest moved)
|
||||
// PASS = n0 has n1 and n2 as peers, n3 has none and its log (or n0's) carries the digest refusal. The refusal case is
|
||||
// the rule's own known-failed case: the fields present DO move the digest.
|
||||
// node infra/fast-time/digest-compat.mjs --new <igneumd> --old <igneumd> [--secs 40]
|
||||
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
|
||||
import { connectRpc } from '../../tools/finality-attacks/lib/rpc.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const sflag = (n) => { const i = args.indexOf(`--${n}`); return i >= 0 ? args[i + 1] : null; };
|
||||
const NEW = sflag('new'), OLD = sflag('old');
|
||||
const SECS = +(sflag('secs') || 40);
|
||||
if (!NEW || !OLD || !existsSync(NEW) || !existsSync(OLD)) { console.error('usage: --new <igneumd 0.3.15> --old <igneumd 0.3.13/14>'); process.exit(2); }
|
||||
const TMP = '/tmp/igneum-digest-compat';
|
||||
const BASE = 29800, SUFFIX = 995;
|
||||
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), ...a);
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
|
||||
// the live thirteen-field object, read (never written) from the Mac node's file; the sixteen-field one adds the exec pin,
|
||||
// the floor and the window as text (the never heights must not pass through a JavaScript number)
|
||||
const live13 = readFileSync('/tmp/igneum-devnet/override-v3.json', 'utf8').trim();
|
||||
if (!/^\{.*\}$/s.test(live13) || (live13.match(/":/g) || []).length !== 13) { console.error(`the live file is not a thirteen-field object: ${live13.slice(0, 80)}`); process.exit(2); }
|
||||
const file13 = `${TMP}/override-13.json`; writeFileSync(file13, live13 + '\n');
|
||||
const file16 = `${TMP}/override-16.json`;
|
||||
writeFileSync(file16, live13.replace(/\}\s*$/, ',"exec_restart_state_root":"0xed27bb2d6128daf50493ed5539a73bb93f9d7c63ad7f70a7cbb46ba81c9e164e","program_class_v4_activation_daa":219600,"program_class_v4_signal_window_daa":86400}\n'));
|
||||
const started = [];
|
||||
class Node {
|
||||
constructor(i, bin, file, connect) { this.i = i; this.bin = bin; this.file = file; this.connect = connect; this.grpc = BASE + i * 10; this.p2p = BASE + i * 10 + 1; this.json = BASE + i * 10 + 2; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`; }
|
||||
async start() {
|
||||
mkdirSync(this.dir, { recursive: true });
|
||||
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--utxoindex', `--appdir=${this.dir}`,
|
||||
`--rpclisten=127.0.0.1:${this.grpc}`, `--rpclisten-json=127.0.0.1:${this.json}`, `--listen=127.0.0.1:${this.p2p}`, `--override-params-file=${this.file}`, '--loglevel=info', '--yes'];
|
||||
if (this.connect) a.push(`--connect=127.0.0.1:${this.connect}`); else a.push('--outpeers=0');
|
||||
const out = openSync(this.logFile, 'a');
|
||||
this.proc = spawn(this.bin, a, { stdio: ['ignore', out, out] }); started.push(this.proc);
|
||||
await sleep(1500);
|
||||
try { this.rpc = await connectRpc(`ws://127.0.0.1:${this.json}`); } catch (e) { log(`n${this.i} rpc: ${e.message}`); }
|
||||
return this;
|
||||
}
|
||||
grep(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
|
||||
digest() { return (this.grep(/Consensus params digest/)[0] || '').replace(/^.*digest: /, '').slice(0, 64) || null; }
|
||||
async peers() { try { const r = await this.rpc.call('getConnectedPeerInfo', {}); const list = r.peerInfo || r.peer_info || r.infos || r; return Array.isArray(list) ? list.length : null; } catch (e) { return `rpc: ${e.message}`; } }
|
||||
}
|
||||
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('unhandledRejection', async (e) => { log(`FAILED: ${e?.stack || e}`); await stopAll(); process.exit(3); });
|
||||
|
||||
const n0 = await new Node(0, NEW, file13, null).start();
|
||||
const n1 = await new Node(1, NEW, file13, n0.p2p).start();
|
||||
const n2 = await new Node(2, OLD, file13, n0.p2p).start();
|
||||
const n3 = await new Node(3, NEW, file16, n0.p2p).start();
|
||||
const nodes = [n0, n1, n2, n3];
|
||||
await sleep(SECS * 1000);
|
||||
const rows = [];
|
||||
for (const n of nodes) {
|
||||
const peers = await n.peers();
|
||||
// the handshake's own line (flow_context.rs): `Refusing peer <addr>: consensus params digest mismatch, local <d> remote <d>`
|
||||
const refusal = n.grep(/Refusing peer .*digest/).map(l => l.replace(/^.*?\] /, '')).slice(0, 2);
|
||||
rows.push({ node: `n${n.i}`, binary: n.bin === NEW ? 'new' : 'old', fields: n.file === file13 ? 13 : 16, digest: n.digest(), peers, refusal });
|
||||
log(`n${n.i} ${n.bin === NEW ? 'new' : 'old'} ${n.file === file13 ? 13 : 16} fields: digest ${n.digest()}, peers ${peers}${refusal.length ? `, digest lines: ${refusal.join(' | ').slice(0, 300)}` : ''}`);
|
||||
}
|
||||
const checks = {
|
||||
new_and_old_on_13_fields_print_one_digest: rows[0].digest && rows[0].digest === rows[1].digest && rows[0].digest === rows[2].digest,
|
||||
the_16_field_digest_differs: rows[3].digest && rows[3].digest !== rows[0].digest,
|
||||
n0_peers_with_n1_and_n2: rows[0].peers === 2,
|
||||
old_node_peers_with_new: rows[2].peers === 1,
|
||||
n3_has_no_peer: rows[3].peers === 0,
|
||||
a_digest_refusal_line_exists: rows[3].refusal.length > 0 || rows[0].refusal.length > 0,
|
||||
};
|
||||
const pass = Object.values(checks).every(Boolean);
|
||||
writeFileSync(`${TMP}/summary.json`, JSON.stringify({ pass, checks, rows, new_binary: NEW, old_binary: OLD, secs: SECS }, null, 2));
|
||||
log(`SUMMARY ${pass ? 'PASS' : 'FAIL'}: COMPAT new-13 ${rows[0].digest?.slice(0, 16)} = old-13 ${rows[2].digest?.slice(0, 16)} (n0 peers ${rows[0].peers}, old node peers ${rows[2].peers}); REFUSAL new-16 ${rows[3].digest?.slice(0, 16)} peers ${rows[3].peers}`);
|
||||
for (const [k, v] of Object.entries(checks)) if (!v) log(`FAILED CHECK ${k}`);
|
||||
log(`summary: ${TMP}/summary.json`);
|
||||
await stopAll();
|
||||
process.exit(pass ? 0 : 1);
|
||||
Loading…
Reference in a new issue