igneum/docs/design/genesis-forward.md

85 lines
16 KiB
Markdown

# Genesis forward-compatibility: the scheme byte, key succession, the cache rung
7 October 2026, 10:1x UK, the project lead's order to build mission item 8 now (`docs/analysis/mission/mission.md` section 2.8; the research in `docs/analysis/mission/future.md` sections 1.3, 7 and 10 and `docs/design/finality-in-proof.md` section 7). Branch `genesis-forward` on the node fork (from release-0.3.19-node dc141409, the merged line that carries the ladder and the W7 leave item, which ca3-v4-0318 alone does not) and `genesis-forward` on the repo. Three genesis fields, every switch never on the devnet (its digest does not move), all three set at the testnet genesis by the testnet lane, which holds the cut and re-pins. The class-group VDF's quantum fallback is flagged in spec 04 section 4.8, not built.
## 1. The scheme byte
| Item | Place | Value |
|---|---|---|
| The byte | `finality::Vote::sig_scheme`, `finality::KeyReveal::sig_scheme`, `finality::Succession::successor_scheme` | `SIG_SCHEME_BLS12_381` = 0 everywhere today |
| The genesis value | `Params::sig_scheme` (override key `sig_scheme`) | 0 on every network |
| The switch | `Params::sig_scheme_activation_daa` (override key `sig_scheme_activation_daa`; `u64::MAX` = never) | never on devnet, simnet, mainnet; 0 at the testnet genesis |
| The digest | both fields enter once the switch is set (the 0.3.15 rule) | the devnet's digest unchanged; the testnet's moves at the cut |
| The wire, plain | vote item tag 1 (`Vote::LEN` = 280 bytes), evidence tag 3, reveal `IGNK` + 288 hex: scheme 0 implied | byte for byte what every live network carries |
| The wire, explicit | vote item tag 5 = `scheme \|\| vote`, evidence tag 6 = `scheme \|\| first \|\| second`, reveal `IGNS` + 2 hex of the scheme + 288 hex | written by every template once the switch is active (`encode_section_explicit_within`); a vote of any other scheme is always explicit, so a scheme the node does not run is never mistaken for one it does |
| The active scheme | `igneum::active_sig_scheme(genesis, class)` = the scheme the program class at the sink names (`SIG_SCHEME_OF_CLASS`, a genesis table with no row today), else the genesis byte; the finality manager re-reads it at every virtual change | 0 |
| The refusal | `FinalityManager::scheme_refusal`: a reveal is not registered, a vote is not recorded, carried or gossiped, a successor is refused; the RPC answers `refused: vote carries signature scheme 1; the active scheme is 0 (BLS12-381); another scheme is named only by a program class the 95 percent signal moves to, and none names one` | every node, whatever its switch |
Why a class change names the scheme: the flip is the P2 mechanism (95 percent of mining weight over a window with a floor height, the one path a consensus change takes on this chain), and the aggregated-vote format must ship first (naive ML-DSA-44 votes at 8,192 voters cost 19.4 MB a checkpoint and 57 GB a day, `future.md` 7.3; with 250x SNARK aggregation about 223 MB a day). Adding a row to the table is a code change under the class path; the byte is in every item from genesis so the row costs no fork.
## 2. Key succession, W5
| Item | Place | Value |
|---|---|---|
| The item | `finality::Succession` (tag 7, 297 bytes): `daa`, old key, successor key, successor scheme, the old key's signature, the successor's signature, both over `"igneum-succeed-v1/" \|\| chain_id \|\| 0 \|\| daa \|\| old \|\| new \|\| scheme` under `IGNEUM_SUCCEED_V1_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_` | the successor's signature is its consent and its proof of possession in one |
| The switch | `Params::finality_succession_activation_daa` (override key of the same name) | never everywhere; 0 at the testnet genesis; in the digest once set |
| Submission | `submitFinalitySuccession` (RPC op 158, gRPC 1131/1132, wRPC, `igneum-miner succeed <grpc url> <old label> <new label>`) | carried after the leaves in the templates of the node that holds it; no p2p gossip kind (the successor's own node mines it) |
| The fold | `successions_at` (deterministic like `leaves_at`: the lowest-DAA carrier in C's past dates it) and `fold_successions` inside `voters_at`: the old key's window blocks are credited to the successor as they stand (count and oldest block), the old key leaves the table, a ban or a leave on the old key lands on the successor; the successor's presence counts the old key's carried votes | from the first checkpoint whose past holds the carrier; the window inherited, not a fresh one |
| Once | one record per old key: a second succession from a key that handed over is refused (`already handed its weight to`), a successor that has itself handed over is refused (`has itself handed`), which also refuses every cycle; a chain old to new to newer is legal | a record outlives the weight it moved by one window, then is trimmed |
| After | the old key's votes are refused (`handed its weight to`), never counted, never carried | the old key is dead |
| The report | `getFinalityWeights`: `succeededFrom` on the successor's row, a weightless row for the old key with `succeededTo` | both nodes of the unit test agree on every voter list |
Not in this round: a p2p gossip kind for successions (the leave's shape, protocol version bump); the app's "rotate key" button, which signs the item from the old key's label and restarts the miner under the new label (the `succeed` subcommand is the primitive); the aggregated-vote format.
## 3. The cache rung beside N
| Item | Place | Value |
|---|---|---|
| The rung | `igneum::CacheRung { mib, admissible }` as `LatencyLadder::cache_rung` (override key `latency_ladder_cache_rung`) | `{512, false}` at genesis; a power of two above the 256 MiB genesis cache |
| The signal | `LadderSignal::Cache` = both ladder bits set (header version 0xc000; until today both set was "no signal" and no node ever stamped it, so no block written so far changes meaning); `IGNEUM_LADDER_SIGNAL=cache` | code 3 in `PowEpochInfo::latency_ladder_signal` |
| The rule | `latency_ladder_step_signalled_with_cache`: the cache step moves 0 to 1 when every one of the newest seven windows reaches 90 percent for the cache rung, the rung is admissible and the cool-down holds (the oldest window begins after the last decision, shared with N); never back; never in the same decision as N (one signal per block, exclusive shares) | `LadderState::cache_step` |
| The digest | the rung's size and flag enter with the ladder, once `latency_ladder_activation_daa` is set | the testnet's digest moves at the cut |
| The RPC | `powEpoch.latencyLadderCacheStep`, `nextLatencyLadderCacheStep`, `latencyLadderCacheMib`, `nextLatencyLadderCacheMib`, `latencyLadderCacheBps`, `latencyLadderCacheWeakestBps`, `latencyLadderCacheAdmissible` (proto fields 37 to 43); the daemon's ladder line (`cache [512 MiB]`) | the rung visible on every node |
| The gate | `admissible` in the genesis list, false until measured: the cold verify of one warp with the 512 MiB cache on the reference core with its SMT sibling loaded under 10 ms (the verifier reads the cache, so this is the bound), the day-cache build on a 2019-class core under twice today's, and the 8 GB tier still holding dataset, cache and the prover footprint | the rule never enters an inadmissible rung (tested) |
Why beside N and not a seventh N rung: the six approved rungs and their indices stand (the project lead, 7 October 2026, 09:3x UK); a rung inserted in the list would either sit behind the three inadmissible rungs (unreachable) or shift the approved indices. A second lever on the same signal carrier keeps the list as approved and the cool-down shared.
Owed with the measurement: the engine's consumption of `cache_mib` (`EpochSeeds` carries `shadow_reps` today and no cache size; the pack and the three hosts build a 256 MiB cache), so the flag stays false until the path exists and is measured. Per tier what the rung means: every tier from 8 GB holds a 512 MiB cache; the day-cache build doubles (about 0.7 s on the reference core today, approximate, from the class v4 fill line); the Apple tier's unified memory holds it; a pool user does nothing.
## 4. The class-group VDF under a quantum computer
Flagged in spec 04 section 4.8, not sized: Shor computes the class-group order, which removes the sequentiality assumption of the Wesolowski VDF, so an attacker with a cryptographically relevant quantum computer grinds the hourly seed (a liveness nuisance against the lottery, not a safety break; finality rests on the vote keys of section 1). The fallback is a hash-chain delay behind the same version byte, designed when the scheme flip is scheduled.
## 5. Gates
| Gate | Where | Result |
|---|---|---|
| The digest test | `consensus_digest_covers_every_consensus_field_and_nothing_else` (30 edits; the scheme byte and the cache rung alone move nothing), `override_params_carry_the_genesis_forward_fields_and_the_digest_moves_only_when_set` | section 6 |
| Scheme 1 refused by every node until the signal | unit: `a_vote_reveal_or_successor_of_another_signature_scheme_is_refused_until_a_class_names_it` (RPC, in a block, a reveal, a successor; the explicit template form); fast-time: `probe-scheme` on three nodes | section 6 |
| A succession carried once and refused twice | unit: `a_key_hands_its_window_to_a_successor_once_and_a_second_succession_is_refused` (two nodes); fast-time: `infra/fast-time/key-succession.mjs` (the known-failed case `--expect no-succession` first) | section 6 |
| The ladder rung visible in the RPC | `powEpoch.latencyLadderCache*` on `getBlockTemplate`, the daemon line; unit: `latency_ladder_rule` (an inadmissible cache rung never entered; with the flag, 90 percent in seven windows enters it, N still steps after it, one rung, never back) | section 6 |
| The codec | `scheme_byte_and_succession_items_round_trip_and_an_older_decoder_stops_at_them` | section 6 |
## 6. Results (7 October 2026, 12:5x to 14:3x UK; igneum-build-1 for the gate build, the harness and the digests, igneum-build-2 for the suites)
| Gate | Run | Result |
|---|---|---|
| The digest test | `cargo test --release -p kaspa-consensus-core --lib` on build-2 at 75810130 | 124 passed, 0 failed, 2 ignored (`consensus_digest_covers_every_consensus_field_and_nothing_else` with 30 edits, `override_params_carry_the_genesis_forward_fields_and_the_digest_moves_only_when_set`, `latency_ladder_rule` with the cache rung) |
| The 60x keeper test | `fast_time_60x_file_is_the_devnet_at_60x` on build-2 against the completed file (repo 9c9a1f52) | 1 passed (the file had lacked 17 fields on master, `emission` and `proving_consensus_verify_daa` among them; the box mirrors carry no `infra/`, so the test skips there unless the file is shipped) |
| Scheme 1 refused by every node | unit `a_vote_reveal_or_successor_of_another_signature_scheme_is_refused_until_a_class_names_it`; fast-time `probe-scheme` on three nodes, both harness cases | green in the suite runs; every node: `SCHEME 1 REFUSED ... (refused: vote carries signature scheme 1; the active scheme is 0 (BLS12-381); another scheme is named only by a program class the 95 percent signal moves to, and none names one)` |
| A succession carried once and refused twice | unit `a_key_hands_its_window_to_a_successor_once_and_a_second_succession_is_refused` (two nodes); fast-time `infra/fast-time/key-succession.mjs` on build-1 (3 nodes, one CPU miner each, override-60x with the three switches at 0) | known-failed case first (`--expect no-succession`, 13:04 to 13:13 UK, `genesis-forward-harness/known-failed-no-succession.json`): FAIL as it must on every carried-once check with the probe, the locks and the sinks holding. Pass case (`--expect succession`, node bdb34f62, 13:23 to 13:28 UK, `pass-succession.json`): PASS, every check holds: w5-old held 42 blocks at DAA 260; the succession accepted on n2 at DAA 261 and carried by a block at DAA 266 on every node; at the fold (DAA 290) every node reads w5-new 37 blocks (7 mined alone) and w5-old 0 with `succeededTo`; w5-old to w5-third refused on n2 and n0 (`already handed`), w5-new to w5-old refused on n1 (`has itself handed`); locks 4 to 7 on every node after the fold; 0 rejected blocks, sinks agree; 317 s wall |
| The cache rung visible in the RPC | `powEpoch.latencyLadderCache*` (proto fields 37 to 43); the daemon's ladder line | the testnet-shaped file prints `rungs 27, 35, 53, [88], [173], [267] shadow passes, cache [512 MiB] beside them` (`digest-testnet-shape.log`) |
| The codec | `scheme_byte_and_succession_items_round_trip_and_an_older_decoder_stops_at_them` | green in the suite runs |
| The digest, cross-binary | igneumd 75810130 against the 0.3.20 base dc141409 (bs0319's build), both with no file, devnet suffix 973 | both `079d8a7e736abbd4` (`digest-no-file.log`, `digest-base-dc141409.log`): the three fields at never move nothing; the ladder alone at 0 reads `772f9f8e3ef59ca1`, the testnet-shaped file (ladder at 0, scheme switch at 0, succession at 0, the cache rung) `60e842a738c7dea0`, on devnet params; the testnet lane's own number comes from `TESTNET_PARAMS` at its cut. The 0.3.18 line's `c562d70e` is behind the decimals, tail-emission and subsidy fields of the 0.3.19 and 0.3.20 lines, not behind these |
| The gate build | `tools/build-remote.sh --priority gate` on build-1 at bdb34f62 | igneumd 57,554,336 B sha256 6d0aa37b..., igneum-miner 10,240,768 B sha256 69fc3c63... (commit string carried) |
| The consensus suite | `cargo test --release -p kaspa-consensus` (all targets) on build-2 at f95178a1, twice | 114 passed, 0 failed, 3 ignored in the lib binary both times, the two moved targets 1 each; `cargo check -p kaspad` clean. Before the fix below the lib binary failed 1 of 114 on one of the two-node tests in three of four runs (the succession test once, the pre-existing F23 test twice), node 1 refusing block 61 at DAA 60 with `UnexpectedDifficulty`, the two nodes' bits about 17,000 apart in the mantissa |
Faults found on the way, both mine: (1) rule v3's frozen table stood at the lock before the carrier, where the old key still weighed and signed nothing, so nothing locked after the fold (fixed in `frozen_table`, 9322cc1d); (2) the template read the explicit-form switch from the process-wide static that only the daemon installs, so `TestConsensus` wrote the plain form (the manager holds the activation now, daa61847).
### 6a. The two-node refusal: found and fixed (f95178a1)
The probe on build-2: F23 alone three times, green each time; the three two-node tests together three times, green each time; so the refusal needed the rest of the lib binary. The cause: `consensus/src/consensus/services.rs` built the difficulty manager with `igneum::pow_epoch_blocks()`, the process-wide static, where every other argument of that constructor comes from `params`; two pruning-proof tests in the same binary (`igneum_m20_tests.rs:27`, `igneum_pow.rs:346`) install a 60-block schedule process-wide, so a `TestConsensus` built inside that window read a 60-block epoch into its difficulty manager while its partner read 3,600, the two disagreed on `reference_window` from DAA 60, and the second node refused block 61, the exact block of every refusal. The fix is the manager's own field, `params.pow_epoch_blocks`. The node's behaviour is unchanged (the daemon installs the static from the same params before building the consensus); the class is the static-at-construction read, the sibling of the signal-table race the node lane moved two installing tests for on 7 October 2026. The test helper now names the node and the block it refuses, which is what found it.
## 7. For the testnet lane
Override keys and testnet values: `sig_scheme: 0`, `sig_scheme_activation_daa: 0`, `finality_succession_activation_daa: 0`, `latency_ladder_cache_rung: {"mib": 512, "admissible": false}` (the six N rungs unchanged). Digest: with the ladder already active from genesis the cache rung's two fields enter after the six rungs' fields; the scheme switch adds two fields, the succession switch one. `print_testnet_object` prints the four keys.