diff --git a/proving/igneum-prove/elf/prior/igneum-prove-aggregator.elf b/proving/igneum-prove/elf/prior/igneum-prove-aggregator.elf new file mode 100644 index 000000000..a6d068225 Binary files /dev/null and b/proving/igneum-prove/elf/prior/igneum-prove-aggregator.elf differ diff --git a/proving/igneum-prove/elf/prior/igneum-prove-aggregator.vk b/proving/igneum-prove/elf/prior/igneum-prove-aggregator.vk new file mode 100644 index 000000000..51e8c6c00 Binary files /dev/null and b/proving/igneum-prove/elf/prior/igneum-prove-aggregator.vk differ diff --git a/proving/igneum-prove/elf/prior/igneum-prove-program.elf b/proving/igneum-prove/elf/prior/igneum-prove-program.elf new file mode 100644 index 000000000..8db866f14 Binary files /dev/null and b/proving/igneum-prove/elf/prior/igneum-prove-program.elf differ diff --git a/proving/igneum-prove/elf/prior/igneum-prove-program.vk b/proving/igneum-prove/elf/prior/igneum-prove-program.vk new file mode 100644 index 000000000..3c052ce7f Binary files /dev/null and b/proving/igneum-prove/elf/prior/igneum-prove-program.vk differ diff --git a/proving/igneum-prove/host/src/bin/pin.rs b/proving/igneum-prove/host/src/bin/pin.rs index 93a4bd6e3..29ec4e7c6 100644 --- a/proving/igneum-prove/host/src/bin/pin.rs +++ b/proving/igneum-prove/host/src/bin/pin.rs @@ -70,10 +70,32 @@ fn main() -> Result<()> { let out = arg("--out").map(PathBuf::from).unwrap_or_else(|| here.join("elf")); std::fs::create_dir_all(&out)?; let host = std::process::Command::new("uname").arg("-a").output().ok().map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()).unwrap_or_else(|| "unknown".into()); + // Key succession (docs/design/key-succession.md, 8 October 2026): a pin over an existing elf/ moves the pair it + // holds to elf/prior/ and writes the manifest's `prior` block (the same per-program fields) and a `succession` + // block naming both pairs' ids; the top-level shard and aggregator stay the newest pair. At most two pairs live: + // a pin over an elf/ that already carries prior/ replaces prior/ with the current pair. The node reads H, W and + // the next ids from its object, never from the manifest; `succession` is for the served manifest and its check. + // `--no-prior` keeps today's shape (the bootstrap, or a pin that replaces a pair never served). + let keep_prior = !args.iter().any(|a| a == "--no-prior"); + let prior = if keep_prior && out.join("manifest.json").exists() { + let current: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(out.join("manifest.json"))?)?; + let prior_dir = out.join("prior"); + std::fs::create_dir_all(&prior_dir)?; + for name in ["igneum-prove-program", "igneum-prove-aggregator"] { + for ext in ["elf", "vk"] { + let f = format!("{name}.{ext}"); + std::fs::copy(out.join(&f), prior_dir.join(&f)).with_context(|| format!("move {f} to prior/"))?; + } + } + println!("RESULT pin: the pair pinned at {} moved to {} (prior)", current["pinned_at"].as_str().unwrap_or("?"), prior_dir.display()); + Some(serde_json::json!({ "shard": current["shard"], "aggregator": current["aggregator"], "pinned_at": current["pinned_at"] })) + } else { + None + }; let light = LightProver::new(); let shard = pin_one(&light, &from, &out, "igneum-prove-program")?; let aggregator = pin_one(&light, &from, &out, "igneum-prove-aggregator")?; - let manifest = serde_json::json!({ + let mut manifest = serde_json::json!({ "format": "igneum-prove-elf-manifest-v1", "sp1_crate_version": "6.8.1", "sp1_circuit_version": sp1_sdk::SP1_CIRCUIT_VERSION, @@ -82,6 +104,26 @@ fn main() -> Result<()> { "shard": shard, "aggregator": aggregator, }); + if let Some(p) = prior { + manifest["succession"] = serde_json::json!({ + "prior_shard_program_id": p["shard"]["program_id"], + "prior_aggregator_id": p["aggregator"]["program_id"], + "next_shard_program_id": manifest["shard"]["program_id"], + "next_aggregator_id": manifest["aggregator"]["program_id"], + }); + manifest["prior"] = p; + } else { + // no pair to carry: prior/ is a copy of the current pair so the host's include_bytes compile, the manifest + // names no `prior` (the in-process verifier reads it as absent) + let prior_dir = out.join("prior"); + std::fs::create_dir_all(&prior_dir)?; + for name in ["igneum-prove-program", "igneum-prove-aggregator"] { + for ext in ["elf", "vk"] { + let f = format!("{name}.{ext}"); + std::fs::copy(out.join(&f), prior_dir.join(&f))?; + } + } + } std::fs::write(out.join("manifest.json"), format!("{}\n", serde_json::to_string_pretty(&manifest)?))?; println!("RESULT pin: manifest written to {}; rebuild the host so it embeds these files", out.join("manifest.json").display()); Ok(()) diff --git a/proving/igneum-prove/host/src/pinned.rs b/proving/igneum-prove/host/src/pinned.rs index c87256d1e..2843a2b4b 100644 --- a/proving/igneum-prove/host/src/pinned.rs +++ b/proving/igneum-prove/host/src/pinned.rs @@ -26,6 +26,13 @@ pub const SHARD_VK_BYTES: &[u8] = include_bytes!("../../elf/igneum-prove-program pub const AGG_ELF_BYTES: &[u8] = include_bytes!("../../elf/igneum-prove-aggregator.elf"); pub const AGG_VK_BYTES: &[u8] = include_bytes!("../../elf/igneum-prove-aggregator.vk"); pub const MANIFEST_JSON: &str = include_str!("../../elf/manifest.json"); +/// Key succession (docs/design/key-succession.md, 8 October 2026): the prior pair's files under `elf/prior/` (the same +/// names), present in every tree (a copy of the current pair until a pin moves one there, the manifest then naming no +/// `prior`); the host embeds them so a node carries both pairs across a succession window. +pub const PRIOR_SHARD_ELF_BYTES: &[u8] = include_bytes!("../../elf/prior/igneum-prove-program.elf"); +pub const PRIOR_SHARD_VK_BYTES: &[u8] = include_bytes!("../../elf/prior/igneum-prove-program.vk"); +pub const PRIOR_AGG_ELF_BYTES: &[u8] = include_bytes!("../../elf/prior/igneum-prove-aggregator.elf"); +pub const PRIOR_AGG_VK_BYTES: &[u8] = include_bytes!("../../elf/prior/igneum-prove-aggregator.vk"); /// One pinned program: file names, hashes and the program id (`agg::vk_bytes` of the verifying key's `hash_u32`). #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -47,6 +54,29 @@ pub struct Manifest { pub pinned_on: String, pub shard: PinnedProgram, pub aggregator: PinnedProgram, + /// Key succession: the pair pinned before this one (`elf/prior/`), present only once a pin has moved one there. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub prior: Option, + /// Key succession: both pairs' ids as the served manifest carries them (the node reads H, W and the next ids + /// from its object, never from here). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub succession: Option, +} + +/// The prior pair's block: the same per-program fields as the top level and the time it was pinned. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct PriorPair { + pub shard: PinnedProgram, + pub aggregator: PinnedProgram, + pub pinned_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct Succession { + pub prior_shard_program_id: String, + pub prior_aggregator_id: String, + pub next_shard_program_id: String, + pub next_aggregator_id: String, } impl Manifest { @@ -78,6 +108,17 @@ pub struct Pinned { pub agg_vk: SP1VerifyingKey, pub shard_id: B256, pub agg_id: B256, + /// Key succession: the prior pair (its two verifying keys and ids) when the manifest carries a `prior` block and + /// the embedded prior files hash to it; `None` on today's shape. + pub prior: Option, +} + +/// The prior pair as loaded: the keys and the ids derived from them. +pub struct PriorKeys { + pub shard_vk: SP1VerifyingKey, + pub agg_vk: SP1VerifyingKey, + pub shard_id: B256, + pub agg_id: B256, } impl Pinned { @@ -95,7 +136,35 @@ impl Pinned { let agg_id = program_id_of(&agg_vk); check_id("shard", shard_id, &manifest.shard.program_id)?; check_id("aggregator", agg_id, &manifest.aggregator.program_id)?; - Ok(Self { manifest, shard_vk, agg_vk, shard_id, agg_id }) + let prior = match &manifest.prior { + Some(p) => { + check_file("prior shard ELF", PRIOR_SHARD_ELF_BYTES, &p.shard.elf_sha256)?; + check_file("prior shard verifying key", PRIOR_SHARD_VK_BYTES, &p.shard.vk_sha256)?; + check_file("prior aggregator ELF", PRIOR_AGG_ELF_BYTES, &p.aggregator.elf_sha256)?; + check_file("prior aggregator verifying key", PRIOR_AGG_VK_BYTES, &p.aggregator.vk_sha256)?; + let pvk: SP1VerifyingKey = bincode::deserialize(PRIOR_SHARD_VK_BYTES).context("the embedded prior shard verifying key does not deserialize")?; + let pavk: SP1VerifyingKey = bincode::deserialize(PRIOR_AGG_VK_BYTES).context("the embedded prior aggregator verifying key does not deserialize")?; + let (psid, paid) = (program_id_of(&pvk), program_id_of(&pavk)); + check_id("prior shard", psid, &p.shard.program_id)?; + check_id("prior aggregator", paid, &p.aggregator.program_id)?; + if let Some(s) = &manifest.succession { + check_id("succession prior shard", psid, &s.prior_shard_program_id)?; + check_id("succession prior aggregator", paid, &s.prior_aggregator_id)?; + check_id("succession next shard", shard_id, &s.next_shard_program_id)?; + check_id("succession next aggregator", agg_id, &s.next_aggregator_id)?; + } + Some(PriorKeys { shard_vk: pvk, agg_vk: pavk, shard_id: psid, agg_id: paid }) + } + None => None, + }; + Ok(Self { manifest, shard_vk, agg_vk, shard_id, agg_id, prior }) + } + + pub fn prior_shard_elf(&self) -> Option { + self.prior.as_ref().map(|_| Elf::Static(PRIOR_SHARD_ELF_BYTES)) + } + pub fn prior_agg_elf(&self) -> Option { + self.prior.as_ref().map(|_| Elf::Static(PRIOR_AGG_ELF_BYTES)) } pub fn shard_elf(&self) -> Elf { diff --git a/tools/ci/release-manifest-check.mjs b/tools/ci/release-manifest-check.mjs index 0cbcdcae3..5b90ddd34 100644 --- a/tools/ci/release-manifest-check.mjs +++ b/tools/ci/release-manifest-check.mjs @@ -28,6 +28,21 @@ if (existsSync(elfp)) { const e = JSON.parse(readFileSync(elfp, 'utf8')); for (const [a, b] of [['shard_program_id', e.shard.program_id], ['shard_elf_sha256', e.shard.elf_sha256], ['shard_vk_sha256', e.shard.vk_sha256], ['aggregator_program_id', e.aggregator.program_id], ['aggregator_elf_sha256', e.aggregator.elf_sha256], ['aggregator_vk_sha256', e.aggregator.vk_sha256], ['sp1_circuit_version', e.sp1_circuit_version], ['sp1_crate_version', e.sp1_crate_version], ['pinned_at', e.pinned_at]]) if (m.proving[a] !== b) fails.push(`manifest: proving.${a} is ${m.proving[a]}, the ELF manifest says ${b}`); + // key succession (docs/design/key-succession.md, 8 October 2026): the served manifest carries the prior pair and the + // succession block exactly as the ELF manifest does; a `prior` present on one side only is a refusal, absent on both is + // today's shape + const ep = e.prior && typeof e.prior === 'object' ? e.prior : null; + const mp = m.proving.prior && typeof m.proving.prior === 'object' ? m.proving.prior : null; + if (!!ep !== !!mp) fails.push(`manifest: proving.prior is ${mp ? 'present' : 'absent'} but the ELF manifest's prior is ${ep ? 'present' : 'absent'}`); + if (ep && mp) { + for (const [a, b] of [['shard_program_id', ep.shard?.program_id], ['shard_elf_sha256', ep.shard?.elf_sha256], ['shard_vk_sha256', ep.shard?.vk_sha256], ['aggregator_program_id', ep.aggregator?.program_id], ['aggregator_elf_sha256', ep.aggregator?.elf_sha256], ['aggregator_vk_sha256', ep.aggregator?.vk_sha256], ['pinned_at', ep.pinned_at]]) + if (mp[a] !== b) fails.push(`manifest: proving.prior.${a} is ${mp[a]}, the ELF manifest's prior says ${b}`); + const es = e.succession || {}, ms = m.proving.succession || {}; + for (const k of ['prior_shard_program_id', 'prior_aggregator_id', 'next_shard_program_id', 'next_aggregator_id']) + if (ms[k] !== es[k]) fails.push(`manifest: proving.succession.${k} is ${ms[k]}, the ELF manifest says ${es[k]}`); + for (const k of ['succession_daa', 'succession_window_daa']) + if (!(k in m.proving) || !(Number.isInteger(m.proving[k]) || m.proving[k] === 'never')) fails.push(`manifest: proving.${k} must be an integer DAA or "never" when a prior pair is carried`); + } } const vercel = JSON.parse(readFileSync(join(site, 'vercel.json'), 'utf8')); if (!(vercel.rewrites || []).some(r => r.source === '/release.json' && r.destination === '/release-manifest.json')) fails.push('vercel.json: no rewrite /release.json -> /release-manifest.json');