igneum/docs/plans/finality-v3-rollout-devnet.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

162 lines
14 KiB
Markdown

# Finality rule v3 on the live devnet: rollout plan, 4 October 2026 (evening)
Prepared by the finality engineer after the founder's "we need to fix these serious issues before making things public"
(ledger F21 and F22). Nothing in this file has been run on the devnet, and the 12-node Hetzner network of the morning
was destroyed at 15:30 UTC, so there was no cloud rehearsal; the measurements are the simulator (`sim/results_v2.md`,
"Rule v3"), the node's unit tests and the fast-time 3-node network with emulated 300-ms links (`docs/bench-log.md`,
"finality rule v3"). The main session runs section 4 in order. Background: spec 03 (Q4, Q5, 3.3.1, 3.7 items 2 and 9,
3.10, 3.11), `docs/fud-ledger.md` F21 and F22, `infra/cloud-devnet/results/2026-10-04/f22-vote-timing.md`. Shape and
rules follow `docs/plans/difficulty-v2-rollout-devnet.md`: the PCs get igneumd only through an OTA app version, and the
activation height leaves at least three hours from the manifest publish.
## 1. What changes and what does not
Only `igneumd` changes. Rule v3 is one height switch, `finality_v3_activation_daa`, read from the override file like
`difficulty_v2_activation_daa`; the default on every network is `u64::MAX` (never). A v3 node applies rule v2 to every
checkpoint whose block's DAA score is below the height and rule v3 from the first checkpoint at or above it:
- F21, the frozen weight table (spec Q5): a certificate locks only if its signers also hold two thirds of the weight
table at the last locked checkpoint on the chain, at that table's weights, while that checkpoint is less than one
weight window old (7,200 DAA on the devnet, 30 days on mainnet). A side of a partition cannot fill that table, so
no side under two thirds locks until a full window has passed without a lock; the heal resumes on one chain.
- F22, the certificate fold (spec Q4): the first certificate still forms at quorum; once every voter has signed, or
`certificate_fold` DAA seconds (3) after the determination, the node rebuilds it from every vote seen and gossips
the heavier one, and every node replaces a held certificate with a verified heavier one over the same block.
Nothing in block validity changes: a certificate verifies against the table at its own checkpoint as before, a block
carrying a lighter certificate is as valid as before, and the chain, the genesis and the databases stay. The persisted
finality state is unchanged (the fold clock is in memory). The miners follow templates and need nothing. A node that
reaches the height WITHOUT the field keeps rule v2: in a connected network it locks the same checkpoints (both rules
pass there) and ignores heavier certificates, so there is no fork; under a partition longer than `W / (3R)` such a
node could lock alone where the v3 nodes pause, which is the fork the switch exists to prevent, so every node must
carry the same height before it arrives: Mac node 1, the observer node, the seed, PC 1 and PC 2.
The new `FinalityParams` field `certificate_fold` has a serde default (3), so every existing override file still
parses; `infra/fast-time/override-60x.json` carries both new fields explicitly.
## 2. The binaries (built 4 October 2026, evening, on the loaded Mac at nice 19, 4 cargo jobs)
Source: `vendor/igneum-node-finality` (branch `finality-fixes`, from the proving head `8c0cff15`, which is difficulty v2
plus proving v0), commit `6aa69a45`. Each binary has its own target directory seeded by APFS clone from the
proving, v2-linux and v2 directories; the live binaries (`target-integration`, `target-linux`, `target-v2`, the seed's
`/opt/igneum/v4/bin`) were not touched.
| Platform | Path | sha256 (igneumd) | Build | Verified |
|---|---|---|---|---|
| Mac arm64 | `vendor/igneum-node/target-finality/release/igneumd` | `fe982a1d5be125ce89eafec87c5d0495373a4dc996614203ecf9a1555f8b2842` | release build, kaspad and igneum-miner, incremental on a target directory cloned from the proving worktree (about 10 min behind other agents' builds) | `--version` = igneumd 2.1.0; started on a private suffix with `{"finality_v3_activation_daa": 123456}` and printed `Finality rule v3 from the override file: active from checkpoint DAA score 123456`; drove the fast-time 3-node runs of the bench-log entry |
| Linux x86-64 (glibc 2.36) | `infra/cross/out-finality-v3/igneumd` (from `vendor/igneum-node/target-finality-linux`) | `7c100fc2b372139a52ed7ec47a579aec59ce5e3a5502b5f39065480d985a5c13` | 2,055 s (34 min) with cargo-zigbuild, zig 0.17.0, target directory cloned from `target-v2-linux`; 47,156,904 bytes | ELF x86-64 PIE, glibc 2.36; `strings` carries the field name, the switch line and the frozen-table LOCKED text; `version.txt` names 6aa69a45. Not run on a Linux host (the Hetzner network is gone) |
| Windows x86-64 | `vendor/igneum-node/target-finality-win/x86_64-pc-windows-gnu/release/igneumd.exe` (50484224 bytes) | `cc1d1001b5b39bba2f4890e947f049292dc7cd7fda472e6c7f65f5a3d018f4db` | 12 min 28 s (cross-build.sh, mingw, 4 jobs, target directory cloned from `target-v2`) | PE32+ x86-64; `strings` carries the field name, the switch line and the frozen-table text; the same DLL import set as the shipped v2 exe; it cannot run here |
`igneum-miner` was built alongside on each platform (the proving-branch miner: vmine, sign-record, the 3bfe346f
guards); it carries no v3 change and the devnet miners can stay on what they run.
## 3. The activation height N3, and how every node learns it
Same rule as difficulty v2 (the founder, 4 October 2026): `N3 = DAA score at the manifest publish + 10,800`, chosen as
`DAA now + 14,400` when the line is edited and checked (`N3 - DAA >= 10,800`) at publish. The switch keys on the
DAA score of the checkpoint block, which trails the sink by 20 to 50 blocks, so the first v3 checkpoint comes about
a minute after the sink crosses N3.
The packaged line carries both switches, the difficulty one at whatever value is live by then (it is empty in this
worktree; the difficulty v2 rollout sets it first):
```
NODE_OVERRIDE_PARAMS='{"difficulty_v2_activation_daa": N, "finality_v3_activation_daa": N3}'
```
The same object goes verbatim into the override files of Mac node 1, the observer node and the seed. The DAA score:
`IGNEUM_RPC=ws://127.0.0.1:28640 python3 infra/cloud-devnet/node/wrpc.py call getBlockDagInfo | python3 -c 'import json,sys; print(json.load(sys.stdin)["virtualDaaScore"])'`.
## 4. The order, with the exact commands
All Mac commands from `/Users/joshm/Projects/igneum`. Steps 1 to 3 are the package; 4 to 6 the hand-run nodes;
7 the watch. The difficulty v2 plan's steps apply with the binary paths below and the two-field override object.
### Step 1: fix N3 and cut the app version
```
sed -i '' "s/^NODE_OVERRIDE_PARAMS=.*/NODE_OVERRIDE_PARAMS='{\"difficulty_v2_activation_daa\": N, \"finality_v3_activation_daa\": N3}'/" packaging/mac/packaged-config.sh
# bump app/igneum-app/Cargo.toml, app/windows/version.h, packaging/windows/resources/igneum-app.rc; commit as igneum-labs, push
```
### Step 2: the Windows payload inputs
The inputs were staged on 4 October 2026 into a scratch downloads folder (section 7a) and NOT deployed. To publish
them to the real downloads host, when the founder says so:
```
IGNEUM_WIN_RELEASE=vendor/igneum-node/target-finality-win/x86_64-pc-windows-gnu/release IGNEUM_NODE_SRC=vendor/igneum-node-finality packaging/windows/push-inputs.sh
```
### Step 3: the Mac DMG and the manifest
```
NODE=vendor/igneum-node/target-finality/release/igneumd packaging/mac/build-dmg.sh
python3 -c 'import json; print(json.load(open("packaging/mac/build/dmg/Igneum Miner.app/Contents/Resources/igneum-app.json"))["node_override_params"])'
packaging/ota/publish-manifest.sh --version <v> --mac packaging/mac/dist/Igneum-Miner-<v>.dmg \
--notes "finality rule v3 from checkpoint DAA score N3" --activation-height N3 --deadline-note "finality v3" --deploy
packaging/windows/fetch-ci-artifacts.sh --deploy
```
### Steps 4 to 6: the observer node, the seed, Mac node 1
Exactly the difficulty v2 plan's steps 4 to 6 with `target-finality/release/igneumd` (Mac) and
`infra/cross/out-finality-v3/igneumd` (seed, sha256 of section 2) and the override file
`{"difficulty_v2_activation_daa": N, "finality_v3_activation_daa": N3}`. Each node's first lines must show BOTH
`Difficulty rule v2 from the override file: active from DAA score N` and
`Finality rule v3 from the override file: active from checkpoint DAA score N3`, and the `Finality v2 (...)` line ends
with `rule v3 (frozen table, certificate fold) from checkpoint DAA N3`.
### Step 7: the watch
| When | Where | Expected |
|---|---|---|
| after each restart | the node's first lines | both switch lines, N3 the same on every node |
| before N3 - 1,800 | log intake STATUS lines, the live page | both PCs on the new app version |
| N3 to N3 + 600 | node 1 and the observer logs, `getFinalityCheckpoints` | locks continue at the 30-s cadence; every LOCKED line from the first v3 checkpoint ends with `..% of the table frozen at lock <j>`; "certificate ... folded" lines appear within 3 s of determinations; the lock margin (`% of total`) rises from the 66.8 to 89% band of the morning toward the vote count of every connected key |
| N3 to N3 + 3,600 | the same, and the observer's finality events | 0 "CONFLICTING certificate", 0 "held by the frozen table" at info level (that line is debug), no node stuck; `finality_active` stays true |
| the next PC restart or miner outage | the same | a checkpoint with a key missing still locks when the rest hold two thirds of the frozen table; a 10-minute outage of a third of the keys, as the morning's hop.sh restarts caused, now pauses finality until they return or the window passes (3.7 item 2) |
If a node forks at N3 (its locks stop while others' continue, or it logs CONFLICTING): its override file lacks N3 or
carries another value. Fix the file, restart; the database re-syncs (same chain).
## 5. Rollback
Before N3: remove the field on every node and restart; nothing has happened. After N3: a node restarted without the
field evaluates rule v2 on every checkpoint from then on; in a connected network it agrees with the v3 nodes on every
lock, so a rollback is a plain restart of each node with the field removed, in any order. What a rollback gives up is
the pause under a partition (the morning's fork at 205 s of a 3/3 split comes back) and the folded certificates.
## 6. Pieces this plan adds to the repository
| Piece | What |
|---|---|
| node `consensus/core/src/config/params.rs` | `finality_v3_activation_daa` in `Params` and `OverrideParams` (default never on every network), test `override_params_carry_the_finality_v3_activation`, the fast-time test reads `IGNEUM_FAST_TIME_FILE` |
| node `consensus/core/src/finality.rs` | `FinalityParams::certificate_fold` (devnet 3, mainnet 6, serde default 3) |
| node `consensus/src/processes/finality.rs` | `frozen_table`, the Q5 test in `evaluate`, the fold round, certificate replacement in `ingest_certificate`, the two unit tests |
| node `kaspad/src/daemon.rs`, `consensus/src/consensus/services.rs`, `test_consensus.rs` | the switch's log line, the manager's activation argument, the test accessor |
| `infra/fast-time/override-60x.json`, `README.md` | both new fields, the rows explaining why the fold is not divided by 60 |
| `sim/finality_v2.py`, `sim/results_v2.md` | `P.frozen`, `rule_v3`, scenario M and its tables |
| `tools/finality-attacks/v3.mjs`, `lib/net.mjs`, `vote-timing.py`, `README.md` | the v3 runner, the env-driven library with a proxy delay, the cloud-log analysis |
| `infra/cross/out-finality-v3/` | the Linux v3 binaries and `version.txt` (not committed: binaries) |
## 7. What was rehearsed and what was not
No cloud rehearsal: the Hetzner network was destroyed before this work started. Before the devnet roll, the cheapest
real-network check is to recreate it and repeat the morning's partition with v3 on every node:
```
infra/cloud-devnet/create.sh && infra/cloud-devnet/provision.sh # 12 nodes, own chain (docs/plans/cloud-devnet.md)
# stage infra/cross/out-finality-v3/igneumd, write {"genesis_bits": ..., "difficulty_v2_activation_daa": 0, "finality_v3_activation_daa": 0}
# (rollout-v2.sh's override_json writes only the difficulty field; add the finality one before using it for v3)
infra/cloud-devnet/experiments/partition.sh sin 10 # expect: 0 locks on the minority, every interval on the majority, certificates with 12 of 12 votes
```
What stands in for it: the fast-time 3-node network of the bench-log entry (300-ms links, the 3/3 split past the old
bound with the heal, the 70/30 split, the fold before and after), the simulator's scenario M, and the unit tests.
### 7a. The Windows payload inputs (staged, not deployed)
`packaging/windows/push-inputs.sh --no-deploy` was run with `IGNEUM_WIN_RELEASE` at the v3 Windows release directory,
`IGNEUM_NODE_SRC` at the finality worktree and `IGNEUM_DLSITE` at a scratch copy of the downloads folder, so the live
downloads folder and host are untouched: `payload-inputs.zip` 64,294,811 bytes, sha256 `7c9d21e3df26fabf1761fa50fc627e628c46af80255d86429ef7d9d6ba5653a9`, manifest `node_source_commit` 6aa69a45, `igneumd.exe` cc1d1001b5b39bba2f4890e947f049292dc7cd7fda472e6c7f65f5a3d018f4db (50,484,224 bytes), `igneum-miner.exe` f1f9a7d96460e4d32e23aa3562c64058c5a2d4d87bb5058d287a697dc360fc1d (10,307,072 bytes), with the same CUDA and OpenCL workers, NVRTC and mingw DLLs as the morning's payload. The zip sits in the scratch folder only; the deploy command the script printed is `cd <dlsite> && npx vercel@latest --global-config ~/.config/igneum/vercel deploy --prod --yes`, to be run against the real folder after the step 2 command above, when the founder says so.