From c5ba3182a01c866313c7409f6b1b73726daa2248 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:15:56 +0000 Subject: [PATCH] Key succession: a re-pin of the proving programs as a scheduled transition (docs/design/key-succession.md; the coordinator's ruling of 8 October 2026, 18:1x UK) The object names a prior pair, a next pair, an activation height and a window; below the height only the prior pair verifies and pays, in the window either, after it only the next; the node embeds both pairs (elf/ and elf/prior/), the body rule, the payment rule and the native segment statement read one floor, the daemon refuses to start without every pair the object accepts at the tip, the manifests carry both pairs and the height; never on every object, in the digest once set. Tests known-failed first (the seven refusals on both sides of the height) and the fast-time case across it; the node branch by 18:00 UK on 9 October 2026. Co-Authored-By: Claude Fable 5.1 --- docs/design/key-succession.md | 86 +++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/design/key-succession.md diff --git a/docs/design/key-succession.md b/docs/design/key-succession.md new file mode 100644 index 000000000..acac441c4 --- /dev/null +++ b/docs/design/key-succession.md @@ -0,0 +1,86 @@ +# Key succession: a re-pin of the proving programs as a scheduled transition, never a genesis + +8 October 2026, 18:2x UK, the enforced-proving lane, under the coordinator's ruling of 18:1x UK after the shard guest's re-pin (`proving-payment-guest` afd952e89: the D4 proving-payment split, shard program id 0x282dcfce…, aggregator id 0x3fd721e8…). The ruling: node 2.0.1 ships node-only on the live keys; the re-pinned guest and the D4 split wait for a designed mechanism, the Igneum 2.0 plan's own pin ("program identities, verifier versions and security parameters pinned in the protocol with a carefully versioned replacement path"). This document is that mechanism. Every figure is stated (code read) or owed with its clock; nothing here changes any network until the node change lands behind its switch, never on every object. + +## 1. The problem, read from the code + +A node pins the proving program ids it embeds. `proving/igneum-prove/elf/` holds one pair of verifying keys (`igneum-prove-program.vk`, `igneum-prove-aggregator.vk`) and their manifest; `igneum/exec/src/nativeverify.rs` embeds that one pair (`Keys::embedded`) and `verify` refuses a proof whose claimed program id (`sp1_vk_digest` of its recursion public values) is not the one pinned id. The object may name the pair (`proving_shard_program_id`, `proving_aggregator_id`) or leave it empty under `verifier_in_consensus`, in which case each node pins what it embeds and the daemon prints the pair at start. The native segment statement names the pair too (`BlockStatement.shard_vk` and `.agg_vk`, checked in `check_segment_record` against `ProvingConfig::{shard_program_id, aggregator_id}`, `igneum/exec/src/proving.rs` 546 to 652). + +So on igneum-devnet-4 (the rule on from block zero, the ids empty) a node built from a tree with the new `elf/` pins the new pair, the live population pins the old, and each refuses the other's records as unverified: the body rule invalidates the other's carrying blocks, the payment rule pays nothing of the other's. Two populations, one chain, no rule that lets them agree: a consensus change wearing a node-only cut's clothes. That is what the node lane refused to ship on 8 October, correctly. + +## 2. The mechanism + +A succession is a scheduled transition from a prior pair to a next pair at an activation height, with a window in which both pairs are accepted, after which only the next pair is. Everything about it is in the object and the manifest, so every node computes the same answer from the block's DAA score alone. + +### 2.1 The object (consensus parameters) + +New fields on `Params` and `OverrideParams` (`consensus/core/src/config/params.rs`), the 0.3.15 rule for new fields (in the digest only once set, so no live digest moves on the binary rollout): + +| Field | Meaning | Default | +|---|---|---| +| `proving_key_succession_daa` | the activation height: from this DAA score the next pair is accepted | `u64::MAX` = never, every object | +| `proving_key_succession_window_daa` | the window: for this many DAA seconds from the height the prior pair is still accepted | 86,400 (one day, one weight window on mainnet's clock; the devnet sets its own) | +| `proving_next_shard_program_id`, `proving_next_aggregator_id` | the next pair, hex | empty | + +The current fields `proving_shard_program_id` and `proving_aggregator_id` keep their meaning as the pair in force before the height (the prior pair). Under `verifier_in_consensus` with the ids empty they are the embedded prior pair, as today. + +The three epochs of a block at DAA score `d`, with `H` the height and `W` the window: + +| Epoch | Accepted pairs | Native segment statement `shard_vk` / `agg_vk` | +|---|---|---| +| `d < H` | prior only | the prior ids | +| `H <= d < H + W` | prior or next | either pair, and the record's own claim decides which statement is native: the node computes both and accepts the one the record names | +| `d >= H + W` | next only | the next ids | + +The epoch is the carrier block's DAA score for a shard or segment record (the block whose coinbase carries the record), the same score every rule in this family reads (`proof_rule_active_from`, `verified_payout_from`). + +### 2.2 The node + +1. **Two embedded pairs.** `proving/igneum-prove/elf/` keeps the current pair; a new directory `proving/igneum-prove/elf/prior/` holds the previous pair's two `.vk` files and manifest once a succession is scheduled (the re-pin tool writes it: the old pair moves to `prior/`, the new pair takes `elf/`). `nativeverify::Keys` grows to `{ current, prior: Option }`; `verify_kind` takes the carrier's DAA score and the succession parameters and verifies under the pair the proof claims, when that pair is accepted at that score; a proof claiming a pair not accepted at that score is `Invalid("program id X is not accepted at DAA d: the succession at H accepts ...")`. A tree without `prior/` has `prior: None` and behaves as today. +2. **The body rule** (`check_carried_proofs`) passes the block's DAA score to the oracle; the oracle's verdict cache is keyed by proof hash and DAA epoch (a proof verified in the window is not thereby valid after it). +3. **The payment rule** (`carried_payouts`, `carried_segment_payouts`) uses the same oracle, so a record pays only under a pair accepted at its carrier; no second rule. +4. **The native segment statement** (`block_statement`, `check_segment_record`): in the window the node computes the statement under the pair the record names (`st.shard_vk`, `st.agg_vk`) when that pair is accepted at the carrier, else under the one accepted pair; outside the window as today with the accepted pair. +5. **The daemon's start refusal**: with the rule set, the binary must embed every pair the object accepts at the current tip's DAA score (the prior pair below `H + W`, the next pair from `H`), or it stops with exit 3 and names the missing pair; it prints both pairs and the height at start, which is the journal's read-back line. +6. **The proof pool and relay** (`submit`, the verifier loop): a relayed proof is verified under its claimed pair when that pair is accepted at the next block's DAA score; a proof under the next pair before `H` is held unverified, not refused, and is re-verified at `H` (the relay may deliver it early). +7. **The prover side** (`igneum-prove-host`, the app's prover): proves under the pair its host embeds; a prover on the old host keeps earning through the window and stops earning at `H + W`, which is the operator's signal to move; `igneum_getProvingStatus` reports the height, the window and which pair the node's own prover uses. + +### 2.3 The manifest and the served text + +`proving/igneum-prove/elf/manifest.json` carries `current` (the pair in `elf/`) and, once scheduled, `prior` (the pair in `elf/prior/`) and `succession: { daa, window_daa }`. `site/release-manifest.json`'s proving block carries the same, and `tools/ci/release-manifest-check.mjs` keeps the two equal, as today; the served page says, verbatim, "program ids change only at a scheduled height every node knows, with a window in which both are accepted", and names the height when one is set. + +### 2.4 What it is not + +Not a genesis: the chain, its history and every record below `H` stand. Not a trust change: every node still verifies every proof under a pinned key; what changes is which key is pinned at which height, and that is in the object like every other height switch. Not a signalling vote: a succession is a protocol parameter set by the object's owner for a devnet and by the class-change threshold (95 percent with a floor height) on a live network, as the design document's three thresholds say. + +## 3. The tests, known-failed first + +Unit level (`consensus-core` for the params, `igneum-exec` for the verifier and the statement; on build-2): + +| Test | Assertion | +|---|---| +| `succession_is_never_on_every_compiled_object_and_in_the_digest_once_set` | the four objects at never; a file that omits it changes nothing; a set height moves the digest | +| `below_the_height_only_the_prior_pair_verifies` | a proof under the next pair at `d < H`: `Invalid("not accepted at DAA")`, the block refused, nothing paid | +| `in_the_window_either_pair_verifies_and_pays_once` | at `H <= d < H + W` a real proof under each pair verifies; the shard pays once whichever came first | +| `after_the_window_only_the_next_pair_verifies` | a proof under the prior pair at `d >= H + W`: refused, nothing paid | +| `the_seven_refusals_hold_on_both_sides_of_the_height` | the seven shapes of `docs/spec/proving-enforcement.md` section 3 under the prior pair below `H` and under the next pair above `H + W`: each refused as before | +| `the_native_segment_statement_follows_the_accepted_pair` | `shard_vk` and `agg_vk` in the statement are the pair accepted at the carrier; in the window the record's claim decides | +| `a_binary_embedding_one_pair_refuses_to_start_under_a_succession_it_cannot_serve` | the daemon's refusal as a function of (embedded pairs, object, tip DAA), exit 3 and the named pair | +| `a_verdict_cached_in_the_window_is_not_valid_after_it` | the cache key carries the epoch | + +Fast-time (`infra/fast-time/key-succession.mjs`, which exists for the finality vote keys and gets a proving mode, or a sibling script): three local nodes on the devnet object with `proving_key_succession_daa` a few epochs ahead and a short window; a real shard proof under the prior pair (the testnet join pass fixtures) and one under the next pair (made on build-1 with the re-pinned host, 94 s a shard at `SHARD_SIZE=1048576`, the build-server lane's figure); the modified producer of the enforcement harness submits the seven shapes under each pair below `H`, in the window and after `H + W`; PASS = below `H` the next-pair proof is refused and the prior-pair proof pays, in the window both pay once, after `H + W` the prior-pair proof is refused and the next-pair proof pays, and the seven shapes pay nothing anywhere. + +## 4. Activation on igneum-devnet-4 and the rule that stands + +1. The node change lands on a branch off `release-2.0.0-node` with the tests above, never on every object; the node lane takes it into the node line it names. +2. igneum-devnet-4 schedules its first succession in its object (the prior pair = the live pair 0x2b1a81cb…/0x474678f3…, the next pair = the re-pinned 0x282dcfce…/0x3fd721e8…) at a height every node can reach after the binary rollout, with a window of at least one epoch; the digest moves with the object, so it is a minute with the fresh-install gate, as every digest move is. +3. `proving_payment_activation_daa` is set at or after `H + W`, never before: the D4 split is in the next pair's guest only, so a block paying the split under the prior pair's guest would prove to another root. The rule of the proving-payment pin stands: the floor only after the next pair is the only accepted pair. +4. Read-back: the daemon's start line names both pairs and the height; the journal shows a prior-pair proof refused after `H + W` and a next-pair proof paid after `H`. + +## 5. Clocks + +| What | Clock | +|---|---| +| This document on master | tonight, 8 October 2026 | +| The node change on a branch, the unit tests green on build-2 | 18:00 UK, 9 October 2026 | +| The fast-time case across the height | with the branch, or named as owed at 18:00 with its own clock | +| The devnet-4 succession object | the node lane's cut, after the branch is green and on main's word |