From 42b236040d1445fe98e117ca72d07445ae7d645c Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:19:57 +0000 Subject: [PATCH] Finality fixes F17 and F1: spec 3.10 rows, divergence rows and merge note, bench-log before/after, ledger statuses - spec 3.10: C5/3.8 (min_daa = weight window, window-filling report), Q4 (drawn aggregator at once, fallback for anyone), S1 (draw by weight), 3.9 (finality_reason) rows for fin-fixes da1eb889 - fork-divergence: four rows for the fin-fixes files and the merge note against the difficulty branch (hot swap is already in master) - bench-log: unit tests and the scenario 2 and 5 re-runs before (master) and after (fin-fixes) - fud-ledger: F17 and F1 status lines Co-Authored-By: Claude Fable 5.1 --- docs/bench-log.md | 36 ++++++++++++++++++++++++++++++++++++ docs/fork-divergence.md | 7 +++++++ docs/fud-ledger.md | 5 +++++ docs/spec/03-finality.md | 10 +++++----- 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/docs/bench-log.md b/docs/bench-log.md index a8fd882d..649b69d0 100644 --- a/docs/bench-log.md +++ b/docs/bench-log.md @@ -454,3 +454,39 @@ Simulator, seeds 7 to 9, Igneum / Kaspa's rule, criterion, verdict: (1) pool hop Test network, 3 `igneumd` nodes, honest 4-thread miner A on node 1 for 15 min, forger F (4 threads, about 50%) honest on node 2 for 5 min then on node 3 with the offset: Igneum, earliest allowed stamp: 0.97 blocks/s at difficulty 91,000 before, 0.24 blocks/s at 275,000 during forging and 0.20 at 341,000 in the last 5 min, forger offsets -121 to -566 s, hash unchanged (A 0.078, F 0.069 MH/s). Igneum, latest allowed stamp (+134 s): 0.98 blocks/s at 89,000 before, 0.59 at 170,000 during, 0.50 at 191,000 in the last 5 min (A 0.112, F 0.110 MH/s); the simulator's 50% cases give 0.12 at 9.9x and 0.56 at 1.8x. Kaspa's rule, earliest allowed stamp: 1.73 blocks/s on a 2.5x too easy genesis to block 600 at 210 s, then 1.02 blocks/s at 83,400 through ten minutes of -180 s stamps. 517, 767 and 1,454 blocks, 0 rejected. Proposed (README.md, diffs, not applied): Part A, timestamp rules: 10 s future tolerance (`FUTURE_TOLERANCE_MS`, a new constant so the past-median window keeps its 27 samples) and a floor at the selected parent's timestamp minus 10 s (`BACK_TOLERANCE_MS`) beside the past median; Part B, the chain steps of the short and epoch lanes measured on a sanitised running clock c(b) = max(c(p) + clamp(t(b) - c(p), -20 T, +20 T), t(b) - 60 T), step = min(c(b) - c(p), 20 T), stored per header, so forgeries telescope instead of cancelling; the long lane unchanged. Measured, both parts, 3 seeds: the 50% forger drifts the rate +0.7% (past), +0.9% (future), +1.1% (alternating), worst seed +2.7%; base profiles unchanged except down50 762 s against 782 s, warm-up 327 s against 381 s, polluted peak 11.4x against 8.0x; the other attacks identical. Either part alone fails (unchanged rule under the tight rules: -36% and -83% to past-stamping; the clock under the 132 s rules: collapse at 50%, a martingale once the forgery range exceeds half the cap). Part C for the flood: clamp the output at `MIN_DIFFICULTY_TARGET` = 2^128 beside the existing maximum, in both rules. Not done: the candidate in the node (simulator only; the per-header clock is a store change); DAG effects of forged stamps on red and merged blocks (one chain in the simulator); a rule change for the hopper's 0.7-point excess (none found that keeps the controller fast; README.md, scenario 1); Kaspa's rule under the flood (same hole, 17x slower, not run). + +## 2026-10-04 finality fixes F17 and F1: aggregators drawn by weight, first-month gate min_daa = window; attack scenarios 2 and 5 before and after (consensus-engineer) + +Machine: Apple M5 Max, rustc stable, macOS Darwin 25.6.0. Branch `fin-fixes` (worktree `vendor/igneum-node-fin-fixes`, from master `2a00ff55`), commit `da1eb889`. Build: `CARGO_TARGET_DIR=target nice -n 19 cargo build --release -j 4 -p kaspad -p igneum-miner --features kaspad/igneum-pow`. Tests: `cargo test --release -p kaspa-consensus-core -p kaspa-consensus -- finality`: 6 of 6 in consensus-core (`sortition_threshold`, `sortition_is_by_weight_not_key_count` with 200 dust keys and 6 real ones, `first_month_rule_is_the_full_window`, the three pre-existing), 1 of 1 in consensus (`processes::finality::tests::no_certificate_while_the_window_is_filling`, a 150-block TestConsensus chain at a 60-DAA window where one key holds all the weight: nothing certifies under DAA 60, a hand-built early certificate is refused, every checkpoint from DAA 60 locks). The live devnet (26610, 26611, 26640, 26641, 28640) and the other agents' nodes (26680, 27700 to 27720, 28680) were never touched. + +The two diffs. (1) F17: `is_aggregator(output, weight, total_weight, aggregators)` is eligible when `output x total < aggregators x weight x 2^64` (was `output x voters < 8 x 2^64`, drawn per key); `ingest_vote` passes the key's weight and the table total. A key split into n parts holds n thresholds that sum to the one it had; a key without weight never draws; a key at 1/8 of total or more always draws. A node that serves a drawn aggregator aggregates at once; any other node after `checkpoint_depth + aggregator_fallback` (15) DAA seconds, so anyone MAY aggregate stays the liveness fallback. (2) F1: `min_daa = weight_window` (mainnet 2,592,000, devnet 7,200); `evaluate` never locks and `ingest_certificate` refuses any certificate while the checkpoint's DAA score is below it; the node logs and reports "finality not active, window filling, N of M" (`finality_reason`, `window_filled_daa`, `window_full_daa` on `getFinalityCheckpoints`). + +Re-run of scenarios 2 and 5 (`tools/finality-attacks`, hostile `igneum-miner` from the fin-attacks worktree, unchanged; it drove the fixed node without modification because the new RPC fields are additive). Six voters on one node, `skip_proof_of_work`, 600 s per run. The harness copy used for the runs is `/tmp/igneum-fin-fixes/harness` (`lib/net.mjs` with ports, data directory, network suffix and the finality override taken from the environment; `rerun.mjs` with the s2 and s5 measurements below); the repo harness was not edited, and its s2 pass test still reads "voters > 8 means per-key sortition", which is now wrong and needs the by-weight test below. Finality override for every run: interval 30, depth 20, window 1,800 DAA, dust 5, presence 20, 8 aggregators, ban 1,800; `min_daa` 0 for the before runs (the master default) and 1,800 for the after runs (the fixed rule, min_daa = window), fallback 15. The window was shortened from 7,200 to 1,800 so it fills inside a 10-minute run; the rule under test is the equality, not the number. Before = fin-attacks `igneumd` (master code, built 3 Oct 23:25) on ports 28100 and 28300, network ids igneum-devnet-801 and 803; after = fin-fixes `igneumd` on 28500 and 28700, ids 805 and 807. Results in `/tmp/igneum-fin-fixes/{before,after}-{s2,s5}/`. + +Scenario 2, Sybil dust (one miner mints 200 keys at 4 blocks and 200 at 6, dust 5; 3 honest voters at 1/3 each; only the honest keys vote, so the measurable is how many honest keys the node records as drawn aggregators per checkpoint). "Crowded" = checkpoints with more than 8 voters above dust (98 of about 127 in each run, mean 125 voters). Expected honest seats per crowded checkpoint: per key, `3 x min(1, 8 / voters)`; by weight, `3 x min(1, 8 x w / T)` with w the honest key's weight. + +| | Before (master) | After (fin-fixes) | +|---|---|---| +| Dust keys with weight or a vote | 0 of 200 | 0 of 200 | +| Total weight = sum of voter blocks | 1,798 = 1,798 | 1,798 = 1,798 | +| Honest weight share, crowded checkpoints (mean) | 32.3% | 28.8% | +| Expected honest seats per crowded checkpoint, per-key draw | 0.33 | 0.35 | +| Expected honest seats per crowded checkpoint, by-weight draw | 1.67 | 1.55 | +| Measured honest seats per crowded checkpoint | 0.32 | 1.61 | +| Locks | 33, first at DAA 629 (a young window held the honest keys alone) | 25, first at DAA 3,059 (window full at 1,800; the silent 1,200 blocks of sybil weight held the honest keys under the 56.7% floor until they aged out, `finality_reason` "paused" from DAA 1,800 to 3,059, then "active") | +| Verdict | sortition FAIL (per key: 0.32 against 0.33) | sortition PASS (by weight: 1.61 against 1.55) | + +Scenario 5, pulsed miner (five steady voters at 1/6, one burster at 1/6 pulsing 10x for 20 s of every 120 s). + +| | Before (master) | After (fin-fixes) | +|---|---|---| +| Burster weight share vs block share over the run | 30.9% vs 32.0%, ratio 0.966 | 30.8% vs 32.1%, ratio 0.959 | +| Checkpoints determined / locked | 148 / 148 | 148 / 88 | +| First lock | checkpoint 1 at DAA 29, built "by 1 of 1 voters, weight 22 (total 22)", the aggregator and sole voter above dust the burster's key (its first 20-s burst); checkpoints 2 and 3 locked at 3 of 3 and 4 of 4 voters as the others cleared dust | checkpoint 61 at DAA 1,829 (the first checkpoint at or above min_daa 1,800), 5 of 6 voters, 84.7% of active and of total | +| Locks with the checkpoint under min_daa 1,800 | not gated (checkpoints 1 to 60 all locked) | 0 | +| Locks carried by one voter above dust | 1 (checkpoint 1) | 0 | +| `finality_reason` over the run | field absent | "window filling, 92 of 1800" ... "1448 of 1800" at 180 s, "paused" at 240 s (window full, first lock pending), "active" from 300 s; 59 "window filling" determination lines in the node log | +| Conflicting certificates | 0 | 0 | +| Verdict | amplification PASS, lock-alone FAIL (F1) | amplification PASS, lock-alone PASS | + +Notes. The fallback path ("fallback: any node may aggregate") fired 0 times in both after runs: every voter on the single node is local and, with six keys of similar weight, each is drawn at every checkpoint, so every certificate named a drawn aggregator. The "paused" reading between the window filling and the first lock is the report's label for "window full, no lock yet"; it lasted one sample in s5 and five minutes in s2 (the floor against silent sybil weight, 3.3.1). The repo harness `tools/finality-attacks/run.mjs` keeps its pre-fix s2 and s5 criteria and should adopt the two measurements above; the fin-attacks miner prints no `FINALITY` line (that is the fin-fixes miner). diff --git a/docs/fork-divergence.md b/docs/fork-divergence.md index 75f357eb..ce75f8f5 100644 --- a/docs/fork-divergence.md +++ b/docs/fork-divergence.md @@ -46,6 +46,11 @@ The rule is `docs/spec/03-finality.md` (implementation notes in its section 3.10 | `kaspad/src/args.rs` | `--devnet-suffix=N` (network id `igneum-devnet-N`, own data directory and handshake magic) | A private test network beside the shared devnet on one machine | None | Pure addition. | | `igneum/miner/src/main.rs` | `Identity` (BLS key from the identity label, `vote_key_hash` = hash of the key, key reveal in every template's extra data); `Voter`: once a second reads `getFinalityCheckpoints`, signs every new unlocked checkpoint (vote plus sortition proof) and submits it, prints `VOTE` and `LOCK` lines and a `VOTER SUMMARY` with lock latency as seen over RPC; `--no-vote`, `--equivocate` (also signs a wrong hash at every index; the node strips the key); the genesis hash is discovered from the node (pruning point walked to the parentless block) so a private test network with its own genesis mines correctly | The miner is the signer (the node never holds a key) | None to consensus | The Windows launcher is unchanged: the positional identity label is the key label, so `nvidia--` identities keep working, with new `vote_key_hash` values (weight restarts under the new hashes). | +| `consensus/core/src/finality.rs` (fin-fixes, 4 Oct 2026) | `is_aggregator(output, weight, total_weight, aggregators)`: eligible when `output x total < aggregators x weight x 2^64` (was `output x voters < aggregators x 2^64`, a per-key draw); `FinalityParams.min_daa` = `weight_window` on both networks (mainnet 2,592,000, devnet 7,200; was 3,600 and 0) with `may_certify` and `window_filling`; new field `aggregator_fallback` (15 DAA s on both networks); `CheckpointsReport` gains `finality_reason`, `window_filled_daa`, `window_full_daa`; tests `sortition_is_by_weight_not_key_count` (200 dust keys plus 6 real ones) and `first_month_rule_is_the_full_window` | Ledger F17 (the 8 aggregators were drawn per key, so a 200-key splitter crowded real signers out) and ledger F1 (a 20-s burst locked checkpoints 1 to 10 alone on a young window), both measured by `tools/finality-attacks` on 4 Oct 2026; spec 3.4 S1, 3.8 and 3.10 | Medium: `min_daa` changes when the first certificate can form (a devnet needs 7,200 DAA s of history before any lock; a restarted devnet node reports the window as filling until its first virtual resolution); `FinalityParams` gains a field, so an `override-params-file` that sets `finality` must name `aggregator_fallback` | Pure addition inside the Igneum file. | +| `consensus/src/processes/finality.rs` (fin-fixes, 4 Oct 2026) | `ingest_vote` passes the key's weight and the table total to the draw; `ingest_certificate` refuses a certificate whose checkpoint is under `min_daa`; `evaluate` never locks under `min_daa` and aggregates at once only when a local signer is a drawn aggregator, else after `daa(C_i) + checkpoint_depth + aggregator_fallback`; the determination log and `checkpoints_report` carry "finality not active, window filling, N of M"; test module `tests::no_certificate_while_the_window_is_filling` (TestConsensus chain of 150 blocks at a 60-DAA window, one key with all the weight) | The same two ledger entries, applied where the node decides | Medium: the fallback adds up to 15 DAA s to a lock on a node that serves no drawn aggregator (none on a network of 8 or fewer equal voters, where everyone is drawn) | The test module uses `TestConsensus` and `ConsensusApi` only; conflicts with the difficulty branch are none (it does not touch this file). | +| `rpc/core/src/model/finality.rs`, `rpc/grpc/core/proto/rpc.proto`, `rpc/grpc/core/src/convert/message.rs`, `rpc/service/src/service.rs` (fin-fixes, 4 Oct 2026) | `GetFinalityCheckpointsResponse` gains `finality_reason`, `window_filled_daa`, `window_full_daa` (proto fields 8 to 10, serializer version 2) | Spec 3.9: the flag carries its reason | Low: a pre-fix gRPC client ignores the new fields (the fin-attacks miner drove the fixed node unchanged) | Additive. | +| `igneum/miner/src/main.rs` (fin-fixes, 4 Oct 2026) | The voter prints `FINALITY (active=...)` whenever the node's reason changes | A window-filling first month is visible in the miner log | None to consensus | Line-local; the difficulty branch edits the same file elsewhere. | + Devnet compatibility and cut-over: the devnet genesis, network id and ports are unchanged, so a v2 node syncs the chain mined so far. Two things are one-way: (1) a v2 node accepts coinbase payloads up to 16,384 bytes and a v1 node accepts 204, so once a v2 miner votes, the blocks that carry a finality section are invalid to every v1 node; (2) a v2 node's p2p protocol version is 12 and it only sends finality messages to version 12 peers, so a v1 node peers but never sees votes. Cut over every node (node 1, the observer peer) and the Windows package together. ## R3 fixes, M15: PoW cost before validation (branch `r3-fixes`, 3 Oct 2026) @@ -63,6 +68,8 @@ Tests: `kaspa-pow --features igneum-pow` cap tests (`one_build_per_seed_pair_und Merge note for the finality branch (which files overlap): `consensus/core/src/errors/block.rs` (both add a `RuleError` variant), `consensus/src/pipeline/header_processor/processor.rs` (finality adds no code here but the hot-swap branch does; the reorder is the conflict), `protocol/flows/src/flow_context.rs` and `protocol/flows/Cargo.toml` (both add to `FlowContext` and the manifest). No shared lines in any of them; each is additive on both sides. +Merge note for `fin-fixes` (4 Oct 2026, branched from master `2a00ff55`, which already contains the hot-swap branch, so no hot-swap conflict exists): against the difficulty branch (`3ea7a3e3`, files `consensus/core/src/config/params.rs`, `consensus/core/src/igneum.rs`, `consensus/src/consensus/services.rs`, `consensus/src/processes/{difficulty,window}.rs`, `igneum/miner/src/main.rs`, the integration tests) the only shared file is `igneum/miner/src/main.rs`, where fin-fixes adds one field to `Voter` and one `println!` in `tick`, both line-local to the voter, so the merge is textual at worst. Fin-fixes does not touch `params.rs`; the difficulty branch's `Params` edits do not touch `finality`. Merge fin-fixes first, then difficulty, and re-run `cargo test -p kaspa-consensus-core -p kaspa-consensus -- finality` after each. + ## The rename (3 Oct 2026): what a miner, a user or an operating system sees Trigger: macOS asked the project lead whether "kaspad" may access the local network. Everything visible from outside the source tree now says Igneum. Internal crate names, module paths, protobuf packages and Rust identifiers keep their upstream names (next table) so `git merge` against rusty-kaspa stays mechanical. diff --git a/docs/fud-ledger.md b/docs/fud-ledger.md index 0d1a14b4..dd1d6724 100644 --- a/docs/fud-ledger.md +++ b/docs/fud-ledger.md @@ -1456,3 +1456,8 @@ Status: Fixed (4 October 2026). Spec 7.5 items 1 to 4; `igneum/exec/src/pgas.rs` Answer: Correct, medium. The 3 October attack run showed it: a 9,000-call modexp loop ran 10.85 ms of native work, was skipped with `BlockProvingBudget`, paid nothing, and the sender's 14,000 and 20,000 loops were never includable behind it. Four rules now hold. (1) pgas is metered incrementally against the including block's remaining `B_p` and the transaction halts before the opcode or precompile that would cross it, so native work is bounded by `B_p`. (2) An aborted transaction is executed, not skipped: status 0, charged for the gas and pgas consumed to the abort, nonce advanced, so a later copy skips by the nonce rule at one account read and the sender's later nonces are free. (3) The mempool refuses a transaction whose estimated pgas exceeds `B_p` and the template packs by the estimate. (4) `eth_estimateGas` names the pgas when the cap is hit and `igneum_estimateGas` returns both dimensions. Measured after the fix: the same loop is refused by the mempool; a hostile miner's inclusion is cut at 29,998,593 pgas after 11.4 ms, the sender pays 0.0394 IGN, the nonce advances, a second inclusion costs 35 us, the next nonce executes; `igneum-exec-diff` 0 mismatches. What remains open is node policy, not consensus: the admission estimate costs up to `B_p` of simulation per heavy submission, under the RPC's state lock. Evidence: `docs/bench-log.md`, 4 October 2026 "execution layer attack fixes" (before and after table); the 3 October attack entry, F-exec-B; unit tests `executor::over_budget_pgas_is_aborted_charged_and_the_nonce_advances`, `executor::the_cap_is_the_remaining_block_budget`, `executor::estimate_reports_the_cap`, `pool::estimated_proving_gas_is_bounded_by_the_block_proving_limit`, `pool::template_never_exceeds_the_remaining_proving_budget`. + +## Status updates, 4 October 2026 (branch fin-fixes, commit da1eb889) + +- **F17** (keys are free, the draw is per key). Status: Fixed in the node (4 October 2026). `is_aggregator` draws the 8 aggregators by weight, `output x total < 8 x weight x 2^64`, so a splitter holds the tickets its weight buys and no more; spec 3.10 S1 row; unit test `sortition_is_by_weight_not_key_count` (200 dust keys plus 6 real ones); attack harness scenario 2 re-run: honest keys drew 1.61 seats per crowded checkpoint against 1.55 expected by weight, where master drew 0.32 against 0.33 per key (`docs/bench-log.md`, "finality fixes F17 and F1"). Still open from this entry: the client's one-key default, S2 (O-3.5), the bitmap size (O-3.12). Was: Rule fixed (spec 7.2 and W6), node per key. +- **F1** (finality is attackable for the first month). Status: Fixed in the node on spec 3.8's recommended rule (4 October 2026); the litepaper statement and the launch-month simulation (O-3.1) remain. `min_daa = weight_window` (2,592,000 DAA s on mainnet, 7,200 on devnet): no checkpoint certifies and no certificate is accepted while the window behind it is younger than its full length, and the node reports "finality not active, window filling, N of M"; spec 3.10 C5 row; unit test `no_certificate_while_the_window_is_filling`; attack harness scenario 5 re-run: master locked checkpoint 1 at DAA 29 by the burster's key alone (1 of 1 voters, 22 of 22 weight), fin-fixes locked nothing under the window and first at checkpoint 61 (DAA 1,829) with 5 of 6 voters (`docs/bench-log.md`, same entry). Was: Conceded, not yet stated in the litepaper; experiment and rule change scheduled. diff --git a/docs/spec/03-finality.md b/docs/spec/03-finality.md index 5fcb18ab..c6acffda 100644 --- a/docs/spec/03-finality.md +++ b/docs/spec/03-finality.md @@ -153,7 +153,7 @@ A certified checkpoint overrides the heaviest chain, so fresh hashrate cannot re ## 3.10 Implementation notes (devnet v2, 3 October 2026) -Status of this section: Implemented in `vendor/igneum-node` (reading guide in `docs/fork-divergence.md`, "Finality v2"), tested on a private four-miner test network and as a follower of the live devnet (`docs/bench-log.md`, entry "igneum-node devnet v2"). Where the implementation departs from the rule above or fills a gap it leaves, the departure is listed here, not silently. +Status of this section: Implemented in `vendor/igneum-node` (reading guide in `docs/fork-divergence.md`, "Finality v2"), tested on a private four-miner test network and as a follower of the live devnet (`docs/bench-log.md`, entry "igneum-node devnet v2"). Where the implementation departs from the rule above or fills a gap it leaves, the departure is listed here, not silently. Update of 4 October 2026 (branch `fin-fixes`, worktree `vendor/igneum-node-fin-fixes`, after the attack harness of `tools/finality-attacks`): the S1 draw is by weight (ledger F17) and the first-month rule of 3.8 is the `min_daa` parameter (ledger F1); the rows for C5, Q4, S1 and 3.9 below say what landed. Measured in `docs/bench-log.md`, entry "finality fixes F17 and F1" of 4 October 2026. | Clause | Implementation | Departure or gap | |---|---|---| @@ -164,17 +164,17 @@ Status of this section: Implemented in `vendor/igneum-node` (reading guide in `d | C2 | BLS signature over `"igneum-vote-v1/" \|\| chain_id \|\| 0 \|\| index \|\| hash(C_i)` under `IGNEUM_VOTE_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_`; the chain id is the prefixed network name (`igneum-devnet`, `igneum-devnet-7`); votes are p2p message 70 and ride in the coinbase extra data of every block | | | C3 | Certificate = index, checkpoint, voter count, signer bitmap over the canonical voter list (keys above dust and not stripped, sorted by key hash), aggregate signature, aggregator key hash and sortition proof. Every template carries the certificates not yet in its past | The validity rule (a block whose selected chain misses a certified checkpoint is invalid) is NOT enforced; only fork choice (F1, F2) is | | C4 | A second certificate at an index for another block is kept and logged (`conflicting_certificates`) | Not published as evidence. The rule is now fixed by 3.11 item 4 (the node keeps the certificate it verified first, never re-evaluates it, and reports the conflict); the node does not yet clear `finality_active` or expose `finality_conflict` when the pair appears | -| C5 | `min_daa` parameter: 3,600 on mainnet, 0 on devnet | The first-month rule of 3.8 is not implemented | +| C5, 3.8 | `min_daa` = `weight_window` (2,592,000 DAA s on mainnet, 7,200 on devnet; a unit test pins the equality). `evaluate` never locks, and `ingest_certificate` refuses a certificate from any source, while the checkpoint's DAA score is below `min_daa`; the node logs "finality not active, window filling, N of M" at every determination until the sink's DAA score reaches `min_daa` and reports the same through `getFinalityCheckpoints` (`finality_reason`, `window_filled_daa`, `window_full_daa`). Unit test `processes::finality::tests::no_certificate_while_the_window_is_filling`: one key holding 100% of the weight signs every checkpoint of a 150-block chain at a 60-DAA window; nothing certifies below DAA 60, a hand-built certificate at an early index is refused, every checkpoint from DAA 60 locks (fin-fixes, 4 October 2026) | Implemented on 3.8's recommendation ahead of the launch-month simulation (O-3.1), which is still not run; gate 3 can lower the gate but not remove it without reopening ledger F1. The sink's DAA score the report compares is the one the virtual processor last handed the manager, so a restarted node reports the window as filling until its first virtual resolution | | Q1, Q2 | Presence window 20 indices on devnet (240 mainnet). Block reading: participation counts the indices in `[i - P, i - 1]` at which a vote by the key is carried by any block, blue or red, in the past of C_i; a key whose first block in the window is younger than P x 30 DAA seconds counts the full window; every template carries up to 48 votes not already in its past, certificates and evidence first | The per-block vote bound (48) is the devnet value of O-3.3. Participation is credited for any vote by the key at the index, whatever block it names; 3.11.1 requires the vote to name the checkpoint on the crediting chain, else a key can stay in the active denominator by voting for blocks of its own and never add to a certificate (O-3.19) | | Q3 | Integer tests: `3 x signed x P >= 2 x active_num` (active_num = sum of weight x participation count) and `30 x signed >= 17 x total`, both at C_i; bans known at evaluation time are applied to the voter list | | -| Q4 | No grace: any node aggregates and gossips a certificate the moment the votes it has seen meet Q3 (anyone MAY aggregate); the certificate names a local eligible voter when the node serves one, else a zero aggregator | Aggregator-only publishing and the grace timer (O-3.4) are not implemented; a certificate therefore often carries fewer signers than the votes that exist (the lock still meets Q3) | -| S1 | VRF output = SHA-256 of the voter's BLS signature over `"igneum-sortition-v1/" \|\| chain_id \|\| 0 \|\| index \|\| hash` under the sortition tag (unique per key and message, so the signature is the proof); eligible when `output x voters < 8 x 2^64`, so with 8 or fewer voters everyone is eligible | | +| Q4 | No grace. A node that serves an eligible aggregator (S1) aggregates and gossips a certificate the moment the votes it has seen meet Q3, naming that aggregator; any other node waits until the sink is `checkpoint_depth + aggregator_fallback` DAA seconds past the checkpoint block (fallback 15 on both networks) and then aggregates with a zero aggregator (anyone MAY aggregate, the liveness fallback; fin-fixes, 4 October 2026) | The grace timer (O-3.4) is not implemented: the fallback bounds how long a checkpoint waits for its drawn aggregators, it does not hold a certificate open for late votes, so a certificate still often carries fewer signers than the votes that exist (the lock still meets Q3) | +| S1 | VRF output = SHA-256 of the voter's BLS signature over `"igneum-sortition-v1/" \|\| chain_id \|\| 0 \|\| index \|\| hash` under the sortition tag (unique per key and message, so the signature is the proof); eligible when `output x total_weight < 8 x weight x 2^64`, drawn by weight (W6, ledger F17, fin-fixes 4 October 2026): the expected number of aggregators is 8 by weight whatever the key count, a key with no weight never draws, a key holding 1/8 of total weight or more always does (so with 8 or fewer equal voters everyone is eligible). Unit test `sortition_is_by_weight_not_key_count`: 200 dust keys draw nothing, 6 real keys draw `sum min(1, 8 w / T)`, 16 equal keys draw 8.00, a key split into 10 or 200 parts draws what it drew whole | Was `output x voters < 8 x 2^64` (per key) until 4 October 2026; measured on the attack harness (`docs/bench-log.md`, "finality v2 attack harness" S2, then "finality fixes F17 and F1"). A key above 1/8 of total weight that splits itself gains seats (its single ticket was capped at 1); seats carry no reward and no power, since anyone MAY aggregate and Q3 is tested by weight | | S2 | Not implemented (sub-user sortition above 8,192 voters) | | | F1, F2 | In `resolve_virtual` the highest locked checkpoint that is in the future of the depth-based finality point and in the past of some body tip replaces the finality point: tips outside its future are not sink candidates | A lock that no body tip passes through is logged and ignored for that resolution | | F3 | Not implemented: the pruning point and `virtual_finality_point` ignore locks | Must land before any pruning network | | F5 | Not implemented (trusted certificate at start) | | | 3.6 | A second vote by one key at one index for another block is evidence: the key's weight is zero until `detection DAA + ban` (7,200 DAA seconds on devnet), the evidence is carried in blocks and re-detected from blocks | Node-local detection timestamps the ban with the sink's DAA score; a block-carried evidence uses the carrying block's DAA score | -| 3.9 | `getFinalityCheckpoints` reports `finality_active` (a lock within the last P indices) and the latest lock | `last_certified` as a DAA score is not reported; the flag carries no reason (3.11 item 3 names three: first month, pause, conflict), so an operator cannot tell a pause from a conflict without the log | +| 3.9 | `getFinalityCheckpoints` reports `finality_active` (the window is full and a lock exists within the last P indices), the latest lock, and since 4 October 2026 `finality_reason` (`active`, `window filling, N of M` with N the sink's DAA score capped at `min_daa` and M `min_daa`, or `paused`) with `window_filled_daa` and `window_full_daa`; the miner prints a `FINALITY` line whenever the reason changes | `last_certified` as a DAA score is not reported; the conflict reason of 3.11 item 4 (two certificates at one index) is not reported (O-3.17), so a conflict still reads as `active` or `paused` | | 3.11 item 6 (seed source) | The devnet keys the hourly program on the header's own `daa_score` (`epoch_seed`, `docs/review/round-3-2026-10-03.md`, R3.26), not on a checkpoint block | The `seed_source` rule (section 4.3 with the uncertified fallback of 3.11 item 6) is not implemented; nothing on the devnet exercises a seed during a finality pause | | 3.11 test table | The four-miner test network of the bench-log entry is the only measurement on a real DAG: 93 checkpoints, 0 conflicting certificates, one equivocation strip, one 12-checkpoint pause under the floor, one heal | d = 20, presence 20 indices and a 7,200-s window are devnet values; the measured pause and heal are at those values, not the mainnet ones |