Key succession, the pin tool's half: elf/prior/ and the manifest's prior and succession blocks, the host embedding both pairs, the release-manifest check comparing both
docs/design/key-succession.md (the enforced-proving lane, 8 October 2026): the object names the prior pair, a next pair, an activation height H and a window W; the node embeds both pairs and verifies under the pair a proof claims when that pair is accepted at the carrier's DAA (the node branch key-succession-node on the fork carries that side). The pin tool (igneum-prove-pin): a pin over an existing elf/ moves the four files it holds to elf/prior/ and writes the manifest's `prior` block (the same per-program fields plus the pinned_at of that pair) 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/); `--no-prior` keeps today's shape. The host (pinned.rs) embeds elf/prior/'s four files, checks them against the `prior` block when the manifest carries one and derives the prior ids from the keys (Pinned::prior, PriorKeys), checks the `succession` block's four ids against both pairs, and builds unchanged on a tree whose manifest names no `prior` (every tree now carries elf/prior/ as a copy of the current pair, so the include_bytes compile; the in-process verifier reads `prior` as absent). The release manifest check refuses a served manifest whose `prior`, `succession`, succession_daa or succession_window_daa differ from elf/manifest.json's, and a `prior` present on one side only. 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. The first real pin (the live pair 0x2b1a81cb…/0x474678f3… into prior/, the re-pinned 0x282dcfce…/0x3fd721e8… on top) runs when main names H. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
ecb70db5bb
commit
07c8380002
7 changed files with 128 additions and 2 deletions
BIN
proving/igneum-prove/elf/prior/igneum-prove-aggregator.elf
Normal file
BIN
proving/igneum-prove/elf/prior/igneum-prove-aggregator.elf
Normal file
Binary file not shown.
BIN
proving/igneum-prove/elf/prior/igneum-prove-aggregator.vk
Normal file
BIN
proving/igneum-prove/elf/prior/igneum-prove-aggregator.vk
Normal file
Binary file not shown.
BIN
proving/igneum-prove/elf/prior/igneum-prove-program.elf
Normal file
BIN
proving/igneum-prove/elf/prior/igneum-prove-program.elf
Normal file
Binary file not shown.
BIN
proving/igneum-prove/elf/prior/igneum-prove-program.vk
Normal file
BIN
proving/igneum-prove/elf/prior/igneum-prove-program.vk
Normal file
Binary file not shown.
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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<PriorPair>,
|
||||
/// 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<Succession>,
|
||||
}
|
||||
|
||||
/// 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<PriorKeys>,
|
||||
}
|
||||
|
||||
/// 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<Elf> {
|
||||
self.prior.as_ref().map(|_| Elf::Static(PRIOR_SHARD_ELF_BYTES))
|
||||
}
|
||||
pub fn prior_agg_elf(&self) -> Option<Elf> {
|
||||
self.prior.as_ref().map(|_| Elf::Static(PRIOR_AGG_ELF_BYTES))
|
||||
}
|
||||
|
||||
pub fn shard_elf(&self) -> Elf {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue