igneum/docs/spec/03-finality.md
igneum-labs 51d61f0dca Finality v2: fork reading guide, spec implementation notes, bench entry, observer checkpoints, live page locks
- docs/fork-divergence.md: "Finality v2" table (every file, risk, merge note), decisions
- docs/spec/03-finality.md: section 3.10 implementation notes, clause by clause
- docs/bench-log.md: test-network results (72 of 72 steady locks, median 0.80 s; equivocation
  strip; partition: 0 locks at 39.6% of total with the floor binding, heal in 30 s), follower
- tools/observer: live_checkpoints table, FinalityLock subscription, "checkpoint N locked" events
- site: /api/live adds checkpoints and locked/final flags; /live draws the lock ring and final line

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 21:54:24 +00:00

30 KiB

Igneum protocol specification, section 3: sustained-mining finality, version 2

Spec version 0.1, 3 October 2026. Status of this section: Designed. Simulated at the checkpoint level with latency, partitions and eclipses but without a DAG (sim/finality_v2.py, sim/results_v2.md, docs/bench-log.md entry "sim/finality_v2.py"). Not implemented. Not externally reviewed. Gate 3 is the external review that tries to break it.

The rule is exactly the design document's "Finality rule, version 2, after the second hostile review" with the quorum floor added on 3 October 2026 after the second simulation. CLAUDE.md's FINALITY RULE V2 paragraph is the short form. Every quantity is in blue score, DAA seconds or past-median time, never wall-clock (section 0.6). Past-median time, mt(B), is the median timestamp of a block's sampled past (Kaspa's consensus/src/processes/past_median_time.rs, PAST_MEDIAN_TIME_SAMPLE_INTERVAL 10, kept): consensus data computed from the block's past, which a burst of blocks cannot run fast the way it runs DAA score (ledger M14 and F14, round 3).

Two statements frame everything below. Finality is miner-only and self-contained: no stake, no bond, no other chain, no committee that is not the set of recent miners. Equivocation costs history, not coins, because there are no coins to slash.

3.1 Weight

  • W1. Every block header names a vote key by vote_key_hash (section 2.4): the hash of a BLS12-381 G1 compressed public key. The first block that uses a key reveals the key in its coinbase payload. A header whose vote_key_hash has never been revealed is valid; the key simply cannot vote until it is revealed.
  • W2. Weight is counted in blocks and denominated in past-median time (rule of 3 October 2026, ledger M14 and F14, round 3; the simulated form is kept below). Cut the trailing 30 days of past-median time before C, (mt(C) - 2,592,000 s, mt(C)], into 43,200 buckets of 60 s; a blue block in C's past falls in the bucket that holds its own past-median time. The weight of key k at C is the sum over buckets of k's share of the blue blocks in that bucket (an empty bucket contributes 0 to everyone). A bucket is worth 1 whatever it holds, so 50x the blocks in one minute is one minute of weight, and a retarget lag can neither inflate a key's count nor age the window. No damping, no cap, no floor. Blocks are the only thing in proof of work that cannot be forged, so weight is counted in blocks; time is the denominator because blocks per unit of DAA time is exactly what a lagging controller lets a renter buy (section 2.3; docs/review/round-3-2026-10-03.md, "Tonight's devnet"). As simulated (sim/results_v2.md, every run): the number of blue blocks in C's past whose header names k and whose DAA score is in (daa(C) - 2,592,000, daa(C)]. The two forms agree whenever the controller holds the target; O-3.14 runs the simulation under both with the DAA in the loop and confirms or reverts this rule at gate 3. The damped rule of version 1 was removed because its 2x cap was defeated by splitting into free keys (sim/results.md table C; docs/bench-log.md finality_sim entry).
  • W3. Dust: a key with fewer than 100 blue blocks in the window (a block count, not bucket weight) is not a voter and is in no denominator. Measured consequence (sim/results_v2.md A and G): every honest key in a 1,000-key Pareto network clears 100 blocks by day 20 from zero history and the smallest new keys need up to 25 days after a doubling; a 9x renter's victims lose about one point to dust (B).
  • W4. Steady state: weight equals hashrate. Simulated correlation 1.00000 at day 60, Gini equal to three figures, weight-to-hash ratio within 0.82 to 1.12 for every key (sim/results_v2.md A).
  • W5. Key succession: a message signed by the old key naming a new key, included in any block, moves the old key's weight history to the new key once. The old key is dead thereafter: its later blocks earn nothing and its votes are invalid.
  • W6. Keys are free. Nothing in the protocol prices a vote key and the devnet launcher mints one per worker process (ledger F17, round 3). Weight is the only Sybil-resistant quantity in this specification, so every rule that draws from the voter population draws by weight and never per key: W2 (no damping, no per-key cap), the shard sortition of section 7.2 (drawn by weight since 3 October 2026) and the sub-user sortition of S2. A rule that counts keys is a rule a splitter wins.

The headline arithmetic (W2 with constant hashrate): an attacker with share a of hashrate for t days holds weight share (t/30) x a/(1+a), verified by simulation to 0.04 points for a = 1 and 2 (sim/results_v2.md B).

Attacker hashrate relative to honest (a) Reaches 1/3 (can veto) Reaches 2/3 (locks alone)
1x (50% of blocks) day 20.0 never (ceiling 50%)
2x (67%) day 15.0 day 30.0
4x (80%) day 12.5 day 25.0
9x (90%) day 11.1 day 22.2
unbounded (100% of blocks, honest miners gone) day 10 day 20

All in public, on the hashrate charts. 51% never reaches 2/3 while honest miners keep mining.

3.2 Checkpoints

  • C1. Checkpoint i is the selected-chain block at blue score 30 i. It is determined once the virtual's blue score reaches 30 i + d. d = 60 at 1 block per second is a placeholder (ledger F7): the gate 3 devnet records the reorg-depth distribution and sets d so that a vote split at one index is rare and self-heals at the next. d scales with block rate. A lock lands about 90 to 120 s after a transaction (Designed; simulated lock latency after the checkpoint block is median 2.5 s, p99 4.6 s at a 2-s inter-region delay, sim/results_v2.md A).
  • C2. A vote is a BLS signature over (chain_id, i, hash(C_i)) under a fixed domain-separation tag. Votes gossip as their own message type.
  • C3. A lock certificate for index i is an aggregate BLS signature over one checkpoint block hash with a bitmap of signers, whose signed weight meets Q3. Every block carries the highest certificate its producer knows. A block whose selected chain does not pass through every certified checkpoint in its past is invalid (section 2.4).
  • C4. A node holding a certificate for index i rejects any other certificate for index i and publishes the pair as evidence (section 3.6).
  • C5. No certificate may form in the chain's first 3,600 DAA seconds (design document). See 3.8 for the proposed first-month rule.

3.3 Quorum

  • Q1. Presence window P = 7,200 s of past-median time: index j is in the window of index i when 0 < mt(C_i) - mt(C_j) <= 7,200. About 240 indices at the target rate. Denominated in median time, not indices or DAA seconds, so a burst of blocks cannot shrink the window to minutes (ledger M14 and F14, round 3; the simulation ran with a fixed 240 indices).
  • Q2. Participation of key k at index i, "block reading" (rule of 3 October 2026, ledger F3): the number of indices j in the presence window of i (Q1) for which a valid vote by k at index j appears in the past of C_i, divided by the number of indices in that window, capped at 1. A vote "appears in the past of C_i" when it is carried, as a vote or inside a certificate, by any block in the past of C_i, blue or red. A key whose first block is younger than the presence window counts 1. Votes are block payload: every block MUST carry every valid vote its producer has received for i_b or an index in its presence window (i_b the highest checkpoint index determined in the block's past) that is not already carried by a block in its past, up to the per-block vote bound of O-3.3; votes for one (index, checkpoint hash) pair MAY be aggregated inside the block into one BLS signature with a bitmap. A block that omits a vote it has received is not invalid (no node can prove what another received); the rule binds honest producers and the argument of 3.3.2 says why that is enough. This reading is objective, since every node computes it from the same past of C_i, and self-healing, since a missed index rolls out of the window after two hours of median time. The "cert reading" of the simulation (only votes inside certificates count) is a subset of it and was the reading the simulation ran with; the node-local "seen" reading is rejected as not objective and the "frozen" reading as total with extra steps (sim/results_v2.md, "Recommended parameters").
  • Q3. Active weight at i is the sum over voters of weight x participation. Total weight at i is the sum of weight over all keys above dust. A certificate for index i locks when the unscaled weight of its signers is at least 2/3 of active weight AND at least 56.7% of total weight (the floor 0.85 x 2/3 = 17/30). Both tests use weights and participation computed at C_i, so any node can verify a certificate from C_i's past.
  • Q4. Certificate grace: an aggregator closes a certificate at the later of quorum time and t0 + grace, where grace MUST be at least 3x the worst honest one-way network delay (15 s in the simulation at a 2-s delay; at a 5-s delay the slowest region already lost 1.7 points of participation to a 15-s grace, sim/results_v2.md A). The value is Open (O-3.4) and does not affect validity, only which votes a certificate carries.

3.3.1 Why the floor, from the simulation

Both denominators alone fail (sim/results_v2.md, seed 7, seed 11 agrees on B and E):

Scenario Active alone (cert reading, P = 240) Total alone Active + floor 0.85
E: 50/50 honest partition, no attacker, 150 min 90 conflicting locks, first at 60 min (240 with instant DAA retarget, first at 30 min) 0 0
E: 60/40 honest partition, 360 min, with retarget 624 conflicts 0 0; majority side locks from minute 13
E: 33/33/34, 360 min, with retarget 1,198 conflicts 0 0
F2: 34% attacker poisons one eclipsed 20% pool, 1 / 2 / 4 h 10 / 65 / 174 conflicts, first at 49 min 0 0 (floor 0.80 gave 10 / 65 / 174, because the eclipsed side's 54% clears 53.3%)
C: 34% of weight silent, keeps mining 0 min to first lock, 0 stalls never (8,666 stalls in 3 days, and for the whole window) 0 min
C: 40% silent 13 min, 26 stalls never 13 min, 138 intermittent stalls in 3 days
C: 45% silent 20 min never never, for as long as they stay silent
D: 35% churn (stops mining and signing) 2 min 41 h 2 min, 98 intermittent stalls in 3 days
D: 50% churn 31 min 10.1 days 4.1 days (floor 0.80: 2.2 days)

The mechanism active alone fails by: a side that cannot see the other side's votes sees stalls, stalls shrink its denominator by 1/240 per index, and once the denominator has fallen to 1.5x the side's own weight the side certifies alone, after 240 x (1 - 1.5 s) / s slots for a side of share s (confirmed within 5% across the sweep): 60 min for a 50% side, 120 for 40%, 180 for 33%. The floor makes the second test of Q3 bind before that point: no honest side of any tested split holds 56.7% of total.

What the floor costs: liveness ends between 40% and 45% of weight silent (total alone: 34%; active alone: above 55%), 50% churn stalls 4.1 days, and at 40% silent or 35% churn the margin is one pool outage thin. The safety bound stays at 1/3 of weight for every event tested; the liveness bound moves from 1/3 (total) to about 42% silent.

The simulation ran with the cert reading of participation. The block reading of Q2 credits a superset of the same votes (every vote in a certificate is in a block, and votes carried outside certificates are added), so a key's participation under the block reading is never lower than under the cert reading. For a key that is silent (C) or gone (D) nothing changes, because it emits no votes. For a key whose votes arrive late (F1 below) the block reading keeps it in the denominator for longer, which raises the weight a lock needs. That direction is safe; its cost in lock latency and in the C and D stall figures is the re-run named in O-3.3, with the per-block vote bound as the parameter.

3.3.2 The eclipse case (ledger F2): closed by the floor

Decision of 3 October 2026. The presence window of Q1 is an eclipse vector on its own: a faction that keeps the big pools' votes from the rest of the network for two hours, without stopping their blocks, becomes the whole of active weight and locks alone. The answer is not a longer window or a silent-key rule; it is the second test of Q3, already in the rule. A lock needs at least 56.7% of total weight whatever the presence window says, so an eclipsed or isolated faction can never lock unless it holds a majority of all 30-day weight, and a faction that holds that much is a public 51% event of 3.1, not an eclipse.

The numbers (sim/results_v2.md, section F, seed 7, 1,000 keys, one pool holding 20% of total weight always online in its own region):

Scenario Denominator Conflicting locks at 1 / 2 / 4 h First conflict Pool participation, minimum Pool back to 1 after the eclipse ends
F1: pool receives every block and vote D hours late and votes correctly, late any 0 / 0 / 0 never 0.500 at 1 h, 0.000 at 2 and 4 h 120 to 125 min
F2: a 34% attacker feeds the pool a private fork, signs both forks; eclipsed side holds 54% of weight, honest side 46% active alone (cert reading) 10 / 65 / 174 49 min 0.787 / 0.562 / 0.108 115 / 85 / 20 min
F2, same active + floor 0.80 (lock needs 53.3% of total) 10 / 65 / 174 49 min same as active alone same
F2, same active + floor 0.85 (lock needs 56.7% of total, rule Q3) 0 / 0 / 0 never 0.738 / 0.471 / 0.000 125 min
F2, same total alone 0 / 0 / 0 never 0.738 / 0.471 / 0.000 125 min

Why 0.85 and not 0.80: the eclipsed side in F2 holds 54% of total weight, which clears a 53.3% floor and fails a 56.7% one. Under the active denominator alone the eclipsed view certifies the attacker's fork 49 minutes in, once its denominator has decayed below 54% / (2/3) = 81%; the 0.85 floor binds before that point at every eclipse length tested, and the honest side saw 0 stalls during and after the heal in every row. A pure delay (F1) never produced a conflicting lock under any denominator because 80% of weight kept signing; its only cost falls on the delayed pool, which leaves the denominator and returns to participation 1 about two hours after its view catches up.

What this does not prove. The model grants the attacker the eclipse for free and lets it mine its fork at its full rate for the pool alone while still signing the honest chain (sim/results_v2.md, "cannot tell us"). The attacker in F2 holds 34%, above the 1/3 safety bound of 3.7, which is the point: with the floor, even a faction above the bound cannot turn an eclipse into a conflicting lock. The devnet single-node eclipse of O-3.7 confirms the rule against a real network; it does not reopen the choice of rule.

3.4 Sortition

  • S1. At launch every voter signs every checkpoint. A private VRF, keyed to the vote key and the checkpoint, selects 8 aggregators per checkpoint who publish certificates; anyone MAY aggregate and publish. Aggregator failure costs latency, not safety: the simulation's lock latency (median 2.5 s) assumes one aggregator per region and no failures, so it is a lower bound (sim/results_v2.md, "cannot tell us").
  • S2. If the number of voters above dust exceeds 8,192 at a checkpoint, the genesis rules switch, at that checkpoint and without a release, to Algorand-style binomial sub-user sortition with an expected 4,000 sub-users per checkpoint and both Q3 thresholds applied to expected sampled weight. The exact VRF construction, the binomial sampling procedure and the threshold on sampled weight are Open (O-3.5).

3.4.1 Participation grinding (ledger F3): closed by the block reading

Decision of 3 October 2026. Under a cert-only reading, whoever aggregates and whoever produces blocks could shape a rival's participation: drop its votes from the certificates you publish and carry, and after two hours its factor has fallen and your share of active weight has risen. Q2 closes this by computing participation from every vote seen in blocks over the presence window, never only from the votes an aggregator chose to certify. Aggregators choose what goes into a certificate; they choose nothing about participation.

Why an aggregator or a block producer cannot push a key's participation below the honest level:

  1. A vote is credited if it appears in any block in the past of C_i, blue or red, as a vote or inside a certificate. Certificates are one carrier among many, so the aggregator's bitmap selects nothing. An aggregator that drops a vote from its certificate changes which certificate locks (Q3 still has to be met by the votes it kept), not whether the vote counts for presence.
  2. A single block producer controls one block's payload. To keep k's vote for index j out of the past of C_i, every producer of every block that ends up in the past of C_i, over the about 240 indices between j and i, has to omit it, and the rule of Q2 makes every honest producer carry it. Under GHOSTDAG the past of C_i includes red blocks and every block merged within the 3,600-s bound, so an honest block that carries the vote is in the past of C_i within an hour of being mined.
  3. k is a voter only if it holds weight, which means it mined at least 100 blue blocks in the window. k carries its own votes in its own blocks, so suppressing k's participation means keeping k's own blocks out of the DAG for the whole presence window. That is excluding a miner from the chain for two hours, which is the eclipse of 3.3.2, where the floor already stops a lock, or a majority attack of 3.1, which the rule never claimed to survive.
  4. Nobody can raise participation falsely, because a vote is a BLS signature by k. Nobody can be made to vote twice at one index without producing equivocation evidence (3.6).

The honest level is therefore the number of indices at which k actually voted and its vote reached any honest producer within the window. A producer that receives votes and omits them lowers nothing while one honest block carries them; it only spends its own block space on less. The per-block vote bound, the window the carriage rule covers, and the lock latency and C and D stall figures under the block reading are the parameter re-run of O-3.3.

3.5 Fork choice

  • F1. Candidate tips are tips whose selected chain passes through the highest certified checkpoint the node holds and every lower certified checkpoint.
  • F2. Among candidates, GHOSTDAG selects by accumulated blue work under Kaspa's merge-depth bound of 3,600 DAA s (section 2.1). A certified checkpoint removes other tips from candidacy; it does not change how blue work is computed.
  • F3. The pruning point never advances past the latest certified checkpoint. virtual_finality_point returns the latest certified checkpoint when it is newer than the depth-based finality point (fork map e2).
  • F4. There is no hidden-block penalty in consensus. It was removed in review round 2 because it breaks DAG determinism and amplifies eclipse attacks. First-seen MAY break ties in a node's own block template only.
  • F5. A node started with a configured trusted certificate follows it. A node started cold selects the DAG with the most accumulated blue work, then follows certificates found in it. A private DAG that out-works the public one over the window is a public 51% event lasting weeks.

What a node does when it holds two valid certificates at one index after a partition heals is not modelled and not defined (sim/results_v2.md, "cannot tell us"; O-3.6). C4 says it publishes the pair. The proposal for gate 3: both certificates are evidence against every key that signed both; the node re-evaluates both against Q3 with those keys' weight struck, and if exactly one still locks it follows that one; if neither or both still lock, F2 decides among the two checkpoint blocks' descendants and the index is treated as uncertified.

3.6 Equivocation evidence

Two votes by one key for different checkpoint blocks at one index are equivocation. The evidence (the two votes) is a transaction that any block MAY include. On inclusion: the key's weight is zero for the rest of the current window and its blocks earn no weight for the next 2,592,000 DAA s. There is no coin penalty. In the simulation, evidence is detected only at the heal and the penalty is forward-looking only; the model does not revoke the conflicting certificates (sim/results_v2.md, "cannot tell us"), which is why 3.5's post-heal proposal strikes the equivocators' weight retroactively for the re-evaluation.

3.7 Residual risks, stated

  1. Safety holds with under one third of window weight under hostile keys. Reaching a third takes ten days of 100% hashrate or twenty days of 51%, in public. Beyond a third, two locks can coexist under a partition (E: a 34% equivocator across a 50/50 split breaks every variant, 33% + 34% = 67% per side) and equivocation costs history, not coins.
  2. Liveness pauses after a sudden loss of more than about 42% of weight from signing (3.3.1). During a pause the chain is proof-of-work only in practice, so the node ships a flag that tells exchanges to credit nothing until the next lock (3.9).
  3. Pools hold their hashers' votes. Vote concentration equals pool concentration, as on Bitcoin, and is public. Stratum v2 job declaration changes transaction choice, not the vote key (ledger F10, G6).
  4. A 51% owner who drives half the honest miners away and holds for 30 days owns finality thereafter. Same as Bitcoin, with a month's warning.
  5. The delay function (section 4) is a new dependency. The evaluator ships in every node.
  6. The dust threshold excludes solo miners under 100 blocks a month from voting, not from rewards.
  7. New honest miners are under-weighted for the whole window: after an overnight doubling the new cohort holds t/60 of weight on day t and the old cohort can lock without a single new signature for 19 days (sim/results_v2.md G). A doubling and a 1x renter are the same event to the rule, by design.
  8. The simulation has no DAG: a partition side's checkpoint block is "the block at blue score 30 i in that view" and conflict counts are index collisions, not reorg depths. Real GHOSTDAG merge under the 3,600-s bound, DAA lag (of the order of an hour, approximate), VRF aggregator noise, uptime (the 0.978 resting participation is a guess), regional silent sets and the cost of keys are all outside the model.

3.8 The first month

W2 gives the chain no weight at genesis. From zero history the honest network holds 3.3% of a full window on day 1, 33.4% on day 10 and 100% on day 30; 905 of 1,000 keys are under dust on day 1 and all are above it by day 20 (sim/results_v2.md A). Ledger F1 shows the consequence: with a one-day honest head start an attacker producing 75% of blocks from day 2 crosses two thirds on day 9 of the chain's life, and the 30-day emission ramp lowers the prize without removing the attack.

Two rules are on the table:

Rule Status
C5 as written: no certificate in the first 3,600 DAA s Designed (design document)
No certificate may form until the window holds 30 days of history: before DAA second 2,592,000 the chain runs plain GHOSTDAG under the merge-depth and finality-depth bounds, exchanges are told so, and the node flag of 3.9 is false Proposed (ledger F1), Open (O-3.1). This specification recommends it: the first month has no weight to defend with, so a lock in that month is a lock by whoever showed up, and the finality depth of section 2.1 already bounds a reorg to 12 hours (ledger F15: the merge-depth bound limits which old blocks can be merged, not which chain the node follows)

Gate 3 decides, with the launch-month simulation that does not yet exist. Either way the litepaper states the rule.

3.9 Exchange confirmation guidance

Designed. The node exposes finality_active (true when a certificate has formed within the last 240 indices and the first-month rule of 3.8 has passed) and last_certified (index, block hash, DAA score).

Situation Guidance
finality_active and the deposit's block is in the past of last_certified Credit. Expected wait about 90 to 120 s after inclusion
finality_active, deposit not yet covered Wait for the next certificate; do not count blocks
finality_active false (first month, or a pause under 3.7 item 2) Treat the chain as proof of work. The reorg bound to rely on is the finality depth of section 2.1, 43,200 DAA s: the node follows any heavier selected chain forked above it (ledger F15, round 3). Merge depth, 3,600 DAA s, limits which old blocks can be merged and is not a reorg bound. Credit nothing until the deposit's block is at least 12 hours of past-median time below the selected tip; count median time, not DAA score, because DAA seconds run fast during a retarget lag (ledger M14). For amounts that matter apply the operator's own hashrate judgement, as for any young proof-of-work chain. Listings are not sought before launch in any case (ledger X8)
Two certificates at one index observed (C4 evidence) Suspend credits until the node's view resolves under 3.5

A certified checkpoint overrides the heaviest chain, so fresh hashrate cannot reorganise past last_certified; two thirds of 30-day weight can, and 3.1 says what that costs.

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.

Clause Implementation Departure or gap
W1 vote_key_hash = BLAKE2b (domain IgneumVoteKeyHash) of the 48-byte compressed G1 key. The key is revealed with a proof of possession in the miner's coinbase extra data (IGNK plus 288 hex characters) and a node registers it only when the hash matches the header. A vote carries the public key too, so a key that votes is revealed by its vote The reveal is hex, not binary, because the template RPC carries extra data as a UTF-8 string. Mainnet should carry the reveal in a dedicated field or transaction
W2 Blue blocks per key in (daa(C) - window, daa(C)], counted along C's selected chain through every chain block's mergeset blues, C included O(window) per checkpoint: fine at the devnet window of 7,200 DAA seconds, not at 2,592,000. Mainnet needs an incremental window kept per chain block
W3, W5 Dust excludes a key from the voter list and from both denominators Key succession (W5) is not implemented
C1 Checkpoint i is the lowest selected-chain block with blue score at least 30 i (blue scores along the chain can skip values), determined when the sink's blue score reaches 30 i + d, d = 20 on devnet. A determination is never revisited d = 20 is below the placeholder 60; the devnet reorg-depth distribution that sets d has not been recorded
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, no automatic resolution (3.5 post-heal proposal not implemented)
C5 min_daa parameter: 3,600 on mainnet, 0 on devnet The first-month rule of 3.8 is not implemented
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
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
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

Node state is one persisted blob (DatabaseStorePrefixes::IgneumFinality), written at most once a second; votes received over RPC but not yet carried by a block are lost on restart, votes in blocks are not.