igneum/docs/plans/finality-v3-rollout-devnet.md

14 KiB

Finality rule v3 on the live devnet: rollout plan, 4 October 2026 (evening)

Prepared by the finality engineer after the project lead'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 project lead, 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 project lead 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 project lead says so.