diff --git a/docs/design/finality-in-proof.md b/docs/design/finality-in-proof.md index 440a8b888..c048229f2 100644 --- a/docs/design/finality-in-proof.md +++ b/docs/design/finality-in-proof.md @@ -34,7 +34,7 @@ Spec 03 W2 as implemented on the node (`compute_weights`, `vendor/igneum-node/co 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. +1. **The key table** (the Merkle form, built 7 October 2026 after the first measurement, section 6.1, showed the inline table at 31 K cycles a key a block). A binary Merkle tree of 2^24 leaf slots (`keys.rs`); a key takes the next free slot the first time it is seen and keeps it until its entry empties, so the non-empty leaves are the dense prefix `0..key_count` and a slot is never reused. An entry is `key_hash` 32, `pubkey` 48 (zero until revealed), `blocks` 8 (blue blocks in the window), `ban_until` 8, `leave_from` 8, `leave_until` 8; a leaf is `sha256(0x05 ‖ entry)`, an empty leaf the zero hash. The fold opens one path (24 siblings) per key it touches and writes it back; the state carries `keys_root` and `key_count`, never the table. The certificate step takes every entry with its slot once, rebuilds the root over the dense prefix (about 2N hashes at N keys), sorts by key hash for the canonical voter list of spec 3.10 C3 and refuses a key that appears twice. What a lying witness can do with the slots: insert a key a second time at a fresh slot (the guest cannot know the key has one already); the split is refused at the next certificate as a duplicate, so no certificate verifies while it stands, and the native compare vetoes the record on the chain. 2. **The block ring.** One leaf per 16 DAA seconds: `sha256` over the sorted list of `(daa, block_hash, key_hash)` of the blue blocks whose DAA score falls in the leaf's span; empty leaves are the zero hash. `ring_root` is the root of a binary Merkle tree of 2^18 leaves indexed by `(daa / 16) mod 2^18` (4,194,304 DAA seconds of span over a 2,592,000 window, so a slot is reused only after it has aged out). The node's tracker holds the ring sparsely (at most about 2 x 2^18 nodes); the guest opens a leaf by witness. Sixteen seconds a leaf is the trade between path length (18 hashes) and leaf size (16 blocks at 1 block/s); a leaf per DAA second would cost 22 hashes a path and a 2^23-node tree on the node. 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. **The fold is per chain block, not per segment.** The aggregator runs once per chain block (`--mode chain` and the app's segment step aggregate block by block, the previous block's proof as the deferred proof; bench-log 5 October, "aggregation is a fixed cost per block"), so the state is folded one chain block at a time and the key table is hashed in and out at every fold. That is what keeps the certificate step exact at the checkpoint's own block (every history leaf commits that block's `keys_hash`), at the price of two table hashes per block, which section 6 measures per key. @@ -103,11 +103,11 @@ The sharp one. The guest adds the blue blocks a witness names; it does not run G | 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 | +| 0 (the first prototype, the morning of 7 October) | 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 (BUILT, `header.rs` and `check_headers` in `fold.rs`; the witness carries the chain block's header and every mergeset block's, blue and red) | Every header hashes to its hash (`kaspa_hashes::BlockHash`, BLAKE2b-256 keyed `BlockHash`, the field order of `consensus/core/src/hashing/header.rs`; the differential test checks the guest's hash against the chain's); the chain block's header names this block and its DAA score; the previous chain block is a direct parent and outranks every other direct parent by (blue work, hash), GHOSTDAG's selected-parent rule; every blue's key and DAA score are its header's; the blue count equals `blue_score(C) - blue_score(selected parent)`; every mergeset block is reached from the chain block by parent links through mergeset blocks (every direct parent other than the selected parent is in the mergeset, parents being an antichain, so the walk never leaves the witness); reds ride into the ring uncounted and flagged, so a block seen in any mergeset, blue or red, is refused in every later one | Swap the colours of two mergeset blocks of one chain block (the blue count holds, both are real blocks reached by parent links, neither was seen before). Nothing invented, nothing omitted without a substitute of the same block's past | the harness and the node's differential test; cycle count in section 6.1 (level 1 witnesses were not in the synthetic run; the headers are 16 BLAKE2b hashes a block, software, approximate 1 to 2 M cycles, to measure on a real export) | | 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. +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 colour-swap bound stated, and fully at level 2. Level 1 is what the pinned guest 0x4ab78fb1 carries when the witness has headers; a witness without headers folds at level 0, and the node's tracker always supplies headers, so a record carried on the chain is level 1 and a level 0 proof chain is a light-client matter only (the extension does not say which level made it: adding a level field is the next cut's one-line change, noted here so the card can read it). ### 5.3 A stale table @@ -161,6 +161,9 @@ The fold is the key table hashed in and out with software SHA-256 (31 K cycles a At one certificate per 30 blocks: 12.3 s a block against 3.1 s, 4x today at 1,000 keys; about 1.4x after the two fixes (approximate, from the cycle rows). +### 6.2a After the two fixes and the Merkle table +To be filled from the fp-2 run (RTX 5090, 7 October 2026, 10:xx UK): the same rows as 6.1 and 6.2 on the pinned guest 0x4ab78fb1. + ### 6.3 Verifier time (measured) `verify-segment` 0.090 s on the finality proof (0.076 to 0.091 s on the plain one, the same light verifier); `final-at` answers from the 504 bytes of public values in under a microsecond after the verification; a browser's WASM verify of the wrapped proof stays frontier 3.4's measurement. @@ -169,11 +172,12 @@ At one certificate per 30 blocks: 12.3 s a block against 3.1 s, 4x today at 1,00 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. +3a. Level 1 of 5.2 (built the same day after the first measurement): `header.rs`, `check_headers`, reds in the ring, the tracker's `header_witness`, the harness test `level_one_pins_the_blues_to_headers_and_parent_links` and the node's differential test `the_proof_fold_agrees_with_weights_at_on_a_real_dag` (a 120-block DAG with side blocks and reds; the fold's table equals `weights_at` and `voters_at_block` at every chain block; a dropped blue and a recoloured red are refused). 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). +What is not in this round: level 2 of 5.2 (GHOSTDAG in the guest), the wrapped verifier in WASM (frontier 3.4), key succession (W5, not on the node either), the certificate's aggregator sortition proof (verified by the node, meaningless to a light client, not in the guest), and a level field in the extension. ## 8. What moves the digest