From b69f8656c9e93285c3d416ac2024bea1d3529dd0 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:46:25 +0000 Subject: [PATCH] Counter ASIC 3.0 gates (node): the receive-side version rule (fork f1ea7a38) and the poisoned-peer case of the node-cut gate (PASS: the new node refuses version-1026 blocks with 0.3.14's line, relays none, stays the old hub's peer), ledger N1 extended with the receive side and the stale-block answer Co-Authored-By: Claude Fable 5.1 --- docs/fud-ledger.md | 4 +- .../node-compat-20261006-poisoned-peer.json | 112 ++++++++++++++++++ docs/plans/counter-asic-3-gate/node-gates.md | 2 +- infra/fast-time/node-compat.mjs | 23 +++- 4 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 docs/plans/counter-asic-3-gate/node-compat-20261006-poisoned-peer.json diff --git a/docs/fud-ledger.md b/docs/fud-ledger.md index 828ba481f..5c75d8d5d 100644 --- a/docs/fud-ledger.md +++ b/docs/fud-ledger.md @@ -2028,9 +2028,9 @@ Two faults found on the live devnet by the fleet's canary boxes and the hub whil "p2-3090-1 on 713ef876 with the live thirteen-field file, at every reconnect since its 19:42Z restart: P2P, got reject message: wrong block version: got 1026 but expected 2 from peer 188.245.5.161:26611; blocks 122,630, synced false, peers 0." -Status: Fixed (6 October 2026, 20:0xZ, fork commit 17c60367 on ca3-v4-order-fix; in 0.3.15). Conceded: the node lane's fault. +Status: Fixed (6 October 2026, 20:0xZ, fork commit 17c60367 on ca3-v4-order-fix, the stamp; and 20:4xZ, f1ea7a38, the receive side, after the clean canary of 21:3x UK showed a 0.3.15 node ACCEPTING and relaying a version-1026 block it would never write, off a poisoned peer, and being disconnected by every 0.3.14 peer in turn; in 0.3.15). Conceded: the node lane's fault, twice. -Answer: the class v4 signal (PROPOSED, `docs/plans/counter-asic-3-node.md` section 6) is the producer's object version in the high byte of the header version; the first 0.3.15 build stamped it from the binary alone, so on the live file (no window, no floor) a new node wrote version 1026 and every 0.3.14 node, whose rule is version 2 or reject, refused its blocks and its relays, although the digest compat rule (the same evening) made the handshake peer. A new miner lost every block; a lagging new node could not re-sync. The fix gates the stamp and the signal read on publish 2's object (both `program_class_v4_signal_window_daa` and `program_class_v4_activation_daa` set): on the thirteen-field file the header is byte for byte what 0.3.14 writes. Why the gates missed it: the digest compat harness peered the two binaries but neither mined; the new gate mines on both sides, joins a clean node through each, restarts the new node and re-syncs it. +Answer: the class v4 signal (PROPOSED, `docs/plans/counter-asic-3-node.md` section 6) is the producer's object version in the high byte of the header version; the first 0.3.15 build stamped it from the binary alone, so on the live file (no window, no floor) a new node wrote version 1026 and every 0.3.14 node, whose rule is version 2 or reject, refused its blocks and its relays, although the digest compat rule (the same evening) made the handshake peer. A new miner lost every block; a lagging new node could not re-sync. The fix gates the stamp, the signal read AND the header version rule on publish 2's object (both `program_class_v4_signal_window_daa` and `program_class_v4_activation_daa` set): on the thirteen-field file the header is byte for byte what 0.3.14 writes, and a header carrying the signal bit is refused with 0.3.14's own `WrongBlockVersion(1026, 2)` before the engine and never relayed, so a poisoned peer cannot poison a new node. What no new-node change can do: a 0.3.14 node whose datadir holds version-1026 blocks is disconnected by its 0.3.14 peers by THEIR rule when it relays them, until those blocks leave the relay window; the fleet wipes the poisoned datadirs. Why the gates missed it: the digest compat harness peered the two binaries but neither mined; the new gate mines on both sides, joins a clean node through each, restarts the new node and re-syncs it. Evidence: unit tests `block_template_uses_current_block_version` (version 2 with no window and no floor, 1026 with both) and `program_class_signal_rule`; the gate `infra/fast-time/node-compat.mjs` on the fixed binary beside 0.3.14 on the live object: one digest on all four nodes, the new miner's 73 blocks accepted by the old hub, counts 169/169/169/169, header versions {0: the genesis, 2: 169}, no reject line, the clean join and the restart re-sync (PASS, 20:06Z; `docs/plans/counter-asic-3-gate/node-compat-20261006-fixed.json`); the same gate on the canary binary 713ef876 reproduces the fault (`HandleRelayInvsFlow flow error: wrong block version: got 1026 but expected 2, disconnecting`, counts 138 against 101, the restarted node never re-synced: FAIL, `node-compat-20261006-canary-failed-case.json`). diff --git a/docs/plans/counter-asic-3-gate/node-compat-20261006-poisoned-peer.json b/docs/plans/counter-asic-3-gate/node-compat-20261006-poisoned-peer.json new file mode 100644 index 000000000..d93951a2d --- /dev/null +++ b/docs/plans/counter-asic-3-gate/node-compat-20261006-poisoned-peer.json @@ -0,0 +1,112 @@ +{ + "pass": true, + "checks": { + "one_digest_on_old_and_new": true, + "new_miner_blocks_accepted": true, + "old_miner_blocks_accepted": true, + "zero_rejected_by_miners": true, + "no_wrong_version_or_reject_line": true, + "every_header_version_is_the_block_version": true, + "old_nodes_hold_the_new_blocks": true, + "counts_agree_at_the_end": true, + "clean_new_node_joined_and_synced": true, + "restarted_new_node_resynced": true, + "new_node_served_a_genesis_sync_and_survived": true, + "no_panic_in_any_node_log": true, + "new_node_refused_the_poisoned_blocks": true, + "poisoned_node_mined_something_to_refuse": true, + "old_hub_never_saw_a_1026_block": true + }, + "new_binary": "vendor/igneum-node-ca3v4/target-ca3v4/release/igneumd", + "old_binary": "/Users/joshm/Projects/igneum/vendor/igneum-node/target-0314/release/igneumd", + "poisoned_binary": "/private/tmp/claude-501/-Users-joshm/cd75457f-4858-4f86-9634-7481ee056b7b/scratchpad/igneumd-713ef876", + "new_node_rejects_of_1026": 12, + "poisoned_blocks_accepted_by_its_own_node": 179, + "digests": [ + "b0afb2ee93d0d6274cbf63d40ac7bed487202c5af84e035c8a634c521bffe6a2", + "b0afb2ee93d0d6274cbf63d40ac7bed487202c5af84e035c8a634c521bffe6a2", + "b0afb2ee93d0d6274cbf63d40ac7bed487202c5af84e035c8a634c521bffe6a2", + "b0afb2ee93d0d6274cbf63d40ac7bed487202c5af84e035c8a634c521bffe6a2", + "b0afb2ee93d0d6274cbf63d40ac7bed487202c5af84e035c8a634c521bffe6a2" + ], + "counts_after_mining": [ + { + "count": 147, + "sink": "9d12ad182ab77726", + "daa": 147 + }, + { + "count": 147, + "sink": "9d12ad182ab77726", + "daa": 147 + }, + { + "count": 147, + "sink": "9d12ad182ab77726", + "daa": 147 + }, + { + "count": 147, + "sink": "9d12ad182ab77726", + "daa": 147 + }, + { + "count": 147, + "sink": "9d12ad182ab77726", + "daa": 147 + } + ], + "final": [ + { + "count": 180, + "sink": "040d5ae85d45bc24", + "daa": 180 + }, + { + "count": 180, + "sink": "040d5ae85d45bc24", + "daa": 180 + }, + { + "count": 180, + "sink": "040d5ae85d45bc24", + "daa": 180 + }, + { + "count": 180, + "sink": "040d5ae85d45bc24", + "daa": 180 + }, + { + "count": 180, + "sink": "040d5ae85d45bc24", + "daa": 180 + } + ], + "served_join_count": 147, + "serving_node_alive": true, + "accepted_old_new": [ + 108, + 72 + ], + "rejected_by_miners_old_new": [ + 0, + 0 + ], + "reject_lines": [ + [], + [ + "HandleRelayInvsFlow flow error: wrong block version: got 1026 but expected 2, disconnecting from peer 127.0.0.1:56593.", + "HandleRelayInvsFlow flow error: wrong block version: got 1026 but expected 2, disconnecting from peer 127.0.0.1:56829.", + "HandleRelayInvsFlow flow error: wrong block version: got 1026 but expected 2, disconnecting from peer 127.0.0.1:57104." + ], + [], + [], + [] + ], + "header_versions": { + "0": 1, + "2": 180 + }, + "secs": 160 +} \ No newline at end of file diff --git a/docs/plans/counter-asic-3-gate/node-gates.md b/docs/plans/counter-asic-3-gate/node-gates.md index 19c261eb6..099010874 100644 --- a/docs/plans/counter-asic-3-gate/node-gates.md +++ b/docs/plans/counter-asic-3-gate/node-gates.md @@ -19,7 +19,7 @@ | 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. The shipper's independent readings on the 0.3.15 Mac binary (release-0.3.15-node = 4c6b129d + 0562a7f2 + 2e5d3d30 + 791ff22c + 713ef876, 19:0xZ): the same three values, and at tonight's live floor 226,800 the sixteen-field object reads `2c1162e2...`, the publish-2 digest if the floor stays there (recomputed at the publish); the 0.3.14 binary exits at parse on the sixteen-field file, which is why the file follows the binary; its suite on igneum-build-1 at 713ef876: kaspa-consensus 97 + the recorded flake alone 1 of 1, consensus-core 111 + 7, igneum-exec 20, kaspa-pow 15 with both engine tests, igneum-miner 18, p2p-flows 33. Summary `digest-compat-20261006.json` | -| P4 | the two 0.3.15 node faults found on the live devnet (the canary's version 1026, the pruned hub's sync panic), fixed in the same fork tree | the unit tests; the node-cut compat gate on real nodes (a MINING new node beside a 0.3.14 node on the live object, the old node accepting the new node's blocks; a clean new node joining through the old hub; a clean OLD node joining through the NEW node, which must survive serving a sync from the genesis; the new node restarted and re-synced; every header version the block version; no panic); the ledger rows | GREEN on fork ca3-v4-order-fix 7961c5f1 (= 791ff22c + 713ef876 + 17c60367 the signal gate + 7961c5f1 the retention error; the shipper's release-0.3.15-node). The gate `infra/fast-time/node-compat.mjs` (20:15:5x to 20:19:04Z, suffix 994, the live object plus CPU genesis bits, the 0.3.14 binary `target-0314/release/igneumd` as the old side): SUMMARY PASS: one digest `b0afb2ee93d0d627` on all five nodes; accepted old/new 120/75, rejected 0/0; header versions {0: the genesis, 2: 195}; counts 195/195/195/195/195 at the end (the clean new node through the old hub, the clean old node served from the genesis by the new node, which stayed alive, and the restarted new node all at 195); no `wrong block version`, reject or panic line in any log. The known-failed case on the canary binary 713ef876 beside 0.3.14 (20:00 to 20:02Z): SUMMARY FAIL, `HandleRelayInvsFlow flow error: wrong block version: got 1026 but expected 2, disconnecting` on the old nodes, `P2P, got reject message: wrong block version` on the new, counts 138 against the new node's 101, the restarted new node never re-synced (the fleet's p2-3090-1). Unit tests: `block_template_uses_current_block_version` (2 on the thirteen-field state, 1026 with both v4 fields), `program_class_signal_rule`, `a_sync_request_below_retention_is_an_error_not_a_panic`; the box on 7961c5f1: kaspa-consensus 99, consensus-core 111 + 7, rc 0 (the shipper's six-crate line: exec 20, miner 18, pow 15, p2p-flows 33). Ledger rows N1 and N2 in `docs/fud-ledger.md`. Owed: a truly pruned serving node in a harness (hours of fast-time chain at pruning depth 13,838); an old node whose datadir holds version-1026 blocks relaying them (the shipper's roll-back finding), queued. Summaries `node-compat-20261006-fixed.json`, `node-compat-20261006-canary-failed-case.json` | +| P4 | the two 0.3.15 node faults found on the live devnet (the canary's version 1026, the pruned hub's sync panic), fixed in the same fork tree | the unit tests; the node-cut compat gate on real nodes (a MINING new node beside a 0.3.14 node on the live object, the old node accepting the new node's blocks; a clean new node joining through the old hub; a clean OLD node joining through the NEW node, which must survive serving a sync from the genesis; the new node restarted and re-synced; every header version the block version; no panic); the ledger rows | GREEN on fork ca3-v4-order-fix 7961c5f1 (= 791ff22c + 713ef876 + 17c60367 the signal gate + 7961c5f1 the retention error; the shipper's release-0.3.15-node). The gate `infra/fast-time/node-compat.mjs` (20:15:5x to 20:19:04Z, suffix 994, the live object plus CPU genesis bits, the 0.3.14 binary `target-0314/release/igneumd` as the old side): SUMMARY PASS: one digest `b0afb2ee93d0d627` on all five nodes; accepted old/new 120/75, rejected 0/0; header versions {0: the genesis, 2: 195}; counts 195/195/195/195/195 at the end (the clean new node through the old hub, the clean old node served from the genesis by the new node, which stayed alive, and the restarted new node all at 195); no `wrong block version`, reject or panic line in any log. The known-failed case on the canary binary 713ef876 beside 0.3.14 (20:00 to 20:02Z): SUMMARY FAIL, `HandleRelayInvsFlow flow error: wrong block version: got 1026 but expected 2, disconnecting` on the old nodes, `P2P, got reject message: wrong block version` on the new, counts 138 against the new node's 101, the restarted new node never re-synced (the fleet's p2-3090-1). Unit tests: `block_template_uses_current_block_version` (2 on the thirteen-field state, 1026 with both v4 fields), `program_class_signal_rule`, `a_sync_request_below_retention_is_an_error_not_a_panic`; the box on 7961c5f1: kaspa-consensus 99, consensus-core 111 + 7, rc 0 (the shipper's six-crate line: exec 20, miner 18, pow 15, p2p-flows 33). Ledger rows N1 and N2 in `docs/fud-ledger.md`. RECEIVE SIDE (the clean canary, 21:3x UK: a 7961c5f1 node accepted and relayed a version-1026 block off a poisoned peer and was disconnected by every 0.3.14 peer): fork f1ea7a38 gates the header version RULE like the stamp (signalling inactive: the version must be exactly 2, 0.3.14's rule; active: the low byte); test inside `cheap_checks_run_before_the_pow_engine` (a 1026 header refused with `WrongBlockVersion(1026, 2)` before the engine on the old-format network, accepted with both fields installed); box on f1ea7a38: kaspa-consensus 99, consensus-core 111 + 7, rc 0. The gate with a POISONED peer (`--poisoned <713ef876 igneumd>` mining into the new node, 20:42:5x to 20:45:42Z): SUMMARY PASS: the new node refused the poisoned blocks 12 times (`HandleRelayInvsFlow flow error: wrong block version: got 1026 but expected 2, disconnecting`), the old hub's log carries no reject and no 1026 block (header versions {0: the genesis, 2: 180}), the new node stayed the hub's peer and mined 72 accepted blocks, counts 180 on every clean node including the clean joins and the restart. The stale blocks on the live devnet: a 0.3.14 node holding 1026 blocks is disconnected by its 0.3.14 peers by THEIR rule when it relays them, which no new-node change alters; new nodes are immune from f1ea7a38; the fleet wipes the poisoned datadirs. Owed: a truly pruned serving node in a harness (hours of fast-time chain at pruning depth 13,838). Summaries `node-compat-20261006-fixed.json`, `node-compat-20261006-canary-failed-case.json`, `node-compat-20261006-poisoned-peer.json` | | P5 | the dependency pass for 0.3.15.1 (the night battery's audit, mirror branch fbb72e5: the fork's lock file) | `cargo audit` before and after on the box; the six-crate suite | DONE on fork `ca3-v4-deps` f8f0f1df (from 7961c5f1): `cargo audit` on igneum-build-1 before: 6 vulnerabilities (crossbeam-epoch RUSTSEC-2026-0204, h2 2026-0258, quinn-proto 2026-0185, ruint 2026-0220, rustls 2026-0285, tracing-subscriber 2025-0055; the battery's 22 counts advisory paths), 16 warnings (unmaintained, unsound, yanked); the three peer-reachable crates first (h2 0.4.6 to 0.4.20, quinn-proto 0.11.14 to 0.11.19, rustls 0.23.39 to 0.23.45 with rustls-webpki 0.103.15), then crossbeam-epoch 0.9.21 and ruint 1.20.1 (rand_pcg added), every one a patch or minor bump by the audit's own solution line; after: 1 vulnerability, 16 warnings; the one left is tracing-subscriber 0.2.25, pinned by ark-relations 0.5.1 through ark-groth16 and ark-snark (a 0.2 to 0.3 major bump outside the fork's pins, which name 0.3 in bridge/Cargo.toml), named here, not taken. The six-crate suite on the box on the new lock: kaspa-consensus 99, consensus-core 111 + 7, igneum-exec 20, kaspa-pow 15, igneum-miner 18, kaspa-p2p-flows 33, rc 0 (80 s). The 16 warnings are unmaintained or unsound transitive crates (async-std, atty, bincode 1, derivative, instant, mach, paste, proc-macro-error, rustls-pemfile, anyhow, event-listener, faster-hex, lru, chacha20, spin), none with a bump to take, each a dependency choice for 0.3.16 | diff --git a/infra/fast-time/node-compat.mjs b/infra/fast-time/node-compat.mjs index e3819f1d7..fab483500 100644 --- a/infra/fast-time/node-compat.mjs +++ b/infra/fast-time/node-compat.mjs @@ -30,6 +30,10 @@ 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'), MINER = sflag('miner'); +// --poisoned : a binary that stamps version 1026 on the live file (the canary 713ef876) mining beside the +// NEW node: the new node must refuse its blocks with 0.3.14's line, relay nothing of them to the old hub, and stay +// the old hub's peer (the 21:3xZ finding: the first 0.3.15 build accepted and forwarded such a block) +const POISONED = sflag('poisoned'); const SECS = +(sflag('secs') || 150); for (const b of [NEW, OLD, MINER]) if (!b || !existsSync(b)) { console.error('usage: --new --old --miner '); process.exit(2); } const TMP = '/tmp/igneum-node-compat'; @@ -79,6 +83,8 @@ const since = () => ((Date.now() - t0) / 1000).toFixed(1); const n0 = await new Node(0, OLD, null).start(); const n1 = await new Node(1, NEW, n0.p2p).start(); const n2 = await new Node(2, OLD, n0.p2p).start(); +let n5 = null; +if (POISONED) { n5 = await new Node(5, POISONED, n1.p2p).start(); n5.mine('poisoned5', SECS + 60); log(`n5 poisoned (${POISONED.split('/').pop()}) mining into the new node, digest ${n5.digest()?.slice(0, 16)}`); } log(`n0 old digest ${n0.digest()?.slice(0, 16)}, n1 new ${n1.digest()?.slice(0, 16)}, n2 old ${n2.digest()?.slice(0, 16)}`); n0.mine('old0', SECS + 60); n1.mine('new1', SECS + 60); @@ -105,6 +111,10 @@ const fin = await Promise.all([n0, n1b, n2, n3, n4].map(n => n.count())); log(`t=${since()} final: counts ${fin.map(x => x.count).join('/')} sinks ${fin.map(x => x.sink).join(' ')}`); const nodes = [n0, n1b, n2, n3, n4]; const rejectLines = nodes.map(n => n.grep(/wrong block version|got reject message|Rejected block|PoW rejected/i).map(l => l.replace(/^.*?\] /, '')).slice(0, 3)); +// the poisoned case: the NEW node's log (before and after its restart) must carry 0.3.14's refusal of the 1026 blocks, +// the OLD hub's log none (nothing bad was relayed to it), and the poisoned node's blocks must be absent from the hub +const newRejects = POISONED ? [n1, n1b].flatMap(n => n.grep(/wrong block version: got 1026 but expected 2/)).length : 0; +const poisonedAccepted = POISONED ? minerLog('poisoned5').filter(x => /ACCEPTED block/.test(x)).length : 0; const minerRej = ['old0', 'new1'].map(l => minerLog(l).filter(x => /rejected nonce=|submit error/.test(x)).length); const accepted = ['old0', 'new1'].map(l => minerLog(l).filter(x => /ACCEPTED block/.test(x)).length); // the header versions on the chain, from the old hub's view @@ -126,7 +136,9 @@ const checks = { new_miner_blocks_accepted: accepted[1] > 0, old_miner_blocks_accepted: accepted[0] > 0, zero_rejected_by_miners: minerRej.every(x => x === 0), - no_wrong_version_or_reject_line: rejectLines.every(r => r.length === 0), + // without a poisoned peer no node sees a reject; with one, only the NEW node (which refuses the 1026 blocks) and the + // poisoned node itself (which hears the refusal) may carry such lines, never the old hub nor the other old nodes + no_wrong_version_or_reject_line: POISONED ? (rejectLines[0].length === 0 && rejectLines[2].length === 0 && rejectLines[4].length === 0) : rejectLines.every(r => r.length === 0), // every mined header carries the block version 2; the devnet genesis header is version 0 and is the one such block every_header_version_is_the_block_version: Object.keys(versions).length > 0 && Object.keys(versions).every(v => +v === 2 || (+v === 0 && versions[v] === 1)), old_nodes_hold_the_new_blocks: fin[0].count != null && fin[2].count != null && fin[0].count >= accepted[1] + accepted[0] - 2, @@ -135,10 +147,15 @@ const checks = { restarted_new_node_resynced: fin[1].count != null && fin[1].count >= maxCount - 3, new_node_served_a_genesis_sync_and_survived: servingNodeAlive && servedCount != null && servedCount >= hubAtJoin - 3 && fin[4].count >= maxCount - 3, no_panic_in_any_node_log: nodes.every(n => n.grep(/panicked at|Exiting\.\.\./).length === 0), + ...(POISONED ? { + new_node_refused_the_poisoned_blocks: newRejects > 0, + poisoned_node_mined_something_to_refuse: poisonedAccepted > 0, + old_hub_never_saw_a_1026_block: rejectLines[0].length === 0 && Object.keys(versions).every(v => +v === 2 || (+v === 0 && versions[v] === 1)), + } : {}), }; const pass = Object.values(checks).every(Boolean); -writeFileSync(`${TMP}/summary.json`, JSON.stringify({ pass, checks, new_binary: NEW, old_binary: OLD, digests, counts_after_mining: c, final: fin, served_join_count: servedCount, serving_node_alive: servingNodeAlive, accepted_old_new: accepted, rejected_by_miners_old_new: minerRej, reject_lines: rejectLines, header_versions: versions, secs: SECS }, null, 2)); -log(`SUMMARY ${pass ? 'PASS' : 'FAIL'}: digest ${digests[0]?.slice(0, 16)} on ${new Set(digests).size === 1 ? 'all four' : 'NOT all'}; accepted old/new ${accepted.join('/')}, rejected ${minerRej.join('/')}; header versions ${JSON.stringify(versions)}; final counts ${fin.map(x => x.count).join('/')}; reject lines ${rejectLines.map(r => r.length).join('/')}`); +writeFileSync(`${TMP}/summary.json`, JSON.stringify({ pass, checks, new_binary: NEW, old_binary: OLD, poisoned_binary: POISONED, new_node_rejects_of_1026: newRejects, poisoned_blocks_accepted_by_its_own_node: poisonedAccepted, digests, counts_after_mining: c, final: fin, served_join_count: servedCount, serving_node_alive: servingNodeAlive, accepted_old_new: accepted, rejected_by_miners_old_new: minerRej, reject_lines: rejectLines, header_versions: versions, secs: SECS }, null, 2)); +log(`SUMMARY ${pass ? 'PASS' : 'FAIL'}: digest ${digests[0]?.slice(0, 16)} on ${new Set(digests).size === 1 ? 'all' : 'NOT all'};${POISONED ? ` POISONED peer: the new node refused ${newRejects} times (its own node accepted ${poisonedAccepted});` : ''} accepted old/new ${accepted.join('/')}, rejected ${minerRej.join('/')}; header versions ${JSON.stringify(versions)}; final counts ${fin.map(x => x.count).join('/')}; reject lines ${rejectLines.map(r => r.length).join('/')}`); for (const r of rejectLines.flat().slice(0, 4)) log(` BAD ${r.slice(0, 220)}`); for (const [k, v] of Object.entries(checks)) if (!v) log(`FAILED CHECK ${k}`); log(`summary: ${TMP}/summary.json`);