igneum/docs/design/genesis-forward.md

16 KiB

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.