diff --git a/docs/design/finality-in-proof.md b/docs/design/finality-in-proof.md new file mode 100644 index 000000000..cb101bd81 --- /dev/null +++ b/docs/design/finality-in-proof.md @@ -0,0 +1,158 @@ +# Finality carried inside the segment proof + +Design, 7 October 2026, 08:4x UK. Lane fin-proof (branch `fin-proof`, fork branch `fin-proof-node` from `release-0.3.18-node` e69e8a39). The item is `docs/analysis/horizon/frontier.md` rank 1 (section 3.3, with the Kaspa developer's attack in 4.2). Status: Designed here, Implemented as a guest prototype behind `finality_in_proof_activation_daa` (default never), Measured where section 6 says so. Nothing here touches the devnet. + +The sentence. Today the segment proof says "the state root after chain block n is R" (spec 7.8). With this design it also says "checkpoint i at chain block m is locked under finality rule v2 by x of y weight, counted from this same chain", and a browser tab, the wallet or the app's light-client card verifies that from one proof and asks no node for the voter set. The weight table of spec 03 W2 rides inside the recursion as a 32-byte commitment, updated once per segment by the segment's own blue blocks, and the BLS certificate is verified inside the guest against the table the proof carries. + +## 1. What the statement gains + +The aggregator guest's public values (`BlockOutput`, 340 bytes, mirrored in consensus as `BlockStatement`) gain a fixed extension when the finality input is present. The layout before the activation is unchanged, byte for byte, so one pinned guest serves both sides of the switch. + +| Field | Bytes | Meaning | +|---|---|---| +| `fin_version` | 2 | 1. Zero means "no finality claim" (the old layout never carries the extension) | +| `table_root` | 32 | Commitment to the weight state after the segment's last chain block (section 2) | +| `history_root` | 32 | Merkle mountain range over every chain block the proof chain attests: leaf `n` = `sha256(number ‖ block_hash ‖ table_root_n ‖ total_n ‖ daa_n)` | +| `lock_index` | 8 | Highest checkpoint index the proof chain has verified a certificate for; 0 before the first | +| `lock_hash` | 32 | That checkpoint's block hash | +| `lock_number` | 8 | Its chain-block number | +| `lock_signed` | 8 | Signed weight of that certificate, in blocks, at the checkpoint's own table | +| `lock_total` | 8 | Total weight of that table | +| `lock_frozen_signed` | 8 | Signed weight at the table of the previous lock (rule v3, Q5) | +| `lock_frozen_total` | 8 | That frozen table's total less the keys that left (W7) | +| `lock_daa` | 8 | DAA score of the certified checkpoint, so a verifier can judge staleness | +| `flags` | 2 | bit 0 `stale`: the previous lock is more than one weight window older than the segment's last block and the guest refused every certificate since (section 4.4) | + +154 bytes. `BlockOutput::LEN2 = 494`. Keccak of the public values stays the record's statement, so `SegmentRecord` is unchanged (its `public_values` field already carries the bytes inline, spec 7.8 item 3). + +What the extension says, in words: every chain block from the proof chain's first block to `number` is in `history_root`; the table at every one of those blocks is committed in its leaf; checkpoint `lock_index` at chain block `lock_number` carries a certificate whose signers hold `lock_signed` of `lock_total` at that block's own table, and `lock_frozen_signed` of `lock_frozen_total` at the previous lock's table; both pass two thirds. A verifier that trusts the aggregator program id and the chain of proofs from a trusted root learns all of that from 494 bytes and one proof verification. + +## 2. The carried weight state + +Spec 03 W2 as implemented on the node (`compute_weights`, `vendor/igneum-node/consensus/src/processes/finality.rs`): the weight of key k at checkpoint C is the number of blue blocks in C's past, counted along the selected chain through every chain block's mergeset blues, whose DAA score lies in `(daa(C) - W, daa(C)]`, W = 2,592,000 on mainnet, 7,200 on the devnet; a key under `dust` blocks is no voter; the canonical voter list is the keys above dust and not stripped or left, sorted by key hash; total is their sum. + +The guest keeps the same quantity incrementally. The state has two parts, both committed under `table_root = sha256("igneum-fin-state-v1" ‖ end_daa ‖ keys_hash ‖ ring_root)`: + +1. **The key table.** One entry per key ever seen in the window, sorted by key hash: `key_hash` 32, `pubkey` 48 (zero until revealed), `blocks` 8 (blue blocks in the window), `ban_until` 8, `leave_from` 8, `leave_until` 8. `keys_hash` is sha256 over the serialised table. The prototype hashes the whole table every segment (section 6 gives the cost per key); the scale form, when the measurement asks for it, is a sorted Merkle tree over the same entries with one path per touched key, and the certificate step then takes the full table as witness once per certificate. +2. **The block ring.** Leaf `d` for every DAA score in the window: `sha256` over the sorted list of `(block_hash, key_hash)` pairs of the blue blocks whose DAA score is `d`; empty leaves are the zero hash. `ring_root` is the root of a binary Merkle tree of 2^22 leaves indexed by `d mod 2^22` (4,194,304 slots over a 2,592,000 window, so a slot is reused only after it has aged out). The ring is what lets the guest age blocks out exactly, block by block, as the node does: when `end_daa` moves from `e` to `e'`, every block with DAA score in `(e - W, e' - W]` leaves the window and its key's `blocks` is decremented; the witness supplies those leaves' contents and paths. It is also the double-counting guard (section 5.2): a block hash already in its leaf is refused. + +**One segment's update.** Input: the previous state (hash-checked against the previous proof's `table_root`), and for each chain block of the segment the list of its blue blocks (itself and its mergeset blues) as `(block_hash, key_hash, daa_score)`, which is exactly `ChainBlockRecord.mergeset` with `is_blue` (`igneum/exec/src/records.rs`). For each block: insert into its ring leaf (path witnessed, duplicate refused), `blocks += 1` for its key (a new key is appended to the table in sorted position). Then age out as above, set `end_daa = daa(last chain block)`, and recompute `keys_hash` and `ring_root`. The witness also carries, when present: key reveals (`pubkey`, proof of possession, verified in-guest under `DST_POP`; the key hash must equal `BLAKE2b("IgneumVoteKeyHash", pubkey)`), equivocation evidence (two votes by one key at one index for different blocks, both signatures verified; the ban dates from the carrier's DAA score the witness names, which the native tracker takes from the carrier block as the node does), and leaves (W7, signature verified under `DST_LEAVE`; dated the same way). + +**The voter list at a block.** Keys with `blocks >= dust`, `ban_until <= daa`, and not `leave_from <= daa < leave_until`, in table order (the table is sorted by key hash, so the filtered table is the canonical list of spec 3.10 C3 with no sort in the guest). `total` is their sum. The guest commits `total` into every history leaf so a verifier can read the denominator at any block without the table. + +Cost model per segment of N = 8 chain blocks at 1 block/s: about 16 blue blocks (8 chain blocks and their mergesets), 16 ring insertions and about 8 ring expiries at 22 hashes each, about 530 SHA-256 compressions; plus two hashes of the key table (in and out). Section 6 measures it. + +## 3. The certificate inside the recursion + +A certificate for index i names a checkpoint block `C_i` and a signer bitmap over the canonical voter list at `C_i` (spec 3.10 C3). Certificates ride in the coinbase extra data of later blocks (the `IGNF` section) and reach the prover through the exec RPC, as the node's finality state already exposes them. The guest takes any number of certificates per segment (in practice one per 30 s, so one every four segments at 1 block/s) and for each: + +1. **The checkpoint is on this chain.** The witness gives the history leaf preimage of chain block `m` (`number`, `block_hash`, `table_root_m`, `total_m`, `daa_m`) and its MMR path against the `history_root` the guest holds at that moment; `block_hash` must equal the certificate's checkpoint. The guest cannot check blue score (the statement has no blue score), so it checks what it can: index strictly above `lock_index`, `m` strictly above `lock_number`, and `daa_m > lock_daa`. Index-to-block binding beyond that is the certificate's own job: two thirds of weight signed `(chain_id, i, hash(C_i))`. +2. **The table at the checkpoint.** The witness gives the full key table as it stood at `m`; the guest hashes it and compares with `table_root_m` from the leaf (so the table is the one the proof chain itself committed when it passed `m`, never the prover's choice). The guest derives the voter list and `total` at `daa_m`, maps the bitmap positions to keys, sums their `blocks` into `signed`, and takes their public keys. +3. **The signature.** Every signer must be revealed (a zero pubkey refuses the certificate, as the node does). The aggregate public key is the G1 sum of the signers; the message is `"igneum-vote-v1/" ‖ chain_id ‖ 0 ‖ index_le ‖ checkpoint` hashed to G2 under `IGNEUM_VOTE_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_`; the check is `e(agg_pk, H(m)) == e(g1, sig)`, the min-pubkey setting blst's `fast_aggregate_verify` implements. In-guest the curve is zkcrypto's `bls12_381` under SP1's patch (the Fp and Fp2 operations run on the BLS12-381 precompiles; the Miller loop and the final exponentiation run as Rust over them). The proof of possession on every reveal (item 2 of section 2) is what makes the aggregate sound against rogue keys; the guest never trusts a node's reveal check. +4. **The tests.** Q3 floor: `3 x signed >= 2 x total_m` (inclusive, `FinalityParams::floor_met`). The active test of Q3 is implied by the floor since 4 October 2026 (O-3.15) and is not computed in the guest: participation (Q2) needs every vote carried in `C_i`'s past, 48 per block, which the proof does not carry; the node's active number is a liveness report, not a lock condition, and the guest emits none. Q5 frozen: when a previous lock exists, the witness gives the key table at `lock_number` (hash-checked against that leaf's `table_root`), the guest sums the signers' weights there, subtracts from that table's total the keys whose leave has taken effect at `daa_m`, and requires two thirds of what is left (`frozen_floor` on the node). Before the first lock there is no frozen table and Q3 alone decides, as on the node. +5. **The first-month gate.** `daa_m >= min_daa` (C5), a parameter of the guest input bound into the statement by the native compare. +6. On success the lock fields of section 1 are set to this certificate; the certificate's own bytes are not in the statement (the statement commits what was verified, not the input). + +A certificate that fails any step makes the guest panic, which makes the proof impossible, so a prover that holds a bad certificate simply leaves it out. The statement then says "no new lock in this segment", which is true. + +## 4. The verifier with no node + +### 4.1 What a light client holds + +A trusted root: the aggregator program id (the pinned manifest's id, shipped in the client as the verifier key is today) and one proof's public values the client accepted out of band at install (the release ships the latest wrapped proof the way spec 10.3 ships a trusted checkpoint). From then on the client holds the latest proof it verified, nothing else. No voter list, no weights, no headers. + +### 4.2 Per update + +1. Fetch the newest segment proof from any node (`igneum_getSegmentProofBytes`); verify it against the aggregator key (today SP1's light verifier, 0.032 s on a Mac core for the compressed form, bench-log 5 October; in a tab the Groth16 or Plonk wrapper of frontier 3.4, unmeasured). +2. Check it extends what the client holds: `chain_id` equal; `shard_vk` and `agg_vk` the pinned ids; `chain_len` at least `number - held.number + held.chain_len` (the recursion verified every proof between); `history_root` consistent with the held one (the held `(number, history_root)` is an MMR prefix of the new one: the witness is the held peaks, bagged, and the client checks the new root re-bags them with the new leaves, log n hashes); `lock_index >= held.lock_index` and, when equal, the same `lock_hash` (a proof chain that drops or changes a lock is refused). +3. Read `lock_index`, `lock_hash`, `lock_number`, `lock_signed / lock_total`, `lock_frozen_signed / lock_frozen_total`, and `stale`. The card says "locked at checkpoint i by x percent of 30-day weight, voter set: verified in the proof" and, when `stale`, "finality paused for more than a window; this client needs a fresh starting point". + +### 4.3 "Is block B final?" + +Final means B is on the chain through the latest lock, at or below it. For the proof's own last block: final iff `number <= lock_number`. For any other chain block: its history leaf and MMR path against `history_root`, then `leaf.number <= lock_number`. For a transaction: its receipt proof against the block's `receipts` commitment (spec 7.6 item 3) plus the above. The harness of section 7 answers exactly this from proof bytes alone. + +### 4.4 What the client trusts, and the one departure from spec 03 + +| What the client trusts | Bounded by | +|---|---| +| The proof system and the pinned aggregator id | Spec 10.1 as today: a soundness bug is a light-client problem; full nodes keep verifying the real BLS certificate natively and veto any record whose extension differs from their own (section 5.1) | +| The root proof it started from | The same out-of-band assumption as spec 10.3's trusted checkpoint | +| The blue colouring of counted blocks | Section 5.2: the prototype takes it as a witness; the carried record is vetoed by full nodes; a client that takes proofs from an untrusted node is exposed to the swap of a blue block for a red one in the same chain block's past, and to nothing cheaper (level 1), or to a free lie (level 0). The trust row says which level the shipped guest is at | +| Nothing else from any node | Headers, votes, certificates and the voter set are never fetched | + +The departure: in the guest the frozen table of Q5 never expires. On the node `frozen_table` returns `None` once the last lock is a full window old, so a chain that paused for 30 days can lock again on the sliding table alone (spec 3.7 item 2: the price of the pause over the fork). A proof-only client cannot afford that rule. It verifies no proof of work, so a chain built from the client's root with no real blocks behind it could age every honest key out of a forged sliding table in 30 forged DAA-days and then certify itself with keys that never mined. With the frozen table standing for ever inside the proof, every certificate after the root needs two thirds of the table at the last real lock, which honest keys hold and a forger does not, whatever it forges. The cost is weak subjectivity: after a pause of a window the guest refuses certificates, sets `stale`, and the client needs a new root (as an Ethereum light client needs a fresh checkpoint after a long sleep). That is a stop, never a wrong "final". The node is untouched by this: its rule is its rule, and a stale proof chain is a light-client matter until the operator ships a new root. + +## 5. Hostile review + +### 5.1 A prover lying about the table (the Monero developer's attack, frontier 3.3) + +The table is a second implementation of W2. Two defences, both required. First, the native compare: from the activation, `check_segment_record` in the exec layer computes the extension natively (`FinTracker`, the same `igneum_prove_core::fin` code fed by the node's own `ChainBlockRecord.mergeset`, its finality state's certificates, reveals, evidence and leaves) and refuses a record whose `table_root`, `history_root` or lock fields differ, the veto of spec 7.2 item 5 extended to the new fields. A lying prover's record pays nothing and is carried by no honest block. Second, the differential test: the tracker and the node's `weights_at` agree on every key's blocks and on the voter list at every checkpoint of a fast-time run (the node test in section 7); a disagreement is a bug in one of the two and fails the build. What a lie in the carried table can reach is therefore a light client that takes a proof from the liar directly, which is section 5.2. + +### 5.2 The blue set from the prover (the Kaspa developer's attack, frontier 4.2) + +The sharp one. The guest adds the blue blocks a witness names; it does not run GHOSTDAG. Three levels, in order of what a lie can still do: + +| Level | What the guest checks per counted block | What a lie can still do | Cost per segment | +|---|---|---|---| +| 0 (the prototype of this round) | Nothing beyond the arithmetic: the pair `(block_hash, key_hash, daa)` is the prover's word | Count blocks that do not exist, omit honest blocks: free to a prover whose proof a client takes directly; vetoed on the chain by every full node | none | +| 1 (designed; the next step) | The block's header bytes hash to its hash (`kaspa_hashes::BlockHash`, BLAKE2b-256, the field order of `consensus/core/src/hashing/header.rs`), so `vote_key_hash`, `daa_score` and `blue_score` are the header's; the block is reached from the chain block by parent links through other counted blocks (every block in a chain block's mergeset is on such a path, since an intermediate in the past of the selected parent would put the target there too); the ring leaf refuses a hash already counted; and the count of blues per chain block equals `blue_score(C) - blue_score(selected parent)`, both headers being chain blocks the proof chain attests | Swap one blue for one red of the same chain block's past: a real block with real proof of work, counted for its own miner; reds are the k-cluster violators, few at 1 block/s. Nothing is invented and nothing is omitted without a substitute | about 16 header hashes (BLAKE2b in the guest, no precompile, approximate 60 to 100 k cycles each) and the paths, under 2 M cycles | +| 2 (open, the frontier's measurement) | GHOSTDAG's blue-set rule for each merged block (anticone at most k) inside the guest | Nothing about the colouring | unmeasured; the reason this round ships level 1 as the next step, not level 2 | + +The light client's trust row names the level the shipped guest is at. "Voter set: verified" on the card (frontier gate b) is honest at level 1 with the red-swap bound stated, and fully at level 2. + +### 5.3 A stale table + +The witness is always the state the previous proof committed, hash-checked; a prover cannot present an old table as the current one because `end_daa` is inside the commitment and must equal the previous proof's last chain block's DAA score, and the next segment's first chain block must be that block's child (the existing `parent_hash` chain). A certificate is tested at the table of its own checkpoint block (section 3 item 2), read from the history leaf, never at the segment's current table; the 60 to 70 blocks between a checkpoint and its certificate's arrival (determination depth d plus relay) change nothing. + +### 5.4 The LEAVE item (W7) + +A leave carried in a block removes its key from every denominator `leave_delay` after the carrier and until the leave is a window old. In the guest the leave enters as a witness with its signature verified and its carrier DAA score named by the witness; the native tracker names the same carrier the node's `leaves_at` does (the lowest-DAA carrier in the checkpoint's past), so the compare pins it. A prover that withholds a leave makes the frozen denominator larger, which is the strict direction: it can only turn a lock into a refusal, never the reverse. The same holds for evidence (a withheld ban leaves a stripped key's weight in the total and its signature in the certificate, and the node's compare refuses the record). Leaves are what keep the never-expiring frozen table of 4.4 live: a fleet that leaves cleanly shrinks the frozen denominator within the hour and the proof chain keeps locking, which is the case the 6 October pause showed the node needs too. + +### 5.5 A split table during a pause + +Two sides of a partition each build their own proof chain from the common prefix. Each side's table counts only its own blocks after the split (W2 is per view, spec 3.7 item 9). Under the frozen test neither side locks until it holds two thirds of the table at the last common lock, which a side under two thirds never does, and the guest never lets that table expire; so neither side's proof chain ever claims a lock the other side could not also verify, and a light client on either side sees `stale` after a window rather than a side-only lock. At the heal the chain follows one side's certificates (3.5, the certificate-driven reorg); the losing side's proof chain is abandoned with its segments, as any reorg deeper than a segment abandons records today (spec 7.8 item 7, the unproven rule), and the winning side's chain is re-proven from the fork point by the recursion restarting at an unproven segment. A client that held a proof from the losing side sees its `history_root` is no prefix of the new chain's and must restart from its root: the new chain's proof chain still passes through the client's root, so the restart is a re-sync, not a new trust decision. This is the fast-time scenario of section 7 (C4 sweep, `docs/fud-ledger.md`). + +### 5.6 Two certificates at one index + +The guest accepts the first certificate it verifies for an index and refuses another for the same index in any later segment (`index > lock_index`), as the node keeps the certificate it verified first (3.11 item 4). A prover cannot replace a lock. A heavier fold certificate (Q4) over the same block, arriving later, is simply not verified: the lock stands at the weight first seen, which is at least two thirds. + +## 6. Costs + +Baselines on record: a chained aggregation on the RTX 5090 costs 2.5 s with the card to itself and 9.7 s while it mines (bench-log 5 October, `chain-pc2-pv1c` and the agg-cost entry); on the Mac CPU 52 to 59 s; the compressed segment proof is 1,272,909 bytes whatever `chain_len`; `verify-segment` 0.032 s. The lane brief names the CPU prover on the box at 137 s and 28 GB for the smallest shard. What this design adds, by part, with the measurement in `docs/bench-log.md` under "finality in proof" once run: + +| Part | Where | Expected (approximate, before measurement) | Gate | +|---|---|---|---| +| Table update, no certificate, 1,000 keys | guest cycles | under 5 M (two table hashes of 112 KB on the SHA-256 precompile, 530 ring hashes) | measured in section 6.1 | +| Table update, 10,000 keys | guest cycles | about 40 M (two hashes of 1.1 MB): the Merkle form of section 2 when this binds | measured | +| One certificate, 1,000 voters | guest cycles | G1 aggregation of up to 1,000 keys on the precompile, one hash-to-G2, one pairing: tens of millions of cycles from memory of SP1's bls12-381 benchmarks, unmeasured here | frontier gate (a): under 50 M | +| Proof bytes | public values | +154 bytes; the compressed proof size is unchanged (constant) | none | +| Prover time per segment | 5090, box CPU | the cycle count divided by the measured cycles per second of the aggregator (the 5090 aggregation at about 2.5 s is the known cost of the existing guest) | section 6.2 | +| Verifier, native | Mac core | unchanged, 0.032 s: the extension is parsed, not verified separately | section 6.3 | +| Verifier, browser WASM | tab | the wrapped-proof verifier of frontier 3.4 (unmeasured); the extension parse and the MMR check are microseconds | open, frontier 3.4 | +| Witness bytes per segment | gossip | the key table (112 bytes per key) once per segment at the prototype, plus the certificate's bitmap and the ring leaves; 1.1 MB at 10,000 keys, which is the reason the Merkle form exists | section 6.1 reports the bytes | + +Per tier (the standing rule): a home miner on any card mines as before, since the table update is the aggregator's work and a shard prover never sees it; a 12 GB prover that aggregates pays the extension's cycles once per segment and the certificate's once per 30 s, so about one more shard in thirty at launch traffic (frontier 3.3's figure, to be replaced by section 6.1's); a rig or pool that aggregates pays the same from the same 20 percent pool, so the aggregator's share (`proving_v1_aggregator_share_bps`) is the parameter to revisit once the cycles are known; a holder or wallet user gets "locked, voter set verified in the proof" from 494 bytes and one verification; a rollup customer's bridge verifies one proof for state and finality, the proof bridge of spec 7.3 delivered early; a node operator relays the key table with the segment witness (the bytes above). + +### 6.1 Cycle counts (box CPU, SP1 execute mode, no GPU needed) +To be filled by the measurement: the aggregator in `execute` mode over the harness's segments at 100, 1,000 and 10,000 keys, with and without a certificate, under `BR_MEASURE=1` on igneum-build-1. + +### 6.2 Prover time (GPU) +To be filled: the fleet lane's RunPod route ("rent fin-proof", a 4090 or 5090 pod with the fleet image, about USD 0.75 an hour), `--mode chain` with the finality input against the same fixtures without it. + +### 6.3 Verifier time +To be filled: `--mode verify-segment` and the new `--mode final-at` on the Mac and on the box. + +## 7. What is built in this round, and the tests + +1. `igneum-prove-core::fin`: the state, its hashing, the ring, the MMR, the segment update, the certificate check with the BLS verify behind a trait (blst natively in tests, `bls12_381` in the guest), the voter list, the frozen test, the leave and ban rules. Unit tests: the table after a window agrees with a brute-force count; a block counted twice is refused; ageing is exact at the window edge; a certificate under two thirds is refused and one at exactly two thirds locks (inclusive); a frozen table holds a side that lost its partner; a leave shrinks the frozen denominator after the delay and not before; a second certificate at a locked index is refused; the stale flag sets after a window without a lock and no certificate passes after it. +2. The aggregator: `AggInput.fin: Option`; the output extension; the same `aggregate` function on host and guest. +3. The known-failed case first, as the standing rule asks: the harness builds a chain with an attacker key that holds 20 percent of real weight and presents a certificate over a checkpoint with a voter list of its own making in which it holds 70 percent. Today's light client (`site/verify/core.js`, which takes `voters` from the node) accepts that certificate when the node is the attacker's; the harness shows the acceptance (the JS logic ported to the test), then shows the proof-carried table refusing the same certificate. The first green test is the second half. +4. The harness `proving/igneum-prove/host --mode final-at`: reads a proof file, optionally a block's leaf and MMR path, and prints `final at checkpoint N` or `not final` or `stale`, from the proof bytes and the pinned aggregator key alone; a test runs it against a chain the stub proof system produced and against one tampered byte. +5. The node: `finality_in_proof_activation_daa` in `Params` (default `u64::MAX`, the override file sets it, in the digest once set); `BlockStatement` with the extension; `FinTracker` in the exec layer; `check_segment_record` compares the extension from the activation; the exec RPC exposes a chain block's finality witness (`igneum_getFinalityWitness(first, last)`: mergeset blues, certificates carried in the range, reveals, evidence and leaves with their carriers) so the app's aggregator step can build `FinInput`. Node unit test: the tracker's table at every checkpoint of a fast-time chain equals `weights_at` (the differential test of 5.1). +6. The wallet-side stub: `final-at` as a library function the app and the wallet call, and `site/verify/finproof.js`, the browser half, which parses the extension and answers the same question from public values a verifier handed it, with the trust row text of 4.4. + +What is not in this round: level 1 of 5.2 (designed above, the next step), the wrapped verifier in WASM (frontier 3.4), key succession (W5, not on the node either), and the certificate's aggregator sortition proof (verified by the node, meaningless to a light client, not in the guest). + +## 8. What moves the digest + +Nothing until `finality_in_proof_activation_daa` is set. Setting it is a consensus change for the statement compare only: a node without the switch accepts the old 340-byte statement and refuses the extended one as "public values are not a block statement", so every node must run this build before the height, as proving v1 did. The pinned aggregator id changes once at re-pin (every prover and verifier moves together, `proving/README.md`); the old id stays accepted for records whose `fin_version` is zero until the activation, after which `agg_vk` must be the new id. The parameter joins the digest with the proving v1 set (`params.rs`, the `field(&mut h, ...)` list), so a mismatched file splits at the handshake, as every other switch. Rollout: Devnet 2 first, the gate of `tools/fleet/devnet2-gate.sh`; the live devnet by the 95 percent signal with a floor height, never a fixed height.