igneum/docs/plans/mission-item-4-fork-gate.md
igneum-labs 7eed16a29a Pre-public scrub, the text pass (7 October 2026, 19:5x UK): no founder name, personal login, earlier business or personal address in any tracked text file, and a gate check that keeps it so
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>
2026-10-07 18:39:50 +00:00

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.