The sweep (main's item 1): 199 tracked text files, 783 lines. The founder's full name, first name and possessive become "the founder" (sentence starts capitalised); the lowercase operating-system user name in WSL paths and commands becomes <user>; the second owner login becomes "the second owner login"; the three earlier businesses and the two other brands become "the other business", "the earlier entity", "the earlier business" and "another brand"; the Chrome profile rule names the igneum.network profile, not the profile's label. The standing commit login igneum-labs is not a founder term here: the fresh-repository step renames it in the history (docs/plans/history-rewrite.md, tools/repo/fresh-repo.sh). The patterns never appear in plain text in the tree (a plaintext list would be the hit): tools/ci/founder-strings.b64 (perl regex, tab, a sample per row) is read by tools/ci/founder-strings-check.sh (every tracked text file, perl, known-failed first: the self-test plants each row's sample in a fixture and the hit must name the file), by tools/community/discord-hooks.mjs (the guard's founder and business rows; the test takes its fixtures from the samples) and by tools/repo/fresh-repo.sh (the business names of the rewrite rules). site/forbidden-strings.txt carries the same patterns as b64: lines, decoded case-insensitive by site/scrub.mjs and tools/ci/launch-gates-check.mjs (whose fixture now plants an encoded made-up name). The check runs in the gate's tree checks on every merge. Not in this commit, by main's word: the 105 commit messages and 40 personal-identity commits that need the history rewrite (listed, not run), and the secrets found by gitleaks over the history (reported with owners). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
15 KiB
Mission item 4: weight-gated deep fork choice, the gate (status log)
Lane: horizon (mission items 4 and 12), started 7 October 2026, 12:41 UK, on main's brief. Branches: horizon (this repo, on master
d1285cef) and horizon-node (the fork, on release-0.3.20-node dc141409, the 0.3.20 line; this work targets 0.3.21). Worktrees
igneum-wt-horizon and igneum-wt-horizon/vendor/igneum-node-horizon. Times are UK (BST) unless marked Z.
The item (mission.md 2.4): a tip whose fork point is older than D (10 min of past-median time) is a fork-choice candidate only if the keys that built it hold at least a third of the weight table at the fork point. Gate, fast time: a 51 percent fresh-key fork from 15 min back is refused by every honest node; a one-third-weight fork is accepted; partition heal unchanged. Known-failed cases first.
1. What already existed (read before writing anything)
| Where | What | State |
|---|---|---|
fork consensus/src/processes/finality.rs deep_fork_refusal, asked by sink_search_algorithm for every popped candidate |
the rule, behind fork_gate_activation_daa (never on every network; in the digest once set) with fork_gate_window_daa 600 s; the fork point is the highest chain ancestor of the candidate on the sink's chain; depth = the larger of sink minus fork and candidate minus fork; builders = the vote keys of every block of the candidate's chain since the fork (chain blocks and mergesets); the table = voters_at(fork); refused under 3 x weight < total; node-local memo per refused block |
on the 0.3.20 line since 61b22057 (6 October), fixes 3301cf32 (depth along both chains) and aa0182aa (no extension shortcut) |
| fork unit tests | without_the_fork_gate_a_minor_keys_deep_fork_wins (known-failed), the_fork_gate_refuses_a_deep_fork_under_a_third_and_passes_the_rest (25 percent refused, 75 percent passes, inside the window passes) |
green on that line |
infra/fast-time/fork-gate.mjs on branch ca3-v4-node (never on master) |
a TWO-node harness: A at 75 percent, B at 25 percent, A IDLE through the split; cases gate-on hold (PASS 23:45Z 6 Oct), gate-off reorg (PASS), gate-off hold (FAIL as it must); run on the Mac; a pkill -f on the devnet suffix |
the record in docs/plans/counter-asic-3-node.md 7.5 |
| the live devnet and Devnet 2 override files, the testnet object | no fork_gate field anywhere: the switch is never on every network |
unchanged by this lane |
What the mission gate asks beyond that record: a FRESH key (0 of the table, not 25 percent), the honest side MINING through the split (the record idled it), EVERY honest node (two, not one), the one-third case accepted, and the partition heal shown unchanged with the gate on and off. Nothing in the rule needed to change for any of these; this lane adds the proof.
2. What this lane added
| Piece | Where | What |
|---|---|---|
| Fresh-key unit test | fork a_fresh_keys_deep_fork_wins_only_with_the_gate_off |
known-failed first: at never a key with no block in the table wins by blue work; at 0 it is refused with weight 0 of the table |
| One-third line unit test | fork the_gate_passes_at_one_third_of_the_table_and_refuses_under_it |
eight (honest mix, fork height) pairs; each outcome equals 3 x weight >= total read from the table the gate reads; both sides seen |
| Three-node harness | infra/fast-time/fork-gate.mjs (new on master's line) |
H1 and H2 honest and mining through every phase, B the third node; modes attack (B fresh, 3 of 5 threads), third (B at about half of the table), partition (H1 against H2); a reorg is read from the chain (getVirtualChainFromBlock of the pre-cut tip lists removed blocks), never from a key; blue work compared as BigInt; leftovers stopped by pid file |
| The six-case runner | infra/fast-time/fork-gate-gate.mjs |
known-failed cases first, then the gate line, the one-third case, the two partition heals; GREEN only when every case gives the verdict it must and the two heals agree |
| Box wrapper | tools/fast-time-remote.sh (whole-body block; listed in tools/ci/whole-body-check.sh) |
runs a harness on a box under a slot with a holder line and a JSONL row, the node binaries from a fork worktree's target on the box, results fetched back |
2a. The hole the known-failed cases found (13:1x to 13:3x UK)
The one-third unit test failed on box 2 at its second pair: B on every fourth block, fork at height 50, B holding 12 of 49 at the fork, and the private tip WON. A probe test (temporary, since removed) read the gate's own answer at fork heights 40, 45, 48, 50, 55, 60 and 65: refused at 40, 48 and 60; let through at 45, 50 and 55 (65 is inside the window, a legitimate pass). The let-through rows read "keys 2, signed 44 of 44": the honest key A was in the attacker's builders. The pattern is the fork block's own key: heights that are multiples of 4 sit on B's blocks and were refused; the others sit on A's blocks and passed.
Cause: collect_builders took each chain block's mergeset (blues and reds) and GHOSTDAG's mergeset_blues[0] is the block's
selected parent, so the first private block above the fork carried the FORK block into the builders, and the fork block was
A's. A 25 percent key forking right after a 75 percent key's block walked through the gate with A's weight on its side. The
same reading let a private chain that merges honest blocks as side parents inherit every merged key. The 6 October harness
PASS (23:45Z) was luck: its pre-cut sink was B-built, so the leaked key was B's own.
Fix (fork, collect_builders(block, sink, keys)): a builder is the key of a block the sink does NOT have (not a DAG ancestor
of the sink, the sink itself included). The fork block is the sink's; honest blocks the attacker merged are the sink's; a
partition side's own blocks are not, so a heal is unchanged. New unit test, known-failed shape first:
a_deep_fork_counts_only_the_blocks_the_sink_lacks_as_builders (B from A's block at 45 refused; a private chain merging
honest blocks 46 to 80 wins at never and is refused at 0).
Also seen: ban_is_decided_by_the_carrying_block_so_nodes_agree_on_every_voter_list died with UnexpectedDifficulty in the
full crate run (box 2 under load 65) and passed alone (1 of 1, 13:1x UK); not this lane's change, noted for the suite count.
3. Runs
Commits
| Repo | Branch | Commit | What |
|---|---|---|---|
| fork | horizon-node (on release-0.3.20-node dc141409) | eb32d2e0 | the builders fix (collect_builders skips every block the sink has) and the fork-gate tests on the two-instance lab |
| fork | horizon-node | a6a71ac2 | the lab inserts into the judge first and rebuilds a block refused on UnexpectedDifficulty (the suite's flake class) |
| repo | horizon (on master d1285cef) | 0dc2adff | the three-node harness, the six-case runner, the box wrapper, the whole-body check row |
Suites (box 2, igneum-build-2, nice 10 on 32 cores)
| Run (UK) | Command | Result |
|---|---|---|
| 12:5x | -- test --release -p kaspa-consensus (the first test version, no fix) |
111 of 113: my one-third test FAIL (the hole), ban_is_decided_by_the_carrying_block... FAIL on UnexpectedDifficulty (load 65; passes alone 13:1x, 1 of 1) |
| 13:2x | -- test --release -p kaspa-consensus --lib processes::finality::tests (the fix, the lab) |
24 of 24 |
| 13:3x | -- test --release -p kaspa-consensus (eb32d2e0 content) |
112 of 114: two lab tests FAIL on UnexpectedDifficulty (the same flake class, now reachable by the lab's second instance) |
| 13:3x | -- test --release -p kaspa-consensus at a6a71ac2, 0dcaf4c5, ca7acb99 (the lab's rebuild, a pause between rebuilds, salted hashes) |
111 or 112 of 114 each: the two labs whose instances share one config still FAIL at block 61 under load; the probes showed UnexpectedDifficulty thirty times over 7.5 s, so neither clock nor hash memo |
| 13:38 | -- test --release -p kaspa-consensus at 6eb21fc9 (the lab on DifficultyRule::KaspaSampled, DAG-only bits) |
113 of 114 in the lib target, 3 ignored; the one FAIL is the pre-existing ban_is_decided_by_the_carrying_block_so_nodes_agree_on_every_voter_list (the same class: the dual rule's template bits and header stamp disagree at block 61, the first retarget, under load; passes alone). The lib target failing stops cargo before the integration targets. |
Fork commits after eb32d2e0, test-only: a6a71ac2, 0dcaf4c5, ca7acb99, f225ed9f, 6eb21fc9 (the lab inserts into the judge first and rebuilds on the flake; a pause between rebuilds; every lab its own hash words; the DAG-only difficulty rule; the import). The binary content of kaspad and igneum-miner is the same from eb32d2e0 on; the gate binary carries 6eb21fc9.
Build (box 1, igneum-build-1)
| Run (UK) | Command | Result |
|---|---|---|
| (pending) | tools/build-remote.sh --no-fetch -- build --release -p kaspad -p igneum-miner --features kaspad/igneum-pow from the fork worktree at a6a71ac2 (igneum-pow at 8c728ca3 through an untracked paths override to igneum-pow-amend, the 0.3.20 pairing); two earlier submissions of the uncommitted tree were stopped by pid so the gate binary carries the commit |
Tree checks (the Mac, checks only)
tools/ci/pre-push.sh --ci on horizon 0dc2adff: GREEN, 43 checks in 31 s (13:3x UK).
The gate (box 2, igneum-build-2, by main's word at 13:4x UK: box 1's slots stay with the 0.3.20 pin)
Binary: igneumd and igneum-miner of horizon-node 6eb21fc9 (igneum-pow 8c728ca3), built on box 2 at 13:5x UK
(tools/build-remote.sh --box 2, 113 s); run through tools/fast-time-remote.sh --box 2 (one slot, the bounded class, 583 s
for the six cases side by side), case summaries and logs in docs/plans/mission-item-4-gate/. Window 60 DAA, joint 240 s,
split 180 s, watch 150 s; honest nodes H1 and H2 at one CPU thread each through every phase; the attacker at 3 threads in
the split (60 percent of the CPU hash: the refusal reads weight, not how much heavier the chain is).
| Case | Must | Got | Side 2 at the fork | Fork depth (DAA) | Blue work at the heal, side 2 against H1 | Honest nodes after the heal |
|---|---|---|---|---|---|---|
| attack, gate off, expect reorg (the rule's known-failed case) | PASS | PASS | 0 bps (fresh key) | 191 | 17,470,131 against 14,893,198 | H1 and H2 reorged at 510 s, 0 refusal lines |
| attack, gate off, expect hold (the harness's own failed shape) | FAIL | FAIL | 0 bps | 179 | 19,683,187 against 17,459,678 | H1 and H2 reorged at 480 s, 0 refusal lines |
| attack, gate on, expect hold (THE GATE LINE) | PASS | PASS | 0 bps | 197 | 20,297,242 against 14,807,915 | H1 and H2 HELD for the whole watch, 177 refusal lines each, both know side 2's tip; side 2 kept its chain |
| third, gate on, expect reorg (a one-third-weight fork accepted) | PASS | PASS | 6,000 bps | 166 | 28,969,222 against 24,628,833 | H1 and H2 reorged at 477 s, 0 refusal lines |
| partition, gate on, expect reorg | PASS | PASS | 3,719 bps | 194 | 18,089,683 against 11,490,863 | H1 reorged at 507 s, 0 refusal lines; H2 kept its chain |
| partition, gate off, expect reorg (the same outcome) | PASS | PASS | 5,583 bps | 191 | 19,576,159 against 12,270,631 | H1 reorged at 507 s, 0 refusal lines; H2 kept its chain |
GATE GREEN, 13:58 UK: 6 of 6 cases gave the verdict they must; the partition heal is the same with the gate on and off (both reorg H1 onto the heavier side, 0 refusals). Reading "15 min back" at fast time: the devnet window is 600 s and the mission's 15 min is one and a half windows; the harness keeps the window at 60 DAA and the split at 180 s, three windows deep, so the refusal was asked from 61 s of the split on and held for its whole second half.
4. Consequences per tier
One sentence: with the fix, a renter's or a chain-splitter's deep reorg is bounded by weight for every tier alike, and an honest key's own blocks are never refused.
| Tier | What the number means |
|---|---|
| Home miner (one card, any size, Windows, Linux or macOS, any vendor) | its node refuses the same tips a rig's or a pool's node refuses; a fresh-key chain from more than ten minutes back never becomes its sink while the key that built it holds under a third of the window; nothing to configure, nothing to pay; the memo costs one mergeset per refused block |
| Rig | the same; a rig's own blocks after a connectivity gap are built by its own keys and pass on share |
| Pool user | the pool's node holds the chain the pool mined; a renter cannot reorganise the pool's payouts past ten minutes without a third of the weight |
| Exchange and holder | a 12-hour double spend during a pause or in the first month (51-percent.md section 5 rank 2, USD 146 at 1 GH/s) is gone: the deep fork is refused at every honest node, lock or no lock |
| The rule's cost | none for certificates; a partition side under a third cannot reorg the other past the window (the intended outcome, unchanged by the fix) |
Before the fix the bound held only when the fork sat on a block of a small key; a renter forking right after a large miner's block, or merging honest blocks into its chain, walked through on every tier.
5. Open
| Item | Owner |
|---|---|
| Merge: horizon-node is on release-0.3.20-node dc141409; main's word is to rebase onto the 0.3.20 pin once named (c4459193 the candidate) for 0.3.21; never a push to a release branch | this lane, when main names the pin |
The difficulty red: ban_is_decided_by_the_carrying_block... and, before the lab moved to the DAG-only rule, two lab tests, on UnexpectedDifficulty at block 61 (the first retarget) under the full suite's load; the dual rule's template bits come from a virtual resolved at one instant and the header is stamped at a later one |
the node lane for 0.3.21 (main's word: logged, not fixed here) |
| The fork's test block builder asks the gate (it runs the sink search from the parents it is given): any future test that builds a private chain on a gated instance builds it on the fork block instead; the lab's two-instance shape is the pattern | the node lane; a note in consensus/src/consensus/test_consensus.rs is owed |
tools/box named in the brief does not exist; the box tooling is tools/build-remote.sh on infra/build-server/lib.sh, which tools/fast-time-remote.sh uses; the wrapper's first run checked the worktree root out on box 2 and took the fork sources with it (fixed the same hour: overlay only, 10140f9f) |
main's reading |
The devnet, Devnet 2 and the testnet object carry no fork_gate field: the switch stays never everywhere until a cut sets it (in the digest once set) |
the shipper and the founder |
6. Rebase for 0.3.21 (14:0x UK, the shipper's order)
horizon-node rebased onto release-0.3.20-node c4459193 (the 0.3.20 pin) in one round, no conflict: tip 437f0438, on both box mirrors under its name. Suite on build-2 at 437f0438: -p kaspa-consensus lib 117 of 119, 3 ignored (the ban difficulty flake and a_chain_that_misses_an_adopted_lock_never_locks_here, which passes alone 1 of 1 and passed in every earlier full run; load 113). Digest on tools/fleet/override.json (origin/master): 7bd98cc4..., the same as the pin's own binary on that file. Handed to the node lane a283f5f0d364ceef0 for the 0.3.21 merge after 55768f88, miner-reliability-20 and pool-finish-node.
Digest on the file hub-1 runs (/root/fleet/override.json, sixteen fields, the copy at /tmp/igneum-devnet/override-v3-live.json on build-1): eada4bda8aa8368c2b2c3d17744bc7a70a0ff0e996dad681884d3ac5de1207eb, the shipper's string, read 14:1x UK from the branch binary on build-2; 7bd98cc4 was master's tools/fleet/override.json, another file. The live digest is unchanged by this branch.