Proving v1: prover on by default, the aggregated segment record, host modes chain, aggregate and verify-segment, the app's aggregator step, spec 7.8, the fast-time harness and the coverage tool

Step 1: app/igneum-app/src/provedefault.rs decides once per install (NVIDIA 12 GB or more, WSL2 answering on
Windows, Linux native, Apple silicon off until measured), never switching an explicit on back off; the Settings
switch line and the tile line say why (5 unit tests). tools/proving-v1/pc2-prover-cost.ps1 is the PC 2 job
(5 min mining alone, 5 min with the prover, GPU memory and host RAM peaks, the sp1-gpu-server's SM targets).

Step 2: the host gains --mode chain (consecutive fixtures, each block aggregated with the previous block's
proof by recursion), --mode aggregate (the live aggregator over shard proof files, a run of blocks in one
process) and --mode verify-segment (the node's verifier against the pinned aggregator key); the app's prover
loop gains aggregate_once (spec 7.8). Eight consecutive live fixtures (blocks 81046 to 81053, node 1's export
at tip 81076) under proving/fixtures/chain/. tools/proving-v1/pc2-chain.ps1 is the PC 2 job (held).

Steps 3 and 4: tools/proving-v1/coverage.mjs (the proven-block share and the on-chain latency from one node's
RPC), tools/proving-v1/net.mjs (the fast-time 3-node harness on 29950+ with the known-finished and
known-failed cases of the chain rule and the unproven rule), the four proving_v1 fields in
infra/fast-time/override-60x.json. Spec 7.8, the 7.4 rows, the 5.3 sentence, docs/plans/proving-v1.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-05 19:08:28 +00:00
parent 2fc34ea370
commit 55ea10cc26
29 changed files with 11967 additions and 7 deletions

View file

@ -87,6 +87,10 @@ pub struct Settings {
/// so it includes proof records it never verified (src/verifier.rs). Default off; a found verifier always wins.
#[serde(default)]
pub proof_verify_trust: bool,
/// Proving v1 step 1 (5 October 2026): the install-time default for `prove` has been applied once (src/provedefault.rs:
/// on when the machine can prove, never switching an explicit on back off). Older installs apply it at their next start.
#[serde(default)]
pub prove_default_applied: bool,
}
fn one() -> u32 {
@ -98,7 +102,7 @@ fn yes() -> bool {
impl Default for Settings {
fn default() -> Settings {
Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false }
Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false, prove_default_applied: false }
}
}

View file

@ -253,6 +253,40 @@ impl Shared {
Ok(json!({ "ok": true, "address": w.address, "display": keys::checksum(&w.address), "private_key": w.private_key, "wallet_file": self.wallet_path.display().to_string() }))
}
/// Proving v1 step 1 (5 October 2026): once per install, after the cards are known, the prover goes on by itself
/// when this machine can prove (src/provedefault.rs: an NVIDIA card with 12 GB or more, WSL2 on Windows, Linux
/// native, Apple silicon off until measured). An explicit on is never switched off; the line goes to the log and
/// to the Proving tile. Older installs apply it at their first start on this version.
pub fn apply_prove_default(&self) {
let (applied, already_on) = {
let s = self.settings.lock().unwrap();
(s.prove_default_applied, s.prove)
};
if applied {
return;
}
let cards = self.state.lock().unwrap().mining.cards.clone();
let wsl = if cfg!(windows) { Some(crate::wslhost::distro_answers()) } else { None };
let d = crate::provedefault::decide(&cards, std::env::consts::OS, wsl);
let on = d.on || already_on;
{
let mut s = self.settings.lock().unwrap();
s.prove = on;
s.prove_default_applied = true;
s.save(&self.settings_path);
}
{
let mut st = self.state.lock().unwrap();
st.settings.prove = on;
st.proving.enabled = on;
st.proving.default_note = d.line.clone();
if !on {
st.proving.status = "off".into();
}
}
self.log(&format!("prover default: {}{}", d.line, if already_on && !d.on { " (left on: it was switched on by hand)" } else { "" }));
}
/// The prover service switch (src/prover.rs); the thread picks it up within 10 s.
pub fn set_prove(&self, on: bool) -> Result<Value, String> {
{
@ -711,6 +745,8 @@ impl Engine {
self.detect_again = false;
self.shared.send(Cmd::Detect);
}
// proving v1 step 1: the install-time prover default, once the cards are known
self.shared.apply_prove_default();
}
Cmd::ApplyCards(choices) => self.apply_cards(choices),
Cmd::Start => self.start(),

View file

@ -29,6 +29,7 @@ mod jobs;
mod jobrun;
mod jobbuild;
mod prover;
mod provedefault;
mod verifier;
mod wslhost;
mod sweep;

View file

@ -0,0 +1,105 @@
//! Proving v1 step 1 (5 October 2026, the project lead: "open the proving round asap"): the prover is on by default on every
//! mining machine that can prove, decided once per install after the cards are detected (src/engine.rs
//! `apply_prove_default`). The rule, one line each:
//!
//! | Machine | Default | Why |
//! |---|---|---|
//! | NVIDIA card with 12 GB or more, Windows, WSL2 (Ubuntu-24.04) answers | on | the SP1 CUDA prover runs inside WSL2 (src/prover.rs) |
//! | NVIDIA card with 12 GB or more, Windows, WSL2 silent | off, with the Set up hint | nothing can prove until the distribution exists |
//! | NVIDIA card with 12 GB or more, Linux | on | the host runs next to the engine |
//! | Apple silicon | off | the CPU prover is minutes per shard; on until it is measured on the GPU |
//! | no NVIDIA card with 12 GB | off | the 12 GB gate (spec 5.1, ledger P1); measured on a 32 GB card only so far |
//!
//! The default never switches an explicit on back off, and Settings always wins afterwards.
use crate::state::CardState;
/// The gate card: 12 GB (spec 5.1, "a shard on a 12 GB card"). `nvidia-smi` reports MiB; 12 GB cards report
/// 12,288 MiB or a little under (the RTX 3060 12 GB reports 12,288), so the test is at 11.5 GB.
pub const MIN_VRAM_MB: u64 = 11_776;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Decision {
pub on: bool,
/// One plain sentence for the log and the Proving tile.
pub line: String,
}
fn gb(mb: u64) -> u64 {
(mb + 512) / 1024
}
/// `os` is `std::env::consts::OS` ("windows", "linux", "macos"); `wsl_answers` is read on Windows only.
pub fn decide(cards: &[CardState], os: &str, wsl_answers: Option<bool>) -> Decision {
let nvidia: Vec<&CardState> = cards.iter().filter(|c| c.vendor == "nvidia").collect();
let able: Vec<&CardState> = nvidia.iter().copied().filter(|c| c.vram_mb >= MIN_VRAM_MB).collect();
let off = |line: String| Decision { on: false, line };
if os == "macos" {
return off("proving stays off on Apple silicon until the GPU prover is measured there; Settings switches it on (CPU, slow)".into());
}
let Some(best) = able.iter().max_by_key(|c| c.vram_mb) else {
let seen = if nvidia.is_empty() {
"no NVIDIA card".to_string()
} else {
nvidia.iter().map(|c| format!("{} {} GB", c.name, gb(c.vram_mb))).collect::<Vec<_>>().join(", ")
};
return off(format!("proving off by default: no NVIDIA card with 12 GB or more ({seen}); Settings switches it on"));
};
let card = format!("{} ({} GB)", best.name, gb(best.vram_mb));
match os {
"windows" => match wsl_answers {
Some(true) => Decision { on: true, line: format!("proving on by default: {card} with WSL2 (Ubuntu-24.04 answers); Settings switches it off") },
_ => off(format!("proving off: {card} qualifies but WSL2 (Ubuntu-24.04) did not answer; Set up installs it, then Settings switches proving on")),
},
"linux" => Decision { on: true, line: format!("proving on by default: {card} on Linux (the host runs next to the engine); Settings switches it off") },
other => off(format!("proving off: {card} on {other}, no prover path there; Settings switches it on")),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn card(vendor: &str, name: &str, vram_mb: u64) -> CardState {
CardState { vendor: vendor.into(), name: name.into(), vram_mb, ..Default::default() }
}
#[test]
fn a_5090_with_wsl2_on_windows_is_on() {
let d = decide(&[card("nvidia", "NVIDIA GeForce RTX 5090", 32_607), card("amd", "AMD Radeon(TM) Graphics", 512)], "windows", Some(true));
assert!(d.on);
assert!(d.line.starts_with("proving on by default: NVIDIA GeForce RTX 5090 (32 GB) with WSL2"), "{}", d.line);
}
#[test]
fn windows_without_wsl2_is_off_with_the_setup_hint() {
let d = decide(&[card("nvidia", "NVIDIA GeForce RTX 4090", 24_564)], "windows", Some(false));
assert!(!d.on);
assert!(d.line.contains("did not answer") && d.line.contains("Set up"), "{}", d.line);
assert!(!decide(&[card("nvidia", "RTX 4090", 24_564)], "windows", None).on, "an unread probe is not an answer");
}
#[test]
fn linux_needs_no_wsl2_and_the_12_gb_gate_holds() {
assert!(decide(&[card("nvidia", "NVIDIA GeForce RTX 3060", 12_288)], "linux", None).on);
let d = decide(&[card("nvidia", "NVIDIA GeForce RTX 3080", 10_240)], "linux", None);
assert!(!d.on);
assert!(d.line.contains("no NVIDIA card with 12 GB or more (NVIDIA GeForce RTX 3080 10 GB)"), "{}", d.line);
assert!(!decide(&[card("amd", "Radeon RX 9070 XT", 16_384)], "linux", None).on, "no CUDA prover for AMD yet");
assert!(decide(&[], "linux", None).line.contains("no NVIDIA card"));
}
#[test]
fn apple_silicon_stays_off() {
let d = decide(&[card("apple", "Apple M5 Max", 65_536)], "macos", None);
assert!(!d.on);
assert!(d.line.contains("Apple silicon"));
assert!(!decide(&[card("nvidia", "RTX 5090", 32_607)], "macos", Some(true)).on, "the OS rule comes first");
}
#[test]
fn the_biggest_qualifying_card_is_named() {
let d = decide(&[card("nvidia", "RTX 3060", 12_288), card("nvidia", "RTX 5090", 32_607)], "linux", None);
assert!(d.line.contains("RTX 5090 (32 GB)"), "{}", d.line);
}
}

View file

@ -339,6 +339,7 @@ pub fn start(shared: Arc<Shared>, bin_dir: PathBuf) {
fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
let mut attempted: HashSet<(String, u32)> = HashSet::new();
let mut attempted_segments: HashSet<u64> = HashSet::new();
let mut tools: Option<Tools> = None;
let mut last_probe = Instant::now() - Duration::from_secs(600);
let mut submitted: Vec<(u64, String, u32, u128)> = Vec::new();
@ -466,6 +467,20 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
p.assigned = assigned;
p.keys = keys.len() as u32;
});
// proving v1 (spec 7.8): the aggregator step, when the node says v1 is active; one attempt a pass
if let Some((label0, _)) = keys.first() {
match aggregate_once(&shared, t, label0, &payout_address(&shared), &mut attempted_segments) {
Ok(Some(msg)) => {
shared.log(&format!("aggregator: {msg}"));
set(&shared, |p| p.segment_note = msg);
}
Ok(None) => {}
Err(e) => {
shared.log(&format!("aggregator: {e}"));
set(&shared, |p| p.segment_note = e);
}
}
}
let Some(w) = choose(&work, &attempted) else {
set(&shared, |p| {
p.status = if submitted.is_empty() { "idle".into() } else { "submitted".into() };
@ -558,6 +573,116 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
}
}
/// Proving v1 (spec 7.8): one aggregation attempt. When the node reports v1 active, takes the newest executed
/// segment that is still pending and not yet attempted here, needs one shard proof per shard of every block in
/// this node's pool (`igneum_getProofBytes`, a verified one when there is one) and, when the previous segment is
/// proven, its aggregated proof (`igneum_getSegmentProofBytes`); runs `igneum-prove-host --mode aggregate` over the
/// run of blocks (one process, one key setup), checks the public values against the node's native statement
/// (every field but `provers`), signs the record with the first key's label and submits it. Returns a line for
/// the log and the tile, or None when there is nothing to do.
fn aggregate_once(shared: &Shared, t: &Tools, label: &str, payout: &str, attempted: &mut HashSet<u64>) -> Result<Option<String>, String> {
let hexu = |x: &Value| x.as_str().and_then(|s| u64::from_str_radix(s.trim_start_matches("0x"), 16).ok()).unwrap_or(0);
let st = evm_rpc(shared, "igneum_getProvingStatus", json!([]), Duration::from_secs(10))?;
let v1 = &st["v1"];
if !v1["active"].as_bool().unwrap_or(false) || v1["start"].is_null() {
return Ok(None);
}
let tip = hexu(&evm_rpc(shared, "eth_blockNumber", json!([]), Duration::from_secs(10))?);
let mut seg = evm_rpc(shared, "igneum_getSegmentStatement", json!([format!("{tip:#x}")]), Duration::from_secs(10))?;
if !seg["executed"].as_bool().unwrap_or(false) {
let first = hexu(&seg["first"]);
if first == 0 || first - 1 < hexu(&v1["start"]) {
return Ok(None);
}
seg = evm_rpc(shared, "igneum_getSegmentStatement", json!([format!("{:#x}", first - 1)]), Duration::from_secs(10))?;
}
let (first, last) = (hexu(&seg["first"]), hexu(&seg["last"]));
if seg["status"]["status"].as_str() != Some("pending") || attempted.contains(&first) || payout.len() != 42 {
return Ok(None);
}
let dir = shared.runtime.app_dir.join("proving").join(format!("seg-{first}"));
let _ = std::fs::create_dir_all(&dir);
let as_host_path = |p: &Path| if t.wsl { wsl_path(p) } else { p.display().to_string() };
// the shard proofs, one per shard of every block, from this node's pool
let mut groups: Vec<String> = Vec::new();
let mut missing: Vec<String> = Vec::new();
for b in seg["blocks"].as_array().cloned().unwrap_or_default() {
let n = hexu(&b["number"]);
let shards = b["shards"].as_u64().unwrap_or(0) as u32;
let have = b["shardProofs"].as_array().cloned().unwrap_or_default();
let mut files = Vec::new();
for i in 0..shards {
let pick = have.iter().find(|e| e["shard"].as_u64() == Some(i as u64) && e["verified"] == json!(true)).or_else(|| have.iter().find(|e| e["shard"].as_u64() == Some(i as u64)));
let Some(e) = pick else {
missing.push(format!("{n}/{i}"));
continue;
};
let got = evm_rpc(shared, "igneum_getProofBytes", json!([format!("{n:#x}"), i, e["keyHash"]]), Duration::from_secs(60))?;
let hex = got["proof"].as_str().ok_or("no proof bytes")?.trim_start_matches("0x").to_string();
let bytes: Vec<u8> = (0..hex.len() / 2).map(|k| u8::from_str_radix(&hex[2 * k..2 * k + 2], 16).unwrap_or(0)).collect();
let f = dir.join(format!("b{n}-s{i}.bin"));
std::fs::write(&f, bytes).map_err(|e| e.to_string())?;
files.push(as_host_path(&f));
}
groups.push(files.join(","));
}
if !missing.is_empty() {
return Ok(Some(format!("segment {first}..{last}: waiting for shard proofs {} in this node's pool", missing.join(" "))));
}
// the previous segment's aggregated proof, when the chain continues
let prev = &seg["previous"];
let (prev_file, expected_pv) = if prev.is_null() {
(None, seg["publicValuesFresh"].as_str().unwrap_or("").to_string())
} else if prev["proofInPool"] == json!(true) {
let got = evm_rpc(shared, "igneum_getSegmentProofBytes", json!([prev["first"], prev["keyHash"]]), Duration::from_secs(60))?;
let hex = got["proof"].as_str().ok_or("no segment proof bytes")?.trim_start_matches("0x").to_string();
let bytes: Vec<u8> = (0..hex.len() / 2).map(|k| u8::from_str_radix(&hex[2 * k..2 * k + 2], 16).unwrap_or(0)).collect();
let f = dir.join("prev-aggregated.bin");
std::fs::write(&f, bytes).map_err(|e| e.to_string())?;
(Some(as_host_path(&f)), seg["publicValuesContinuing"].as_str().unwrap_or("").to_string())
} else {
return Ok(Some(format!("segment {first}..{last}: the previous segment's proof is not in this node's pool; waiting")));
};
attempted.insert(first);
let parent = seg["blocks"][0]["parentHash"].as_str().ok_or("no parent hash")?.to_string();
let last_hash = seg["blocks"].as_array().and_then(|a| a.last()).and_then(|b| b["hash"].as_str()).ok_or("no last hash")?.to_string();
let results = dir.join("results.json");
let started = Instant::now();
set(shared, |p| p.message = format!("aggregating segment {first}..{last} ({})", if t.cuda { "GPU" } else { "CPU, slow" }));
let mut args: Vec<String> = vec!["--mode".into(), "aggregate".into(), "--proofs".into(), groups.join(";"), "--parent".into(), parent, "--out".into(), as_host_path(&results)];
if let Some(pf) = prev_file {
args.push("--prev".into());
args.push(pf);
}
let (ok, out) = run_tool(shared, t, &t.host, &args, &[("SP1_PROVER", if t.cuda { "cuda" } else { "cpu" }), ("RUST_LOG", "off")], Duration::from_secs(2 * 3600), &dir.join("aggregate.log"));
if !ok || !results.exists() {
return Err(format!("segment {first}..{last}: aggregator: {}", out.lines().rev().find(|l| l.contains("RESULT") || l.contains("rror")).unwrap_or("failed")));
}
let res: Value = serde_json::from_str(&std::fs::read_to_string(&results).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
let pv = res["segment_public_values"].as_str().ok_or("no public values in the results")?.to_string();
let proof_sha = res["segment_proof_sha256"].as_str().ok_or("no proof hash in the results")?.to_string();
let proof_file = res["segment_proof_file"].as_str().ok_or("no proof file in the results")?.to_string();
// the node's native statement, every field but provers (bytes 236..268 of the 340)
let strip = |h: &str| { let h = h.trim_start_matches("0x"); if h.len() == 680 { format!("{}{}", &h[..472], &h[536..]) } else { h.to_string() } };
if strip(&pv) != strip(&expected_pv) {
return Err(format!("segment {first}..{last}: the aggregated statement differs from the node's native statement (it would be vetoed); ours {} node {}", &pv[..66.min(pv.len())], &expected_pv[..66.min(expected_pv.len())]));
}
let proof_path = if t.wsl { PathBuf::from(proof_file.replace("/mnt/c/", "C:/")) } else { PathBuf::from(proof_file) };
let sg = crate::detect::run_timeout(crate::platform::quiet(&mut Command::new(&t.miner)).args(["sign-segment-record", label, &chain_name(shared), &first.to_string(), &last.to_string(), &last_hash, payout, &pv, &proof_sha]), None, Duration::from_secs(20)).ok_or("sign-segment-record did not run")?;
let signed: Value = serde_json::from_str(sg.lines().last().unwrap_or("")).map_err(|_| format!("sign-segment-record: {}", sg.trim()))?;
let record = signed["record"].as_str().ok_or("sign-segment-record gave no record")?.to_string();
let proof = std::fs::read(&proof_path).map_err(|e| format!("proof file {}: {e}", proof_path.display()))?;
let proof_hex = format!("0x{}", proof.iter().map(|b| format!("{b:02x}")).collect::<String>());
let r = evm_rpc(shared, "igneum_submitSegmentRecord", json!([{ "record": record, "proof": proof_hex }]), Duration::from_secs(60))?;
if !r["accepted"].as_bool().unwrap_or(false) {
return Err(format!("segment {first}..{last}: record refused: {}", r["reason"].as_str().unwrap_or("?")));
}
let secs = started.elapsed().as_secs_f64();
shared.event("proving", &format!("segment {first}..{last} aggregated and submitted in {secs:.0} s (chain_len {})", res["segment_chain_len"]));
set(shared, |p| p.aggregated += 1);
Ok(Some(format!("segment {first}..{last} aggregated in {secs:.0} s and submitted; paid when a block carries it")))
}
/// Windows: runs the WSL2 setup from the payload (`wsl2/setup-wsl.sh` next to the engine) in a window of its own;
/// the user watches it and reboots when it asks. Elsewhere there is nothing to set up.
pub fn setup(shared: &Shared) -> Result<Value, String> {

View file

@ -196,6 +196,11 @@ pub struct ProvingState {
/// the pinned guests' ids (`igneum-prove-host --mode id`): the shard program and the aggregator; empty until read
pub program_id: String,
pub aggregator_id: String,
/// proving v1 step 1: the install-time default's one plain line (why proving is on or off on this machine)
pub default_note: String,
/// proving v1: segment records this machine aggregated and submitted, and the aggregator's last line
pub aggregated: u32,
pub segment_note: String,
}
#[derive(Clone, Serialize, Default)]

View file

@ -41,6 +41,43 @@ pub fn candidates(bin_dir: &Path) -> Vec<String> {
}
/// The candidates as one line for a message.
/// Whether the distribution answers at all (`wsl.exe -d Ubuntu-24.04 -- echo <marker>` within 30 s): the install-time
/// prover default (src/provedefault.rs) needs WSL2 on Windows before it switches proving on. Elsewhere: false.
#[allow(dead_code)] // also compiled into src/bin/prove-verify.rs, which does not call it
pub fn distro_answers() -> bool {
if !cfg!(windows) {
return false;
}
// self-contained (this file is also compiled into src/bin/prove-verify.rs, which has no detect or platform module)
let mut cmd = std::process::Command::new("wsl");
cmd.args(["-d", DISTRO, "--", "echo", "igneum-wsl-answers"]).stdin(std::process::Stdio::null()).stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::null());
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
}
let Ok(mut child) = cmd.spawn() else { return false };
let Some(out) = child.stdout.take() else { return false };
let reader = std::thread::spawn(move || {
let mut s = String::new();
let _ = std::io::Read::read_to_string(&mut std::io::BufReader::new(out), &mut s);
s
});
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
loop {
match child.try_wait() {
Ok(Some(_)) => break,
Ok(None) if std::time::Instant::now() < deadline => std::thread::sleep(std::time::Duration::from_millis(100)),
_ => {
let _ = child.kill();
let _ = child.wait();
break;
}
}
}
reader.join().map(|o| o.replace('\0', "").contains("igneum-wsl-answers")).unwrap_or(false)
}
pub fn candidates_text(bin_dir: &Path) -> String {
candidates(bin_dir).join(", ")
}

View file

@ -1005,7 +1005,8 @@ if (typeof document !== 'undefined') (function () {
setText('pv-state', w.word + (pv.backend && enabled && pv.available ? ' (' + pv.backend.toUpperCase() + ')' : ''));
setText('pv-state-sub', w.sub);
var cell = $('pv-state').parentNode; cell.classList.toggle('ok', w.tone === 'on' || w.tone === 'ok'); cell.classList.toggle('bad', w.tone === 'bad');
setText('pv-note', w.note);
// proving v1: when off by the install-time default, the default's own line says why (src/provedefault.rs)
setText('pv-note', (!enabled && pv.default_note) ? 'Off. ' + pv.default_note + '.' : w.note);
setText('pv-assigned', String(pv.assigned || 0));
setText('pv-submitted', String(pv.submitted || 0));
setText('pv-paid', String(pv.paid || 0));

View file

@ -206,7 +206,7 @@
<div class="lead-row">
<div class="lead-text">
<h3>Prove shards on this machine</h3>
<p class="help">Every block on Igneum is turned into a short mathematical proof, in pieces called shards. The chain assigns shards to your keys; this machine proves them and is paid for each one.</p>
<p class="help">Every block on Igneum is turned into a short mathematical proof, in pieces called shards. The chain assigns shards to your keys; this machine proves them and is paid for each one. On by default on an NVIDIA card with enough memory (the line below says which); off on a Mac, whose CPU prover is slow.</p>
</div>
<label class="switch lg" title="Prove assigned shards"><input type="checkbox" id="s-prove" aria-label="Prove shards on this machine"><span class="track"></span></label>
</div>

83
docs/plans/proving-v1.md Normal file
View file

@ -0,0 +1,83 @@
# Proving v1: segment records, the chain rule, the unproven rule; the 0.3.11 rollout
5 October 2026, from 18:55 UTC (the project lead: "open the proving round asap"). Branches `proving-v1` in the main repository
(worktree `/Users/joshm/Projects/igneum-wt-proving-v1`, from master a93199a) and in the fork
(`vendor/igneum-node-pv1`, from release-0.3.6 a24ab01a; to be rebased onto the 0.3.10 tip when it lands on
release-0.3.6). Status words follow `docs/spec/00-overview.md` 0.2. Every number here is in `docs/bench-log.md`
with its command. Nothing ships from this plan: it delivers branches, numbers and the rollout for 0.3.11.
## The gap this closes
The litepaper says every block is proven within about a minute. Proving v0 (`proving-v0.md`, spec 7.7) proves
some shards: one prover (PC 2's RTX 5090) takes the newest shard assigned to it, about one shard every 30 s, so
under a tenth of blocks carry a proof; consensus does not require one; the aggregator guest (design 5.3) runs
on fixtures only. Proving v1 adds the aggregated segment record on chain (spec 7.8), the chain rule (segment N's
record verifies N-1, inside the proof by recursion), the unproven rule (a segment nobody proves in T seconds pays
nothing and may be skipped), the prover on by default on every machine that can prove, and the measurements
that say how many cards cover the chain.
## The round, step by step
| Step | What | State |
|---|---|---|
| 1 | Prover on by default (`app/igneum-app/src/provedefault.rs`, `engine.rs apply_prove_default`): on at install when the machine can prove (NVIDIA card with 12 GB or more; WSL2 answering on Windows; Linux native; Apple silicon off until measured), never switching an explicit on back off; the Settings switch line and the tile line say why. Unit tests (5). The cost of proving on a mining machine: PC 2 job `prover-cost-pc2-pv1` (5 min mining alone, 5 min with the prover, the GPU memory peak and the host RAM peak, the sp1-gpu-server's compiled SM targets) | Implemented; the measurement is HELD (coordinator, 19:00Z): PC 2's RTX 5090 worker has been exiting on a pack seed mismatch since 18:35Z, so the first run's "mining alone" is 0 MH/s and void; re-run after the go |
| 2 | Segment aggregation: `SegmentRecord` (586 bytes, the aggregator guest's 340-byte statement inline), section `IGNS` before the shard section, p2p message 72 at protocol 14, the native block statement and the veto, the credit split (`split_pool_credit`), the payout at the carrier, `igneum-miner sign-segment-record`, the RPCs; host modes `chain` (consecutive fixtures), `aggregate` (live shard proofs from the pool, a run of blocks in one process) and `verify-segment` (the node's verifier, pinned aggregator key); the app's aggregator step (`prover.rs aggregate_once`) | Implemented, unit-tested (consensus core 2 new tests, exec 2, params 1); the GPU measurement (N = 2, 4, 8 blocks on PC 2) is HELD with step 1; the Mac CPU run of `--mode chain` over 2 live blocks is the known-finished case |
| 3 | Coverage: `tools/proving-v1/coverage.mjs` (the proven-block share and the on-chain proof latency over a window from one node's RPC, the live page beside it) | Implemented and run for 3 min (below); the 30-min window waits for the fleet |
| 4 | The chain rule and the unproven rule in consensus behind `proving_v1_activation_daa` (spec 7.8 items 2, 6, 7); unit tests; the fast-time 3-node harness `tools/proving-v1/net.mjs` (ports 29950+, suffix 956, trust mode) with the known-finished and known-failed cases | Implemented; the harness run waits for the Mac build of the fork (`vendor/igneum-node/target-pv1`) |
| 5 | This plan: the rollout for 0.3.11 and the project lead's decisions | Written below |
## Numbers (every one from `docs/bench-log.md`, "proving v1")
(filled as the measurements land; see the bench log entries of 5 October 2026 named "proving v1 ...")
## The rule, in one paragraph (spec 7.8)
From the first chain block `A` at or above `proving_v1_activation_daa`, chain blocks form fixed segments of `N`
(`proving_v1_segment_blocks`). A segment record carries the aggregated proof of the segment's last block, whose
`chain_len` says how many consecutive blocks the recursion attests. Every node checks the record's statement
against its own native block statement (every field but the provers commitment and `chain_len`), the chain rule
(a proof that does not chain to the previous segment, `chain_len = N`, is valid only for the first segment or
after an unproven one) and the deadline (`T = proving_v1_unproven_daa` DAA seconds after the segment's last block;
a record carried later pays nothing). The pool credit of every attested block splits: `proving_v1_aggregator_share_bps`
to the aggregator, the rest to the shards as v0. A block is never invalid for lack of a proof; the mandatory rule
(spec 7.8 item 10) is Designed and off, with no switch yet.
## Rollout for 0.3.11 (the digest handshake pattern of 0.3.9 and tonight's switches)
The switch moves the consensus digest only once it is set (`consensus_digest`: the four v1 fields enter the hash
when `proving_v1_activation_daa != never`), so a 0.3.11 node on the unswitched devnet keeps the 0.3.10 digest and
the rolling upgrade does not partition the network. The order, each step with its check:
1. **Rebase and build.** Fork `proving-v1` rebased onto the 0.3.10 tip on `release-0.3.6`; the six node suites and
the app tests as PC 2 build jobs; the Mac node and the Windows exes by the Mac cross-build; the Linux node by
PC 1; the HiveOS package republished from the same fork commit (rule: the HiveOS package carries the node of
the release commit and the same override object, `infra/hive`, as 0.3.9's `hive-sync-039o` checked it).
2. **Pinned guests.** The guest ids do not change in this round (shard `0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a`,
aggregator `0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896`, pinned 2026-10-05T16:20:38Z):
the aggregator guest already carried the chain rule and only the HOST gained modes. So no provers-off drain is
needed for the guests; `--mode id` on every machine after the update must print the same two ids, and the node
now reads them at start (`program_ids`: `IGNEUM_PROOF_PROGRAM_IDS` or the verifier's `--mode id`) and names them
in the native statement.
3. **Hand nodes and the seed first**, with the UNCHANGED override object (the digest stays): observer, node 1, the
seed on the 0.3.11 node; peers back within 20 s; the `proving v1: segment records from DAA score never` line in
each log.
4. **Manifest and apps.** `publish-manifest.sh --version 0.3.11` with the unchanged object; `update-now` to every
app; every machine on 0.3.11 with a DAA score and a hash rate (the watcher takes the commit as an argument).
The app's prover default applies at the first start on 0.3.11: every NVIDIA machine with WSL2 goes on; the
log line `prover default: ...` on each.
5. **The switch.** When every node runs 0.3.11: publish the object with `proving_v1_activation_daa` = H (24 h
ahead, the rule of `fee-switch-devnet.md`) and the three parameters; read the expected digest on a scratch node
first; `update-now`; the hand nodes and the seed with the same object; the digest sweep; the first paid segment
record (`igneum_getSegmentRecords`) and `igneum_getProvingStatus.v1.segmentsInWindow` after H.
6. **The mandatory rule** stays off: no switch exists for it yet; it gets one when the measured share is one.
## What the project lead must decide
| Decision | Proposed | Why |
|---|---|---|
| `proving_v1_segment_blocks` (N) | 4 | the N = 2, 4, 8 measurement on the 5090 decides; 4 keeps a record every 4 s at 1 block/s and the recursion cost per block constant |
| `proving_v1_unproven_daa` (T) | 600 | equals the 600-block record window of v0: nothing is payable for a segment after it either way; the fleet's measured latency (p99) must sit well inside it |
| `proving_v1_aggregator_share_bps` | 1,000 (a tenth) | the aggregation is one recursion per block, far cheaper than the shards; a tenth pays a second role without starving the shard provers; it is a consensus parameter in the digest |
| `proving_v1_activation_daa` (H) | 24 h after the 0.3.11 publish | the fee-switch rule |
| Aggregator sortition | none in v1 (first valid record wins) | design 5.3's VRF draw is O-7.3; with one or two aggregators on the devnet a draw changes nothing yet |
| Apple silicon default | off | until the GPU prover is measured on a Mac |

View file

@ -36,6 +36,8 @@ Self-dealing: a developer who also mines the including block collects 80% plus 2
Designed. The 20% emission share (section 2.5) and the provers' part of the 80% tip share are paid per block as a fixed amount for that block, divided among the block's shards by consensus proving cost, so a stuffed block earns no more than an honest one. Shards are not claimed first-come and carry no bond: each shard is assigned by sortition to 8 eligible provers for a 10-s exclusive window, then open to anyone, and the first valid proof included in a block is paid (section 7.2, decided 3 October 2026, ledger P8, C9). The parameters 8 and 10 s are set on the phase 4 devnet (O-5.1). A withheld shard costs nothing to bond against because nothing waits on an assigned prover: an unproven block delays only its proof; execution and the 30-s lock do not wait for it (ledger P9). The bond, slashed on a bad or late proof, remains in the external job market (5.4), where a customer does wait; its size and timeout are Open (O-5.6).
Proving v1 (section 7.8, 5 October 2026, Implemented behind `proving_v1_activation_daa`): from the switch, `proving_v1_aggregator_share_bps` of a block's fixed amount (a tenth, the project lead's decision at 0.3.11) goes to the aggregator whose segment record attests the block, the rest to the shards as before; a block in a segment that stays unproven past `proving_v1_unproven_daa` pays no aggregator share.
## 5.4 External job market
Designed. At launch external proving jobs are paid on the customer's chain, in the customer's currency, to a payout contract keyed by miner address, because Igneum cannot yet see Ethereum; the customer chain's own bond and slashing apply (design document, "The first six months"). When the job market settles on Igneum, which needs the proof bridge (phase 2 consensus proof, ledger P4, E7), every job fee paid in IGN splits:

View file

@ -78,6 +78,13 @@ Nothing in consensus changes for any of this: the segment claim already commits
| Records per block | 8 | Implemented, 7.7 (Designed value) |
| Payout per shard | the segment's pool credit in equal parts, remainder to shard 0, paid by the carrying segment | Implemented, 7.7 |
| Proving v0 activation | `proving_v0_activation_daa`, default never | Implemented, 7.7 |
| Segment record (v1) | per segment of `proving_v1_segment_blocks` chain blocks, 586 bytes (the aggregator guest's 340-byte statement inline), BLS-signed by the aggregator's vote key, in the coinbase extra data before the shard record section (`IGNS`); proof bytes on p2p message 72 (protocol 14) | Implemented, 7.8 (branch `proving-v1`, 5 October 2026) |
| Segment records per block | 2 | Implemented, 7.8 (Designed value) |
| Segment length `N` | `proving_v1_segment_blocks`, 4 | Implemented, value Designed (the project lead decides at 0.3.11) |
| Unproven deadline `T` | `proving_v1_unproven_daa`, 600 DAA s after the segment's last chain block | Implemented, value Designed (the project lead decides at 0.3.11) |
| Aggregator share | `proving_v1_aggregator_share_bps`, 1,000 (a tenth of every attested block's pool credit; the shards share the rest) | Implemented, value Designed (the project lead decides at 0.3.11) |
| Proving v1 activation | `proving_v1_activation_daa`, default never; the segment grid starts at the first chain block at or above it | Implemented, 7.8 |
| Mandatory proofs | the rule of 7.8 item 9, no switch yet, off | Designed |
## 7.5 Proving gas per transaction: the cap and the abort
@ -112,3 +119,20 @@ Added 4 October 2026 because the implementation (`vendor/igneum-node-proving`, b
8. **The plan.** Every segment has at least one shard; an empty segment is one shard whose statement applies the rewards and payouts only. The node cuts from its own per-transaction boundaries (`TxBoundary`: the carry of 7.6, the state root after every transaction) with the cut of `igneum_prove_core::plan`; `igneum-prove-export` must reproduce the node's plan on the same export, and the test network checks that it does.
RPCs (the execution layer's JSON-RPC): `igneum_getShardPlan(block)`, `igneum_getProofRecords(block)`, `igneum_submitProofRecord({record, proof})`, `igneum_getAssignedShards([keyHash...], lookback)` (the prover's work list), `igneum_getProvingStatus()`. Signing without the BLS key material in the prover process: `igneum-miner sign-record` and `key-hash`.
## 7.8 Segment records, the chain rule and the unproven rule, as implemented (proving v1)
Added 5 October 2026 (the project lead: "open the proving round asap"; branch `proving-v1` of the fork and of the main repository, `docs/plans/proving-v1.md`). Every item is Implemented on the branch and behind `proving_v1_activation_daa` (default never); nothing here changes the devnet until the 0.3.11 rollout sets the switch. Item 9 is Designed and off. Proving v0 (7.7) keeps running underneath: per-shard records stay valid and paid.
1. **The aggregated proof.** The aggregator guest of 7.6 (pinned, `elf/igneum-prove-aggregator`) verifies every shard proof of one chain block and, by recursion, the previous chain block's aggregated proof (`AggInput.prev`): its public values (`BlockOutput`, 340 bytes, mirrored in consensus as `BlockStatement`) carry `chain_len`, the number of consecutive chain blocks the proof attests, and `agg_vk`, the aggregator's own id whenever `chain_len > 1`. One compressed proof of constant size therefore attests any run of consecutive chain blocks (measured: `docs/bench-log.md`, "proving v1, aggregated chains on the RTX 5090"). This is design 5.3's "segment N verifies N-1" realised inside the proof rather than beside it.
2. **Segments.** From the first chain block `A` whose DAA score reaches `proving_v1_activation_daa`, chain blocks are grouped in fixed segments of `N = proving_v1_segment_blocks`: segment `k` is `A + kN ..= A + kN + N - 1`. The grid is a pure function of the chain, so every node names the same segments.
3. **The record.** One `SegmentRecord` (`kaspa_consensus_core::proving`): version 2, `first`, `last`, the hash of chain block `last`, the aggregator's BLS vote key, the payout address, the aggregated proof's 340 public values inline, the SHA-256 of the proof bytes, and a BLS signature over all of it under `IGNEUM_SEGMENT_RECORD_V1`, domain-separated with the network name. 586 bytes. The statement the verifier checks is keccak256 of the public values.
4. **Carriage.** Records ride in the coinbase extra data as a section `records || len_le32 || "IGNS"` placed before the shard record section (`IGNP`), which sits before the finality section; at most 2 per block. A node that does not read the section sees miner bytes. The proof bytes travel beside the record on p2p message `IgneumSegmentRecordMessage` (type 72, protocol version 14; peers at 13 never receive it) and through `igneum_submitSegmentRecord`.
5. **What every node checks on a carried record (consensus).** The segment is aligned on the grid and its last block is on the executor's own chain, at most 600 chain blocks behind the carrier; the signature verifies; the public values equal the node's native block statement for chain block `last` in every field but `provers` and `chain_len` (`chain_id`, `number`, `block_hash`, `parent_hash`, `shard_count`, `tx_commitment`, `pre_root`, `post_root`, the keccak of the shard receipts roots, gas, pgas, executed, skipped, `shard_vk` = the pinned shard program id, `agg_vk` = the pinned aggregator id when `chain_len > 1` and zero otherwise); `chain_len` is at least `N` and at most the chain height; the chain rule of item 6; and the deadline of item 7. A record that fails is ignored, not a fault of the block: it pays nothing. The SP1 proof is not verified on this path (item 8).
6. **The chain rule.** A record whose `chain_len > N` chains to the previous segment's proof by recursion (the proof itself verified it), and is valid whatever the chain says about that segment. A record whose `chain_len = N` starts a fresh chain and is valid only for the first segment of the grid, or when the previous segment is unproven (item 7). So a proven segment is followed only by records that verify it; an unproven one may be skipped.
7. **The unproven rule.** A segment whose last chain block is more than `T = proving_v1_unproven_daa` DAA seconds old at the carrier, with no paid record, is unproven: the pool pays nothing for it (its aggregator share stays in the escrow), a record for it carried after the deadline is invalid, and the next segment may start a fresh chain. A segment with a paid record is proven; before its deadline it is pending. `igneum_getProvingStatus` reports the three counts over the record window.
8. **Verification is off the consensus path**, as 7.7 item 4: the proof pool verifies segment proofs through `igneum-prove-host --mode verify-segment` (SP1's light verifier against the pinned aggregator key; the shard id and the aggregator id inside the statement checked against the pinned ids; keccak of the public values against the statement) and a producer offers only verified records to its templates. The native statement bounds what an unverified record can do to the aggregator's payout, never to state.
9. **Payout.** From the activation, a chain block's pool credit (5.3) splits: `proving_v1_aggregator_share_bps` of it to the aggregator of the segment record that attests the block, the rest to its shards in equal parts as 7.7 item 6 (`split_pool_credit`). At the carrying chain block, for every segment record its blocks carry in sequence order, the first valid record per segment pays the sum of the aggregator shares of the segment's blocks to the record's payout address, from the escrow, after the shard payouts and before the transactions; a reorg unwinds it with the carrier. There is no aggregator sortition yet: the first valid record carried wins (Open, O-7.3: the VRF draw of design 5.3).
10. **Mandatory proofs (Designed, off).** The rule that makes a proof a condition of validity: a chain block is invalid if the segment ending `T` DAA seconds before it is unproven. It needs an activation height of its own (not yet a parameter) and a consensus-level check in the block validator; it is written here so the devnet measures the coverage the fleet can meet first (`docs/plans/proving-v1.md`, step 3) and the project lead sets the height when the share is one.
RPCs: `igneum_submitSegmentRecord({record, proof})`, `igneum_getSegmentStatement(block)` (the segment, its status, the native public values for a fresh and a continuing chain, the shard proofs the pool holds per block, the previous paid record), `igneum_getSegmentRecords(block)`, `igneum_getProofBytes(block, shard, keyHash)` and `igneum_getSegmentProofBytes(first, keyHash)` (the aggregator's inputs), the `v1` object of `igneum_getProvingStatus`. Signing: `igneum-miner sign-segment-record`. Host modes: `chain`, `aggregate`, `verify-segment`.

View file

@ -55,5 +55,9 @@
"proving_v0_activation_daa": 18446744073709551615,
"finality_v3_activation_daa": 18446744073709551615,
"fees_v1_activation_daa": 0,
"proving_v1_activation_daa": 18446744073709551615,
"proving_v1_segment_blocks": 4,
"proving_v1_unproven_daa": 60,
"proving_v1_aggregator_share_bps": 1000,
"fees": {"pgas": {"version": 1, "cycles_per_pgas": 1000, "intrinsic_pgas_per_tx": 300, "modexp_base": 10, "modexp_per_byte_numer": 1, "modexp_per_byte_denom": 10}, "block_proving_gas_limit": 120000, "shard_proving_gas_budget": 30000, "min_execution_base_fee_wei": 100000000000, "min_proving_base_fee_wei": 10000000000000, "initial_execution_base_fee_wei": 100000000000, "initial_proving_base_fee_wei": 10000000000000, "base_fee_change_denominator": 8}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -78,10 +78,25 @@ fn run() -> Result<()> {
// proving v0 (spec 7.7): the node's proof pool verifies a submitted shard proof off the consensus path
return run_verify(&pinned, &arg("--proof").context("--proof <file>")?, &arg("--statement").context("--statement 0x<keccak of the public values>")?);
}
let path = args.get(1).filter(|a| !a.starts_with("--")).context("usage: igneum-prove-host <fixture.json> [--mode native|execute|shard|compressed|block|all] [--shard N] [--prover 0x..] [--out results.json]; igneum-prove-host --mode verify --proof <file> --statement 0x..; igneum-prove-host --mode id")?;
let shard_index: usize = arg("--shard").map(|s| s.parse()).transpose()?.unwrap_or(0);
if mode == "verify-segment" {
// proving v1 (spec 7.8): the node's pool verifies an aggregated segment proof against the pinned aggregator key
return run_verify_segment(&pinned, &arg("--proof").context("--proof <file>")?, &arg("--statement").context("--statement 0x<keccak of the block public values>")?);
}
let prover: Address = arg("--prover").map(|s| s.parse()).transpose()?.unwrap_or_else(|| Address::from_slice(&[0x19; 20]));
let out_path = arg("--out");
if mode == "aggregate" {
// proving v1: the live aggregator, from shard proof files (the node's pool) and the previous segment proof
return run_aggregate(&pinned, &arg("--proofs").context("--proofs <a.bin,b.bin,...> (the segment's shard proofs in shard order)")?, &arg("--parent").context("--parent 0x<parent chain block hash>")?, arg("--prev").as_deref(), out_path.as_deref());
}
if mode == "chain" {
// proving v1: N consecutive fixtures proven shard by shard, each block aggregated with the previous block's
// proof (the chain rule of design 5.3), the measurement of docs/plans/proving-v1.md step 2
let list = arg("--chain").or_else(|| args.get(1).filter(|a| !a.starts_with("--")).cloned()).context("--chain <f1.json,f2.json,...> (consecutive fixtures)")?;
let fixtures: Vec<String> = list.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
return run_chain(&pinned, &fixtures, prover, out_path.as_deref());
}
let path = args.get(1).filter(|a| !a.starts_with("--")).context("usage: igneum-prove-host <fixture.json> [--mode native|execute|shard|compressed|block|all] [--shard N] [--prover 0x..] [--out results.json]; --mode chain --chain <f1,f2,...> [--prover 0x..] [--out results.json]; --mode aggregate --proofs <a.bin,...> --parent 0x.. [--prev prev.bin] [--out results.json]; --mode verify --proof <file> --statement 0x..; --mode verify-segment --proof <file> --statement 0x..; --mode id")?;
let shard_index: usize = arg("--shard").map(|s| s.parse()).transpose()?.unwrap_or(0);
let fixture: Fixture = serde_json::from_str(&std::fs::read_to_string(path).with_context(|| format!("read {path}"))?)?;
if fixture.format != igneum_prove_core::fixture::FORMAT {
bail!("fixture format {} is not {} (regenerate with igneum-prove-export)", fixture.format, igneum_prove_core::fixture::FORMAT);
@ -531,6 +546,314 @@ fn run_block(sp1: &Sp1ProofSystem, shards: &[BuiltShard], claim: &SegmentClaim,
Ok(())
}
/// Loads a fixture with the checks `run` makes (format, the plan's budget against the fee set at its DAA score).
fn load_fixture(path: &str) -> Result<Fixture> {
let fixture: Fixture = serde_json::from_str(&std::fs::read_to_string(path).with_context(|| format!("read {path}"))?)?;
if fixture.format != igneum_prove_core::fixture::FORMAT {
bail!("fixture format {} is not {} (regenerate with igneum-prove-export)", fixture.format, igneum_prove_core::fixture::FORMAT);
}
let fee_set = fixture.block.fees.at(fixture.block.env.daa_score);
if fixture.plan.consensus && fixture.plan.shard_budget != fee_set.shard_proving_gas_budget {
bail!("{path}: the fixture's plan says consensus budget {} but the fee set at DAA score {} ({}) has S_p {}; regenerate with igneum-prove-export", fixture.plan.shard_budget, fixture.block.env.daa_score, fee_set.name(), fee_set.shard_proving_gas_budget);
}
Ok(fixture)
}
fn setup_sp1(pinned: &pinned::Pinned, results: &mut serde_json::Map<String, serde_json::Value>) -> Result<Sp1ProofSystem> {
stage("setup");
let t = Instant::now();
let sp1 = Sp1ProofSystem::from_env(pinned.shard_elf(), pinned.agg_elf())?;
let setup_s = t.elapsed().as_secs_f64();
println!("RESULT setup: {:.2} s, ProofSystem v{} shard program id {} aggregator id {} at {}", setup_s, Sp1ProofSystem::VERSION, sp1.program_id(), sp1.aggregator_id(), now());
if sp1.program_id() != pinned.shard_id || sp1.aggregator_id() != pinned.agg_id {
bail!("SP1's key setup derived shard program id {} and aggregator id {} from the embedded guests, the pinned manifest says {} and {}: this host would make proofs no other node accepts (re-pin with proving/igneum-prove/pin-guests.sh)", sp1.program_id(), sp1.aggregator_id(), pinned.shard_id, pinned.agg_id);
}
results.insert("setup_seconds".into(), setup_s.into());
results.insert("shard_program_id".into(), sp1.program_id().to_string().into());
results.insert("aggregator_id".into(), sp1.aggregator_id().to_string().into());
results.insert("prover".into(), std::env::var("SP1_PROVER").unwrap_or_else(|_| "cpu".into()).into());
Ok(sp1)
}
/// What a segment record carries about an aggregated proof (spec 7.8): the public values, their keccak (the
/// statement), the proof's SHA-256 and where the proof bytes went.
fn segment_results(seg: &proof_system::Sp1SegmentProof, out_dir: &std::path::Path, results: &mut serde_json::Map<String, serde_json::Value>) -> Result<(B256, usize)> {
let bytes = bincode::serialize(&seg.proof)?;
let pv = seg.proof.public_values.as_slice().to_vec();
let statement = alloy_primitives::keccak256(&pv);
let proof_hash: [u8; 32] = sha2::Sha256::digest(&bytes).into();
let file = out_dir.join(format!("segment-{}-aggregated.bin", seg.output.number));
std::fs::write(&file, &bytes)?;
results.insert("segment_number".into(), seg.output.number.into());
results.insert("segment_block_hash".into(), seg.output.block_hash.to_string().into());
results.insert("segment_chain_len".into(), seg.output.chain_len.into());
results.insert("segment_public_values".into(), format!("0x{}", hex::encode(&pv)).into());
results.insert("segment_statement".into(), statement.to_string().into());
results.insert("segment_proof_sha256".into(), format!("0x{}", hex::encode(proof_hash)).into());
results.insert("segment_proof_bytes".into(), bytes.len().into());
results.insert("segment_proof_file".into(), file.display().to_string().into());
results.insert("segment_provers".into(), seg.output.provers.to_string().into());
println!("segment proof written to {} ({} bytes); statement {statement} proof sha256 0x{}", file.display(), bytes.len(), hex::encode(proof_hash));
Ok((statement, bytes.len()))
}
fn out_dir_of(out_path: Option<&str>) -> std::path::PathBuf {
out_path.and_then(|p| std::path::Path::new(p).parent().map(|d| d.to_path_buf())).filter(|d| !d.as_os_str().is_empty()).unwrap_or_else(|| std::path::PathBuf::from("."))
}
/// `--mode chain`: every fixture in order, consecutive on the chain (number and parent hash), each block's shards
/// proven compressed and aggregated with the previous block's aggregated proof (`AggInput.prev`, the chain rule),
/// every proof verified. One RESULT line per shard, per block (with the running totals) and for the chain.
fn run_chain(pinned: &pinned::Pinned, fixtures: &[String], prover: Address, out_path: Option<&str>) -> Result<()> {
if fixtures.is_empty() {
bail!("--chain needs at least one fixture");
}
let mut results = serde_json::Map::new();
results.insert("mode".into(), "chain".into());
results.insert("fixtures".into(), fixtures.iter().map(|f| serde_json::Value::from(f.as_str())).collect::<Vec<_>>().into());
let mut loaded = Vec::with_capacity(fixtures.len());
for path in fixtures {
let f = load_fixture(path)?;
if let Some(prev) = loaded.last().map(|(_, f): &(String, Fixture)| &f.block.env) {
if f.block.env.number != prev.number + 1 || f.block.env.parent_hash != prev.hash {
bail!("{path}: block {} (parent {}) does not follow block {} ({}); the chain needs consecutive fixtures", f.block.env.number, f.block.env.parent_hash, prev.number, prev.hash);
}
}
loaded.push((path.clone(), f));
}
let first = loaded[0].1.block.env.number;
let last = loaded[loaded.len() - 1].1.block.env.number;
println!("igneum-prove-host sources {}: chain of {} consecutive blocks {first}..={last}, prover payout {prover}, SP1_PROVER={}; {}", env!("IGNEUM_PROVE_SOURCES"), loaded.len(), std::env::var("SP1_PROVER").unwrap_or_else(|_| "cpu".into()), now());
// native first: every block's cut and every shard statement must reproduce the fixture before a proof is made
stage("native");
let mut built: Vec<(u64, B256, Vec<BuiltShard>, B256)> = Vec::with_capacity(loaded.len());
for (path, f) in &loaded {
let (outcome, pre_root, shards) = build_shards(&f.block, f.plan.shard_budget, prover);
let e = &f.expected;
if pre_root != e.pre_state_root || outcome.state_root != e.post_state_root || outcome.receipts_root != e.receipts_root || outcome.gas_used != e.gas_used || outcome.pgas_used != e.pgas_used || shards.len() != f.plan.shards.len() {
bail!("{path}: native execution differs from the fixture; regenerate it with igneum-prove-export");
}
if let Some((_, _, prev_shards, _)) = built.last() {
let prev_post = prev_shards.last().map(|s| s.output.post_root).unwrap_or_default();
if pre_root != prev_post {
bail!("{path}: block {} starts from state root {pre_root}, the previous block ended at {prev_post}: the state does not chain", f.block.env.number);
}
}
println!("RESULT chain native block {}: {} shard(s), pgas {}, gas {}, pre {} post {}", f.block.env.number, shards.len(), outcome.pgas_used, outcome.gas_used, pre_root, outcome.state_root);
built.push((f.block.env.number, f.block.env.hash, shards, pre_root));
}
let sp1 = setup_sp1(pinned, &mut results)?;
let chain_t = Instant::now();
let mut prev: Option<proof_system::Sp1SegmentProof> = None;
let mut blocks_json = Vec::with_capacity(built.len());
let (mut shard_total, mut agg_total, mut shards_total) = (0.0f64, 0.0f64, 0usize);
let out_dir = out_dir_of(out_path);
for (number, _hash, shards, _) in &built {
let block_t = Instant::now();
let mut proofs: Vec<Sp1ShardProof> = Vec::with_capacity(shards.len());
let mut shard_secs = Vec::new();
for s in shards {
let i = s.output.shard_index;
stage(&format!("chain block {number} compressed shard {i}"));
let p = sp1.prove_shard(&ShardWitness { input: s.input.clone() })?;
let dt = sp1.last_timing("compressed").unwrap_or_default().as_secs_f64();
let (ok, vdt) = sp1.verify_shard(&p.proof, &s.output);
println!("RESULT chain block {number} shard {i}: compressed prove {dt:.1} s, proof {} bytes, verify {:.3} s, {}, pgas {} at {}", bincode::serialize(&p.proof)?.len(), vdt.as_secs_f64(), if ok { "VERIFIED" } else { "VERIFY FAILED" }, p.output.pgas_used, now());
if !ok {
bail!("compressed proof of block {number} shard {i} did not verify");
}
shard_secs.push(dt);
shard_total += dt;
proofs.push(p);
}
shards_total += proofs.len();
stage(&format!("chain block {number} aggregate {} shards{}", proofs.len(), if prev.is_some() { " with the previous block proof" } else { "" }));
let seg = sp1.aggregate(prev.as_ref(), &proofs)?;
let adt = sp1.last_timing("aggregate").unwrap_or_default().as_secs_f64();
agg_total += adt;
let claim = SegmentClaim::from_block(&seg.output);
let ok = sp1.verify_segment(&seg, &claim);
let vdt = sp1.last_timing("verify-block").unwrap_or_default().as_secs_f64();
let bytes = bincode::serialize(&seg.proof)?.len();
let block_s = block_t.elapsed().as_secs_f64();
let cumulative = chain_t.elapsed().as_secs_f64();
println!(
"RESULT chain block {number}: {} shards ({:.1} s of shard proofs), aggregate prove {adt:.1} s, proof {bytes} bytes, verify {vdt:.3} s, {}; chain_len {}, agg_vk {}; this block {block_s:.1} s, cumulative {cumulative:.1} s over {} block(s) at {}",
seg.output.shard_count,
shard_secs.iter().sum::<f64>(),
if ok { "VERIFIED (shard program id, aggregator id and claim checked)" } else { "VERIFY FAILED" },
seg.output.chain_len,
seg.output.agg_vk,
blocks_json.len() + 1,
now()
);
if !ok {
bail!("the aggregated proof of block {number} did not verify");
}
let expected_len = blocks_json.len() as u64 + 1;
if seg.output.chain_len != expected_len {
bail!("block {number}: chain_len {} is not {expected_len}", seg.output.chain_len);
}
blocks_json.push(serde_json::json!({
"number": number, "shards": seg.output.shard_count, "shard_prove_seconds": shard_secs, "aggregate_prove_seconds": adt,
"aggregate_verify_seconds": vdt, "proof_bytes": bytes, "chain_len": seg.output.chain_len, "block_seconds": block_s, "cumulative_seconds": cumulative,
"post_root": seg.output.post_root.to_string(), "statement": alloy_primitives::keccak256(seg.output.to_bytes()).to_string(),
}));
prev = Some(seg);
}
let seg = prev.unwrap();
let total = chain_t.elapsed().as_secs_f64();
let (statement, bytes) = segment_results(&seg, &out_dir, &mut results)?;
println!(
"RESULT chain: {} blocks {first}..={last}, {shards_total} shards, shard proofs {shard_total:.1} s, aggregation {agg_total:.1} s, end to end {total:.1} s; final proof {bytes} bytes attests chain_len {} (statement {statement}), pre {} post {} provers {} at {}",
built.len(),
seg.output.chain_len,
built[0].3,
seg.output.post_root,
seg.output.provers,
now()
);
results.insert("blocks".into(), blocks_json.into());
results.insert("first".into(), first.into());
results.insert("last".into(), last.into());
results.insert("shards".into(), shards_total.into());
results.insert("shard_prove_seconds_total".into(), shard_total.into());
results.insert("aggregate_prove_seconds_total".into(), agg_total.into());
results.insert("chain_seconds".into(), total.into());
drop(sp1);
finish(results, out_path.map(|s| s.to_string()))
}
/// `--mode aggregate`: the live aggregator (the app's segment step, spec 7.8). `--proofs` names the shard proof
/// files of one or more consecutive chain blocks: blocks separated by `;`, a block's shards by `,` (what the node's
/// pool holds, `igneum_getProofBytes`); `--parent` is the first block's parent chain block hash; `--prev` the
/// previous segment's aggregated proof when the chain continues. Each block is aggregated with the previous
/// block's proof in one process (one key setup); the output is the last block's aggregated proof, its public
/// values, the statement and the proof hash the segment record carries.
fn run_aggregate(pinned: &pinned::Pinned, proofs: &str, parent: &str, prev_path: Option<&str>, out_path: Option<&str>) -> Result<()> {
let first_parent: B256 = parent.parse().context("--parent is not 32 bytes of hex")?;
let mut results = serde_json::Map::new();
results.insert("mode".into(), "aggregate".into());
let mut blocks: Vec<Vec<Sp1ShardProof>> = Vec::new();
let mut parent_hash = first_parent;
for group in proofs.split(';').map(str::trim).filter(|s| !s.is_empty()) {
let mut shards: Vec<Sp1ShardProof> = Vec::new();
for path in group.split(',').map(str::trim).filter(|s| !s.is_empty()) {
let bytes = std::fs::read(path).with_context(|| format!("read {path}"))?;
let proof: sp1_sdk::SP1ProofWithPublicValues = bincode::deserialize(&bytes).with_context(|| format!("{path} is not a bincode SP1 proof"))?;
let output = ShardOutput::from_bytes(proof.public_values.as_slice()).with_context(|| format!("{path}: public values are not a shard statement"))?;
if let Some(c) = pinned::claimed_program_id(&proof) {
if c != pinned.shard_id {
bail!("{path}: shard proof made with program id {c}, ours is {}", pinned.shard_id);
}
}
shards.push(Sp1ShardProof { proof, output, parent_hash });
}
if shards.is_empty() {
bail!("a block in --proofs names no file");
}
shards.sort_by_key(|s| s.output.shard_index);
if let Some(prev) = blocks.last().and_then(|b: &Vec<Sp1ShardProof>| b.first()) {
if shards[0].output.number != prev.output.number + 1 {
bail!("block {} does not follow block {}: the blocks of --proofs must be consecutive", shards[0].output.number, prev.output.number);
}
}
parent_hash = shards[0].output.block_hash;
blocks.push(shards);
}
if blocks.is_empty() {
bail!("--proofs names no file");
}
let mut prev = match prev_path {
None => None,
Some(p) => {
let bytes = std::fs::read(p).with_context(|| format!("read {p}"))?;
let proof: sp1_sdk::SP1ProofWithPublicValues = bincode::deserialize(&bytes).with_context(|| format!("{p} is not a bincode SP1 proof"))?;
let output = BlockOutput::from_bytes(proof.public_values.as_slice()).with_context(|| format!("{p}: public values are not a block statement"))?;
Some(proof_system::Sp1SegmentProof { proof, output })
}
};
let first = blocks[0][0].output.number;
let last = blocks[blocks.len() - 1][0].output.number;
println!("igneum-prove-host sources {}: aggregate blocks {first}..={last} ({} shard proofs){}; {}", env!("IGNEUM_PROVE_SOURCES"), blocks.iter().map(|b| b.len()).sum::<usize>(), prev.as_ref().map(|p| format!(", chaining to block {} (chain_len {})", p.output.number, p.output.chain_len)).unwrap_or_default(), now());
let sp1 = setup_sp1(pinned, &mut results)?;
let t_all = Instant::now();
let mut per_block = Vec::new();
for shards in &blocks {
let number = shards[0].output.number;
stage(&format!("aggregate block {number}, {} shards", shards.len()));
let seg = sp1.aggregate(prev.as_ref(), shards)?;
let adt = sp1.last_timing("aggregate").unwrap_or_default().as_secs_f64();
let claim = SegmentClaim::from_block(&seg.output);
let ok = sp1.verify_segment(&seg, &claim);
let vdt = sp1.last_timing("verify-block").unwrap_or_default().as_secs_f64();
println!("RESULT aggregate block {number}: {} shards, prove {adt:.1} s, proof {} bytes, verify {vdt:.3} s, {}; chain_len {}, post {} at {}", seg.output.shard_count, bincode::serialize(&seg.proof)?.len(), if ok { "VERIFIED" } else { "VERIFY FAILED" }, seg.output.chain_len, seg.output.post_root, now());
if !ok {
bail!("the aggregated proof of block {number} did not verify");
}
per_block.push(serde_json::json!({ "number": number, "shards": seg.output.shard_count, "aggregate_prove_seconds": adt, "aggregate_verify_seconds": vdt, "chain_len": seg.output.chain_len }));
prev = Some(seg);
}
let seg = prev.unwrap();
let (statement, bytes) = segment_results(&seg, &out_dir_of(out_path), &mut results)?;
println!("RESULT aggregate: blocks {first}..={last} in {:.1} s, final proof {bytes} bytes, chain_len {}, statement {statement} at {}", t_all.elapsed().as_secs_f64(), seg.output.chain_len, now());
results.insert("blocks".into(), per_block.into());
results.insert("first".into(), first.into());
results.insert("last".into(), last.into());
results.insert("aggregate_seconds".into(), t_all.elapsed().as_secs_f64().into());
drop(sp1);
finish(results, out_path.map(|s| s.to_string()))
}
/// `--mode verify-segment --proof <file> --statement 0x..`: the node's verifier for an aggregated segment record
/// (spec 7.8): SP1's light verifier against the PINNED aggregator key, the public values' keccak against the
/// statement, the shard program id inside the statement against ours, the aggregator id inside it against ours
/// (or zero when the proof chains to nothing). Exit 0 = verified, 3 = not verified.
fn run_verify_segment(pinned: &pinned::Pinned, proof_path: &str, statement: &str) -> Result<()> {
use sp1_sdk::blocking::{LightProver, Prover};
let bytes = std::fs::read(proof_path).with_context(|| format!("read {proof_path}"))?;
let want: B256 = statement.parse().context("statement is not 32 bytes of hex")?;
stage("setup");
let t = Instant::now();
let verifier = LightProver::new();
println!("RESULT setup: {:.3} s (light verifier, pinned aggregator key), aggregator id {} shard program id {} at {}", t.elapsed().as_secs_f64(), pinned.agg_id, pinned.shard_id, now());
stage("verify-segment");
let t = Instant::now();
let proof: sp1_sdk::SP1ProofWithPublicValues = bincode::deserialize(&bytes).context("the file is not a bincode SP1 proof")?;
let got = alloy_primitives::keccak256(proof.public_values.as_slice());
let output = BlockOutput::from_bytes(proof.public_values.as_slice());
let claimed = pinned::claimed_program_id(&proof);
let same_program = claimed == Some(pinned.agg_id);
let crypto_ok = same_program && verifier.verify(&proof, &pinned.agg_vk, None).is_ok();
let ids_ok = output.as_ref().map(|o| o.shard_vk == pinned.shard_id && (o.agg_vk == pinned.agg_id || (o.chain_len == 1 && o.agg_vk == B256::ZERO))).unwrap_or(false);
let ok = crypto_ok && ids_ok && got == want;
let dt = t.elapsed().as_secs_f64();
let program = match claimed {
Some(c) if same_program => format!("aggregator id {c} (ours)"),
Some(c) => format!("aggregator id {c} IS NOT OURS {} (the aggregator runs another guest build)", pinned.agg_id),
None => "not a compressed proof".to_string(),
};
match &output {
Some(o) => println!(
"RESULT verify-segment: {} in {dt:.3} s; block {} ({}) chain_len {} shards {} statement {got} (want {want}) {program}; inner ids {}; proof {} bytes at {}",
if ok { "VERIFIED" } else { "NOT VERIFIED" },
o.number,
o.block_hash,
o.chain_len,
o.shard_count,
if ids_ok { "ours".to_string() } else { format!("NOT OURS (shard {} agg {} chain_len {})", o.shard_vk, o.agg_vk, o.chain_len) },
bytes.len(),
now()
),
None => println!("RESULT verify-segment: NOT VERIFIED in {dt:.3} s; public values are not a block statement; {program} at {}", now()),
}
if ok {
Ok(())
} else {
std::process::exit(3)
}
}
fn check_shard_output(out: &ShardOutput, native: &ShardOutput) -> Result<()> {
if out != native {
bail!("the guest's public values differ from the native run:\n guest {out:?}\n native {native:?}");

View file

@ -74,8 +74,7 @@ pub fn program_id_of(vk: &SP1VerifyingKey) -> B256 {
pub struct Pinned {
pub manifest: Manifest,
pub shard_vk: SP1VerifyingKey,
/// For the aggregated record's verifier (proving v0 next step); checked against the manifest today.
#[allow(dead_code)]
/// The aggregated segment record's verifier (`--mode verify-segment`, proving v1).
pub agg_vk: SP1VerifyingKey,
pub shard_id: B256,
pub agg_id: B256,

View file

@ -0,0 +1 @@
/Users/joshm/Projects/igneum/tools/prove-fixtures/node_modules

View file

@ -0,0 +1,89 @@
#!/usr/bin/env node
// Proving v1 step 3: the live devnet's proven-block share and the proof latency distribution over a window, read
// from one node's execution-layer RPC (what the chain carried: every node agrees on it) and, beside it, the live
// page's 10-minute proving object. Read-only: no job, no switch.
//
// node tools/proving-v1/coverage.mjs [--rpc http://127.0.0.1:26790] [--minutes 30] [--live https://igneum.network/api/live]
// [--out <json>] [--watch]
//
// Without --watch: one pass over the last --minutes of chain blocks (by block timestamp) at the tip: per block the
// shard plan (shards), the paid rows (carrierNumber) and the carrier's timestamp; prints the table for the bench log.
// With --watch: samples the live page every 60 s for --minutes, then the pass.
//
// Latency here is on-chain: the carrying chain block's timestamp minus the proven block's timestamp (the record was
// verified by its producer before that, so this bounds "block time to verified record" from above).
import { writeFileSync } from 'node:fs';
const args = process.argv.slice(2);
const opt = (n, d) => { const i = args.indexOf(n); return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : d; };
const RPC = opt('--rpc', 'http://127.0.0.1:26790');
const LIVE = opt('--live', 'https://igneum.network/api/live');
const MINUTES = +opt('--minutes', 30);
const OUT = opt('--out', null);
const WATCH = args.includes('--watch');
const log = (...a) => console.log(new Date().toISOString().slice(11, 19), ...a);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let id = 0;
async function rpc(method, params = []) {
const r = await fetch(RPC, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++id, method, params }), signal: AbortSignal.timeout(30000) });
const j = await r.json(); if (j.error) throw new Error(j.error.message); return j.result;
}
const hexn = (v) => (v == null ? null : Number(BigInt(v)));
const pct = (a, b) => (b ? (100 * a / b).toFixed(1) + '%' : 'n/a');
function quantiles(xs) {
if (!xs.length) return { n: 0 };
const s = [...xs].sort((a, b) => a - b);
const q = (p) => s[Math.min(s.length - 1, Math.floor(p * (s.length - 1)))];
return { n: s.length, min: s[0], p50: q(0.5), p90: q(0.9), p99: q(0.99), max: s[s.length - 1], mean: +(s.reduce((a, b) => a + b, 0) / s.length).toFixed(1) };
}
async function pass() {
const tip = hexn(await rpc('eth_blockNumber'));
const tipBlock = await rpc('eth_getBlockByNumber', ['0x' + tip.toString(16), false]);
const tipTs = hexn(tipBlock.timestamp);
const from = tipTs - MINUTES * 60;
// walk back by timestamp
const ts = new Map();
const tsOf = async (n) => { if (!ts.has(n)) { const b = await rpc('eth_getBlockByNumber', ['0x' + n.toString(16), false]); ts.set(n, hexn(b.timestamp)); } return ts.get(n); };
let first = tip;
while (first > 1 && (await tsOf(first - 1)) >= from) first--;
log(`window: chain blocks ${first}..${tip} (${tip - first + 1} blocks, ${MINUTES} min by block timestamp), tip DAA via status`);
const status = await rpc('igneum_getProvingStatus');
const rows = [];
for (let n = first; n <= tip; n++) {
const r = await rpc('igneum_getProofRecords', ['0x' + n.toString(16)]);
const plan = await rpc('igneum_getShardPlan', ['0x' + n.toString(16)]).catch(() => null);
const shards = plan ? plan.shards.length : 0;
const paid = (r.paid || []).filter(Boolean);
const bt = await tsOf(n);
const lat = [];
for (const p of paid) { const c = hexn(p.carrierNumber); if (c != null) lat.push((await tsOf(c)) - bt); }
rows.push({ n, shards, pgas: plan ? plan.shards.reduce((a, s) => a + hexn(s.pgas), 0) : 0, paid: paid.length, latencies: lat, carried: (r.carried || []).length });
}
const blocks = rows.length;
const any = rows.filter((x) => x.paid > 0).length;
const full = rows.filter((x) => x.shards > 0 && x.paid === x.shards).length;
const shardsTotal = rows.reduce((a, x) => a + x.shards, 0), shardsPaid = rows.reduce((a, x) => a + x.paid, 0);
const lat = quantiles(rows.flatMap((x) => x.latencies));
const content = rows.filter((x) => x.pgas > 0);
const contentPaid = content.filter((x) => x.paid === x.shards).length;
const summary = { rpc: RPC, window: { first, last: tip, blocks, minutes: MINUTES, from_ts: from, to_ts: tipTs }, blocks_with_any_paid_shard: any, blocks_fully_proven: full, share_any: pct(any, blocks), share_full: pct(full, blocks), shards_total: shardsTotal, shards_paid: shardsPaid, share_shards: pct(shardsPaid, shardsTotal), content_blocks: content.length, content_blocks_fully_proven: contentPaid, latency_s: lat, status: { paidShards: status.paidShards, pool: status.pool, verifier: status.verifier, tipDaa: hexn(status.tipDaa), v1: status.v1 || null } };
log(`RESULT coverage: ${blocks} blocks, ${any} with a paid shard (${summary.share_any}), ${full} fully proven (${summary.share_full}); shards ${shardsPaid}/${shardsTotal} (${summary.share_shards}); content blocks ${content.length}, fully proven ${contentPaid}; on-chain latency s: ${JSON.stringify(lat)}`);
return { summary, rows };
}
const samples = [];
if (WATCH) {
const end = Date.now() + MINUTES * 60 * 1000;
while (Date.now() < end) {
const lv = await fetch(LIVE, { signal: AbortSignal.timeout(20000) }).then((r) => r.json()).catch((e) => ({ error: e.message }));
const p = lv.proving || {};
const s = { t: new Date().toISOString(), block_count: lv.state?.block_count, blocks_per_second_60s: lv.state?.blocks_per_second_60s, shards_proven_10m: p.shards_proven_10m, blocks_fully_proven_10m: p.blocks_fully_proven_10m, median_proof_lag_s: p.median_proof_lag_s, provers_10m: p.provers_10m, error: lv.error };
samples.push(s);
log(`live: ${JSON.stringify(s)}`);
await sleep(60000);
}
}
const result = await pass();
result.live_samples = samples;
if (OUT) { writeFileSync(OUT, JSON.stringify(result, null, 2)); log(`written ${OUT}`); }

246
tools/proving-v1/net.mjs Normal file
View file

@ -0,0 +1,246 @@
#!/usr/bin/env node
// Proving v1 (spec 7.8, docs/plans/proving-v1.md step 4) on a private 3-node fast-time test network: ports 29950 and
// up, network igneum-devnet-956, data under /tmp/igneum-proving-v1, infra/fast-time's 60x profile with
// skip_proof_of_work, proving v0 at DAA 60 and proving v1 at DAA 120 (4 blocks a segment, unproven after 60 DAA, a
// tenth of the pool credit to the aggregator). Every node runs in trust mode (IGNEUM_PROOF_VERIFY=trust): the rule is
// what is under test, not SP1, so the proof bytes are placeholders and the statement is the node's own native block
// statement (igneum_getSegmentStatement), signed with igneum-miner sign-segment-record.
//
// Cases, each timed and asserted (the CLAUDE.md rule: a gate is trusted only after one known-finished and one
// known-failed case):
// 1. known-finished: the first v1 segment's record (a fresh chain, chain_len 4) relays, verifies, is carried and
// pays the aggregator's share (4 x 10% of the credits) to the payout address on every node
// 2. the chain rule: the second segment refuses a fresh-chain record while the first is proven, and accepts the
// continuing one (chain_len 8)
// 3. known-failed: the third segment gets no record; after its deadline it is unproven, a late record for it is
// refused, and the fourth segment's fresh-chain record is accepted and paid (the chain restarts)
// 4. the v0 side after the switch: a shard's shardWei is 90% of its block's share
// Never touches the live devnet (26610/26611, 26640/28640), the proving-v0 harness (29800+) or the C4 runs (29900+).
//
// node tools/proving-v1/net.mjs [--secs 1200] [--v1 120] [--segment 4] [--unproven 60]
// IGNEUM_PV1_BIN=<dir with igneumd and igneum-miner> (default vendor/igneum-node/target-pv1/release)
import { spawn, spawnSync } from 'node:child_process';
import { mkdirSync, rmSync, writeFileSync, readFileSync, openSync, existsSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { connectRpc } from '../finality-attacks/lib/rpc.mjs';
import { privateKeyToAccount } from '../prove-fixtures/node_modules/viem/_esm/accounts/index.js';
const ROOT = new URL('../../', import.meta.url).pathname;
const FILE = `${ROOT}infra/fast-time/override-60x.json`;
const REL = process.env.IGNEUM_PV1_BIN || `${ROOT}vendor/igneum-node/target-pv1/release`;
const IGNEUMD = `${REL}/igneumd`;
const MINER = `${REL}/igneum-miner`;
const TMP = '/tmp/igneum-proving-v1';
const BASE = 29950, SUFFIX = 956, CHAIN_NAME = `igneum-devnet-${SUFFIX}`;
const args = process.argv.slice(2);
const flag = (name, dflt) => { const i = args.indexOf(name); return i >= 0 && args[i + 1] !== undefined ? +args[i + 1] : dflt; };
const SECS = flag('--secs', 1200);
const V0 = 60, V1 = flag('--v1', 120), SEG = flag('--segment', 4), UNPROVEN = flag('--unproven', 60), SHARE_BPS = 1000;
const started = [];
const t0 = Date.now();
const since = () => ((Date.now() - t0) / 1000).toFixed(1);
const log = (...a) => console.log(new Date().toISOString().slice(11, 23), `t=${since()}s`, ...a);
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const hexn = (v) => Number(BigInt(v));
for (const b of [IGNEUMD, MINER]) if (!existsSync(b)) { console.error(`missing ${b}`); process.exit(2); }
const funded = privateKeyToAccount('0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d');
const PAYOUT = '0x4343434343434343434343434343434343434343';
rmSync(TMP, { recursive: true, force: true }); mkdirSync(TMP, { recursive: true });
const override = `${TMP}/override.json`;
let overrideText = readFileSync(FILE, 'utf8')
.replace(/"proving_v0_activation_daa":\s*\d+/, `"proving_v0_activation_daa": ${V0}`)
.replace(/"proving_v1_activation_daa":\s*\d+/, `"proving_v1_activation_daa": ${V1}`)
.replace(/"proving_v1_segment_blocks":\s*\d+/, `"proving_v1_segment_blocks": ${SEG}`)
.replace(/"proving_v1_unproven_daa":\s*\d+/, `"proving_v1_unproven_daa": ${UNPROVEN}`)
.replace(/"proving_v1_aggregator_share_bps":\s*\d+/, `"proving_v1_aggregator_share_bps": ${SHARE_BPS}`)
.replace(/"skip_proof_of_work":\s*(true|false)/, '"skip_proof_of_work": true');
for (const re of [/"skip_proof_of_work": true/, new RegExp(`"proving_v1_activation_daa": ${V1}`), new RegExp(`"proving_v1_segment_blocks": ${SEG}`), new RegExp(`"proving_v1_unproven_daa": ${UNPROVEN}`)]) if (!re.test(overrideText)) throw new Error(`override edit failed: ${re}`);
writeFileSync(override, overrideText);
class Node {
constructor(i, connect = [], env = {}) {
this.i = i; this.grpcPort = BASE + i * 10; this.p2pPort = BASE + i * 10 + 1; this.jsonPort = BASE + i * 10 + 2; this.evmPort = BASE + i * 10 + 3;
this.connect = connect; this.env = env; this.dir = `${TMP}/n${i}`; this.logFile = `${this.dir}/node.log`;
}
get grpc() { return `grpc://127.0.0.1:${this.grpcPort}`; }
get evm() { return `http://127.0.0.1:${this.evmPort}`; }
async start() {
mkdirSync(this.dir, { recursive: true });
const a = ['--devnet', `--devnet-suffix=${SUFFIX}`, '--nodnsseed', '--disable-upnp', '--nologfiles', '--enable-unsynced-mining', '--utxoindex', '--unsaferpc',
`--appdir=${this.dir}`, `--rpclisten=127.0.0.1:${this.grpcPort}`, `--rpclisten-json=127.0.0.1:${this.jsonPort}`, `--evm-rpclisten=127.0.0.1:${this.evmPort}`,
`--listen=127.0.0.1:${this.p2pPort}`, `--override-params-file=${override}`, '--loglevel=info', '--yes'];
if (this.connect.length) a.push(...this.connect.map(c => `--connect=${c}`)); else a.push('--outpeers=0');
const out = openSync(this.logFile, 'a');
this.proc = spawn(IGNEUMD, a, { stdio: ['ignore', out, out], env: { ...process.env, ...this.env } });
started.push(this.proc);
await sleep(800);
this.rpc = await connectRpc(`ws://127.0.0.1:${this.jsonPort}`);
log(`n${this.i} up pid ${this.proc.pid} json ${this.jsonPort} evm ${this.evmPort} p2p ${this.p2pPort} env ${JSON.stringify(this.env)}`);
return this;
}
async eth(method, params = []) {
const r = await fetch(this.evm, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) });
const j = await r.json();
if (j.error) throw new Error(`${method}: ${j.error.message}`);
return j.result;
}
grepLog(re) { try { return readFileSync(this.logFile, 'utf8').split('\n').filter(l => re.test(l)); } catch { return []; } }
}
function miner(argv, name) {
const out = openSync(`${TMP}/${name}.log`, 'a');
const p = spawn(MINER, argv, { stdio: ['ignore', out, out] });
started.push(p);
return p;
}
async function stopAll() {
for (const p of started.reverse()) { try { p.kill('SIGINT'); } catch { } }
await sleep(1500);
for (const p of started) { try { p.kill('SIGKILL'); } catch { } }
}
process.on('SIGINT', async () => { await stopAll(); process.exit(130); });
const report = { v0: V0, v1: V1, segment: SEG, unproven: UNPROVEN, share_bps: SHARE_BPS, steps: {}, checks: [] };
const step = (k, v) => { report.steps[k] = v; log(`STEP ${k}: ${JSON.stringify(v)}`); };
const check = (name, ok, detail) => { report.checks.push({ name, ok, detail }); log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ': ' + JSON.stringify(detail) : ''}`); if (!ok) throw new Error(`check failed: ${name} ${JSON.stringify(detail)}`); };
function run(cmd, argv, env = {}) {
const r = spawnSync(cmd, argv, { encoding: 'utf8', maxBuffer: 1 << 28, env: { ...process.env, ...env } });
return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
}
const sha256 = (buf) => '0x' + createHash('sha256').update(buf).digest('hex');
async function waitDaa(n, want, label) {
let daa = 0;
while (daa < want) { await sleep(1000); const s = await n.eth('igneum_getProvingStatus'); daa = hexn(s.tipDaa); }
log(`${label}: daa ${daa}`);
return daa;
}
async function waitExecuted(n, number) {
for (let k = 0; k < 600; k++) { const tip = hexn(await n.eth('eth_blockNumber')); if (tip >= number) return tip; await sleep(500); }
throw new Error(`block ${number} not executed in 300 s`);
}
// signs a segment record over the given public values with v0's key; the proof bytes are a placeholder (trust mode)
function signSegment(first, last, hash, pv, tag) {
const proof = Buffer.from(`igneum-proving-v1-harness-${tag}-${first}-${last}`);
const sg = run(MINER, ['sign-segment-record', 'v0', CHAIN_NAME, hash, String(first), String(last), PAYOUT, pv, sha256(proof)]);
if (sg.code !== 0) throw new Error(`sign-segment-record failed: ${sg.out}`);
const signed = JSON.parse(sg.out.trim().split('\n').pop());
return { record: signed.record, proof: '0x' + proof.toString('hex'), keyHash: signed.keyHash, statement: signed.statement };
}
async function waitSegmentPaid(n, first, secs = 240) {
const deadline = Date.now() + secs * 1000;
while (Date.now() < deadline) {
const r = await n.eth('igneum_getSegmentRecords', ['0x' + first.toString(16)]);
if (r.paid) return r;
await sleep(1000);
}
throw new Error(`segment ${first} not paid within ${secs} s on n${n.i}`);
}
try {
const n0 = await new Node(0, [], { IGNEUM_PROOF_VERIFY: 'trust' }).start();
const n1 = await new Node(1, [`127.0.0.1:${n0.p2pPort}`], { IGNEUM_PROOF_VERIFY: 'trust' }).start();
const n2 = await new Node(2, [`127.0.0.1:${n0.p2pPort}`, `127.0.0.1:${n1.p2pPort}`], { IGNEUM_PROOF_VERIFY: 'trust' }).start();
const nodes = [n0, n1, n2];
log(`node 0 says: ${n0.grepLog(/proving v1/).join(' | ') || '(no proving v1 line)'}`);
nodes.forEach((n, i) => miner(['vmine', n.grpc, String(SECS), '--label', `v${i}`, '--share', String(1 / 3), '--bps', '1', ...(i === 0 ? ['--evm-address', funded.address] : [])], `vmine-v${i}`));
const keyHashes = [];
for (let i = 0; i < 3; i++) {
for (let k = 0; k < 50 && !keyHashes[i]; k++) { const m = (() => { try { return readFileSync(`${TMP}/vmine-v${i}.log`, 'utf8').match(/key=([0-9a-f]{64})/); } catch { return null; } })(); if (m) keyHashes[i] = '0x' + m[1]; else await sleep(200); }
}
log(`vote keys: ${keyHashes.join(' ')}`);
const st0 = await n0.eth('igneum_getProvingStatus');
step('status', { v0: st0.activationDaa, v1: st0.v1 });
check('the node carries the v1 parameters', hexn(st0.v1.activationDaa) === V1 && hexn(st0.v1.segmentBlocks) === SEG && hexn(st0.v1.unprovenDaa) === UNPROVEN && hexn(st0.v1.aggregatorShareBps) === SHARE_BPS, st0.v1);
// the v1 start: the first chain block at DAA >= V1
await waitDaa(n0, V1 + 8, 'past the v1 activation');
let st = await n0.eth('igneum_getProvingStatus');
const S = hexn(st.v1.start);
step('v1_start', { start: S, tipDaa: hexn(st.tipDaa) });
check('every node agrees on the v1 start', (await Promise.all(nodes.map(n => n.eth('igneum_getProvingStatus')))).every(x => hexn(x.v1.start) === S), S);
// ---- case 1: the first segment, a fresh chain, carried and paid
const seg0 = [S, S + SEG - 1];
await waitExecuted(n0, seg0[1] + 1);
const stmt0 = await n0.eth('igneum_getSegmentStatement', ['0x' + seg0[0].toString(16)]);
check('segment 0 is aligned at the start and pending', hexn(stmt0.first) === seg0[0] && hexn(stmt0.last) === seg0[1] && stmt0.status.status === 'pending', { first: stmt0.first, last: stmt0.last, status: stmt0.status });
check('the native statement is the same on every node', (await Promise.all(nodes.map(n => n.eth('igneum_getSegmentStatement', ['0x' + seg0[0].toString(16)])))).every(x => x.publicValuesFresh === stmt0.publicValuesFresh), stmt0.publicValuesFresh.slice(0, 24));
const expectedWei0 = stmt0.blocks.reduce((a, b) => a + BigInt(b.aggregatorWei), 0n);
const creditSum0 = stmt0.blocks.reduce((a, b) => a + BigInt(b.poolCreditWei), 0n);
check('the aggregator share is a tenth of the segment credits', expectedWei0 === creditSum0 / 10n, { expectedWei0: expectedWei0.toString(), creditSum0: creditSum0.toString() });
const lastHash0 = stmt0.blocks[stmt0.blocks.length - 1].hash;
const rec0 = signSegment(seg0[0], seg0[1], lastHash0, stmt0.publicValuesFresh, 'seg0');
const tSubmit0 = Date.now();
const sub0 = await n1.eth('igneum_submitSegmentRecord', [{ record: rec0.record, proof: rec0.proof }]);
check('known-finished: segment 0 record accepted by n1', sub0.accepted === true && sub0.new === true, sub0);
const paid0 = await waitSegmentPaid(n0, seg0[0]);
const paidAt0 = (Date.now() - tSubmit0) / 1000;
check('known-finished: segment 0 paid on n0 (relayed over p2p, verified in trust mode, carried)', BigInt(paid0.paid.wei) === expectedWei0 && paid0.paid.payout.toLowerCase() === PAYOUT, { paid: paid0.paid, secs: paidAt0 });
await sleep(3000);
const agree0 = await Promise.all(nodes.map(n => n.eth('igneum_getSegmentRecords', ['0x' + seg0[0].toString(16)]).then(r => r.paid && r.paid.wei)));
check('every node paid segment 0 the same', agree0.every(w => w && BigInt(w) === expectedWei0), agree0);
const bal0 = BigInt(await n0.eth('eth_getBalance', [PAYOUT, 'latest']));
check('the payout address holds the aggregator share', bal0 === expectedWei0, { balance: bal0.toString() });
step('case1', { segment: seg0, wei: expectedWei0.toString(), paidSecs: paidAt0, carrier: paid0.paid.carrierNumber });
// ---- case 2: the chain rule on segment 1
const seg1 = [S + SEG, S + 2 * SEG - 1];
await waitExecuted(n0, seg1[1] + 1);
const stmt1 = await n0.eth('igneum_getSegmentStatement', ['0x' + seg1[0].toString(16)]);
check('segment 1 names segment 0 as its proven previous', stmt1.previous && hexn(stmt1.previous.first) === seg0[0] && hexn(stmt1.previous.chainLen) === SEG, stmt1.previous);
const lastHash1 = stmt1.blocks[stmt1.blocks.length - 1].hash;
const fresh1 = signSegment(seg1[0], seg1[1], lastHash1, stmt1.publicValuesFresh, 'seg1-fresh');
const subFresh = await n0.eth('igneum_submitSegmentRecord', [{ record: fresh1.record, proof: fresh1.proof }]);
check('chain rule: a fresh-chain record for segment 1 is refused while segment 0 is proven', subFresh.accepted === false && /does not chain to segment/.test(subFresh.reason), subFresh);
const cont1 = signSegment(seg1[0], seg1[1], lastHash1, stmt1.publicValuesContinuing, 'seg1-cont');
const subCont = await n2.eth('igneum_submitSegmentRecord', [{ record: cont1.record, proof: cont1.proof }]);
check('chain rule: the continuing record (chain_len 8) is accepted', subCont.accepted === true, subCont);
const paid1 = await waitSegmentPaid(n0, seg1[0]);
check('segment 1 paid with chain_len 8', hexn(paid1.paid.chainLen) === 2 * SEG, paid1.paid);
step('case2', { segment: seg1, refused: subFresh.reason, paid: paid1.paid });
// ---- case 3: segment 2 left unproven; segment 3 restarts the chain
const seg2 = [S + 2 * SEG, S + 3 * SEG - 1];
const seg3 = [S + 3 * SEG, S + 4 * SEG - 1];
await waitExecuted(n0, seg3[1] + 1);
const stmt2 = await n0.eth('igneum_getSegmentStatement', ['0x' + seg2[0].toString(16)]);
const deadline2 = hexn(stmt2.status.deadline_daa);
check('segment 2 is pending with a deadline', stmt2.status.status === 'pending' && deadline2 > 0, stmt2.status);
const stmt3early = await n0.eth('igneum_getSegmentStatement', ['0x' + seg3[0].toString(16)]);
const lastHash3 = stmt3early.blocks[stmt3early.blocks.length - 1].hash;
const fresh3 = signSegment(seg3[0], seg3[1], lastHash3, stmt3early.publicValuesFresh, 'seg3-fresh');
const subEarly = await n0.eth('igneum_submitSegmentRecord', [{ record: fresh3.record, proof: fresh3.proof }]);
check('known-failed: segment 3 cannot start a fresh chain while segment 2 is pending', subEarly.accepted === false && /pending until DAA/.test(subEarly.reason), subEarly);
await waitDaa(n0, deadline2 + 2, 'past segment 2 deadline');
const stmt2late = await n0.eth('igneum_getSegmentStatement', ['0x' + seg2[0].toString(16)]);
check('known-failed: segment 2 is unproven after its deadline', stmt2late.status.status === 'unproven', stmt2late.status);
const lastHash2 = stmt2late.blocks[stmt2late.blocks.length - 1].hash;
const late2 = signSegment(seg2[0], seg2[1], lastHash2, stmt2late.publicValuesContinuing || stmt2late.publicValuesFresh, 'seg2-late');
const subLate = await n0.eth('igneum_submitSegmentRecord', [{ record: late2.record, proof: late2.proof }]);
check('known-failed: a late record for segment 2 pays nothing (refused as unproven)', subLate.accepted === false && /unproven/.test(subLate.reason), subLate);
const subRestart = await n1.eth('igneum_submitSegmentRecord', [{ record: fresh3.record, proof: fresh3.proof }]);
check('segment 3 restarts the chain with a fresh-chain record after the unproven segment', subRestart.accepted === true, subRestart);
const paid3 = await waitSegmentPaid(n0, seg3[0]);
check('segment 3 paid with chain_len 4', hexn(paid3.paid.chainLen) === SEG, paid3.paid);
st = await n0.eth('igneum_getProvingStatus');
check('the status counts proven and unproven segments', st.v1.segmentsInWindow.proven >= 3 && st.v1.segmentsInWindow.unproven >= 1, st.v1.segmentsInWindow);
step('case3', { unproven: seg2, restarted: seg3, status: st.v1 });
// ---- case 4: the shard side after the switch
const work = await n0.eth('igneum_getAssignedShards', [[keyHashes[0]], 40]);
const w = work.find(x => hexn(x.number) >= S);
check('a v1 shard is paid 90% of its block share (one shard a block here)', w && BigInt(w.shardWei) === BigInt(w.poolCreditWei) - BigInt(w.poolCreditWei) / 10n, w && { number: w.number, shardWei: w.shardWei, credit: w.poolCreditWei });
const before = work.find(x => hexn(x.number) < S);
if (before) check('a pre-v1 shard keeps the whole share', BigInt(before.shardWei) === BigInt(before.poolCreditWei), { number: before.number });
report.ok = report.checks.every(c => c.ok);
log(`RESULT proving v1 harness: ${report.ok ? 'PASSED' : 'FAILED'} (${report.checks.length} checks) in ${since()} s`);
} catch (e) {
report.error = e.message;
log(`FAILED: ${e.message}`);
} finally {
writeFileSync(`${TMP}/report.json`, JSON.stringify(report, null, 2));
console.log(JSON.stringify(report, null, 2));
await stopAll();
process.exit(report.ok ? 0 : 1);
}

View file

@ -0,0 +1,88 @@
# Proving v1, step 2 (5 October 2026): aggregated chains on PC 2's RTX 5090. A signed `run` job (shell powershell,
# not elevated; the miners keep mining; the live prover in /opt/igneum is untouched: this build lands in /opt/igneum-pv1).
# 1. exports the chain from PC 2's own node (igneum_exportSegments 0..tip) to the job folder
# 2. inside WSL2 (root, Ubuntu-24.04): the fetched package igneum-prove-wsl2-pv1 -> ~/igneum-prove-pv1, every file
# re-stamped, built with the cuda feature against the live build's warm target dir, installed to /opt/igneum-pv1
# 3. cuts 8 consecutive live fixtures (tip-30 .. tip-23) with the new exporter, runs --mode native on each
# 4. --mode chain over the 8 (shards compressed, each block aggregated with the previous block's proof, verified),
# SP1_PROVER=cuda, a 1-s nvidia-smi sampler underneath for the GPU memory peak
# 5. --mode verify-segment on the final proof (the node's light-verifier path) for its timing
# Every number is a RESULT line. The results JSON is printed at the end (RESULTS-JSON ... END).
$ErrorActionPreference = 'Continue'
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
function Rpc($port, $method, $params) {
try { (Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:$port" -ContentType 'application/json' -Body (@{jsonrpc='2.0'; id=1; method=$method; params=$params} | ConvertTo-Json -Compress -Depth 6) -TimeoutSec 180).result } catch { "rpc error: $_" | Out-Host; $null }
}
$evmPort = $null
foreach ($p in 26790, 26800, 26810) { if (Rpc $p 'igneum_getProvingStatus' @()) { $evmPort = $p; break } }
$job = $env:IGNEUM_JOB_DIR
if (-not $job) { $job = Join-Path $env:TEMP 'igneum-pv1-chain' }
New-Item -ItemType Directory -Force -Path $job | Out-Null
$tipHex = Rpc $evmPort 'eth_blockNumber' @()
$tip = [Convert]::ToInt64($tipHex, 16)
$first = $tip - 30; $last = $first + 7
"RESULT start $(Stamp) node_evm_port=$evmPort tip=$tip chain blocks $first..$last"
# 1. the export (the whole chain: the exporter replays from genesis)
$t = Get-Date
$body = (@{jsonrpc='2.0'; id=1; method='igneum_exportSegments'; params=@('0x0', ('0x{0:x}' -f $last))} | ConvertTo-Json -Compress)
$seqFile = Join-Path $job 'seq.json'
try {
$resp = Invoke-WebRequest -Method Post -Uri "http://127.0.0.1:$evmPort" -ContentType 'application/json' -Body $body -TimeoutSec 600 -UseBasicParsing
[IO.File]::WriteAllBytes($seqFile, $resp.Content)
} catch { "RESULT export FAILED: $_"; exit 1 }
$len = (Get-Item $seqFile).Length
"RESULT export $(Stamp) $len bytes in $([math]::Round(((Get-Date) - $t).TotalSeconds,1)) s to $seqFile"
# the JSON-RPC envelope: the exporter wants the result object; unwrap with python inside WSL (below)
$pkg = Join-Path $env:LOCALAPPDATA 'igneum\prove\igneum-prove-wsl2-pv1\igneum-prove-wsl2'
if (-not (Test-Path $pkg)) { $pkg = Join-Path $env:LOCALAPPDATA 'igneum\prove\igneum-prove-wsl2-pv1' }
"RESULT package $(Stamp) $pkg exists=$(Test-Path (Join-Path $pkg 'package'))"
function WslPath($p) { $w = (& wsl.exe -d Ubuntu-24.04 -u root -- wslpath -a ($p -replace '\\', '/') 2>$null); if ($w) { ($w -replace "`0", '').Trim() } else { '/mnt/c' + ($p.Substring(2) -replace '\\', '/') } }
$pkgW = WslPath $pkg; $jobW = WslPath $job
$bash = @"
set -uo pipefail
export PATH="`$HOME/.cargo/bin:`$HOME/.sp1/bin:`$PATH"
CUDA_DIR="`$(ls -d /usr/local/cuda-12.* 2>/dev/null | sort -V | tail -1 || true)"; [ -n "`$CUDA_DIR" ] && export PATH="`$CUDA_DIR/bin:`$PATH" && export LD_LIBRARY_PATH="`$CUDA_DIR/lib64:/usr/lib/wsl/lib:`${LD_LIBRARY_PATH:-}"
stamp() { date -u +%Y-%m-%dT%H:%M:%SZ; }
PKG='$pkgW'; JOB='$jobW'; DEST="`$HOME/igneum-prove-pv1"; LIVE_TARGET="`$HOME/igneum-prove/proving/igneum-prove/target"
mkdir -p "`$DEST"
rsync -a --delete --exclude target "`$PKG/package/" "`$DEST/"
find "`$DEST" -name target -prune -o -type f -exec touch {} + 2>/dev/null
ls -la "`$DEST/proving/igneum-prove/elf/" | sed 's/^/elf: /'
grep -o '"program_id": "0x[0-9a-f]*"' "`$DEST/proving/igneum-prove/elf/manifest.json" | sed 's/^/RESULT manifest /'
cd "`$DEST/proving/igneum-prove"
echo "RESULT build start `$(stamp) target `$LIVE_TARGET (warm from the live build; the three workspace crates recompile)"
t0=`$(date +%s)
if ! CARGO_TARGET_DIR="`$LIVE_TARGET" cargo build --release -p igneum-prove-export -p igneum-prove-host --features igneum-prove-host/cuda 2>&1 | tail -3; then echo "RESULT build FAILED"; exit 1; fi
echo "RESULT build `$(stamp) exit 0 in `$(( `$(date +%s) - t0 )) s"
mkdir -p /opt/igneum-pv1 && cp "`$LIVE_TARGET/release/igneum-prove-host" "`$LIVE_TARGET/release/igneum-prove-export" /opt/igneum-pv1/
H=/opt/igneum-pv1/igneum-prove-host; X=/opt/igneum-pv1/igneum-prove-export
echo "RESULT installed `$(sha256sum `$H | cut -c1-16) host, `$(sha256sum `$X | cut -c1-16) export; live /opt/igneum untouched: `$(sha256sum /opt/igneum/igneum-prove-host | cut -c1-16)"
`$H --mode id | sed 's/^/RESULT pv1-host /'
# 3. the fixtures: unwrap the JSON-RPC envelope, cut eight consecutive blocks
python3 -c "import json,sys; d=json.load(open('`$JOB/seq.json')); json.dump(d['result'], open('`$JOB/export.json','w'))"
LIST=""
for n in `$(seq $first $last); do
if ! `$X "`$JOB/export.json" `$n "`$JOB/block-`$n.json" --source "PC 2 live devnet export, proving v1 chain run" 2>&1 | tail -1 | sed "s/^/export `$n: /"; then echo "RESULT cut `$n FAILED"; exit 1; fi
`$H "`$JOB/block-`$n.json" --mode native 2>&1 | grep -E "^RESULT (native|plan)" | sed "s/^/block `$n /"
LIST="`$LIST`${LIST:+,}`$JOB/block-`$n.json"
done
echo "RESULT fixtures `$(stamp) `$LIST"
# 4. the chain on the GPU, with the memory sampler
nvidia-smi --query-gpu=timestamp,index,memory.used,utilization.gpu,power.draw --format=csv,noheader,nounits -l 1 > "`$JOB/smi-chain.csv" 2>/dev/null &
SMI=`$!
t0=`$(date +%s)
SP1_PROVER=cuda RUST_LOG=off `$H --mode chain --chain "`$LIST" --prover 0xCAfc6e74000000000000000000000000000000c2 --out "`$JOB/chain-results.json" 2>&1 | grep -E "^(RESULT|STAGE|igneum-prove-host sources|segment proof written)" | sed 's/^/chain: /'
echo "RESULT chain wall `$(( `$(date +%s) - t0 )) s at `$(stamp)"
kill `$SMI 2>/dev/null; sleep 1
awk -F', *' '{ if (`$3+0 > max[`$2]) max[`$2]=`$3+0; n[`$2]++ } END { for (i in max) print "RESULT gpu_memory_peak_chain index=" i " samples=" n[i] " memory_used_max_mib=" max[i] }' "`$JOB/smi-chain.csv"
free -m | awk '/Mem:/ {print "RESULT wsl_ram_now total_mb=" `$2 " used_mb=" `$3}'
# 5. the node's verifier path on the final proof
STMT=`$(python3 -c "import json; print(json.load(open('`$JOB/chain-results.json'))['segment_statement'])")
PROOF=`$(python3 -c "import json; print(json.load(open('`$JOB/chain-results.json'))['segment_proof_file'])")
for i in 1 2 3; do `$H --mode verify-segment --proof "`$PROOF" --statement "`$STMT" 2>&1 | grep -E "^RESULT" | sed "s/^/verify-segment run `$i: /"; done
echo "RESULTS-JSON"; cat "`$JOB/chain-results.json"; echo; echo "END"
"@
$bashFile = Join-Path $job 'chain.sh'
[IO.File]::WriteAllText($bashFile, ($bash -replace "`r`n", "`n"), (New-Object System.Text.UTF8Encoding $false))
& wsl.exe -d Ubuntu-24.04 -u root -- bash (WslPath $bashFile) 2>&1 | ForEach-Object { ($_ -replace "`0", '') }
"RESULT end $(Stamp)"

View file

@ -0,0 +1,76 @@
# Proving v1, the 12 GB requirement (5 October 2026, the project lead: "make sure we can prove on 12gb cards"): the GPU memory peak
# of one shard proof on PC 2's RTX 5090 under SP1 6.8.1's knobs, the miners STOPPED by the job (stop_miners_first) and
# the live prover switched off for the run (its sp1-gpu-server would otherwise be the one the client connects to, with
# the live environment, not this one). Every config: the server killed, a 1-s nvidia-smi sampler, one compressed shard
# proof, the peak and the time as a RESULT line. Fixtures: the empty live shard (block 83616, the chain job's cut),
# the full shard at S_p (block-338-shard1, 6.75 M pgas) and block 344 (27 M pgas) re-cut at S_p/2 and S_p/4.
# Leaves the prover ON. The runner restores the miners.
$ErrorActionPreference = 'Continue'
$urlFile = if ($env:IGNEUM_APP_DIR) { Join-Path $env:IGNEUM_APP_DIR 'app.url' } else { Join-Path $env:LOCALAPPDATA 'igneum\app\app.url' }
if (-not (Test-Path $urlFile)) { $urlFile = Join-Path $env:LOCALAPPDATA 'igneum\app\app.url' }
$base = (Get-Content $urlFile -Raw).Trim().TrimEnd('/')
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
function Prove($on) { try { (Invoke-RestMethod -Method Post -Uri "$base/api/prove" -ContentType 'application/json' -Body (@{on=$on} | ConvertTo-Json -Compress) -TimeoutSec 10) | ConvertTo-Json -Compress } catch { "error: $_" } }
"RESULT start $(Stamp) prover off for the sweep: $(Prove $false)"
Start-Sleep -Seconds 45
"RESULT gpus $(Stamp) $((& nvidia-smi --query-gpu=index,name,memory.used,memory.total,utilization.gpu,power.draw --format=csv,noheader,nounits 2>$null) -join ' | ')"
$job = $env:IGNEUM_JOB_DIR; if (-not $job) { $job = Join-Path $env:TEMP 'igneum-pv1-mem' }; New-Item -ItemType Directory -Force -Path $job | Out-Null
function WslPath($p) { $w = (& wsl.exe -d Ubuntu-24.04 -u root -- wslpath -a ($p -replace '\\', '/') 2>$null); if ($w) { ($w -replace "`0", '').Trim() } else { '/mnt/c' + ($p.Substring(2) -replace '\\', '/') } }
$jobW = WslPath $job
$emptyW = WslPath (Join-Path $env:LOCALAPPDATA 'igneum\app\jobs\chain-pc2-pv1c\block-83616.json')
$bash = @"
set -uo pipefail
export PATH="`$HOME/.cargo/bin:`$HOME/.sp1/bin:`$PATH"
CUDA_DIR="`$(ls -d /usr/local/cuda-12.* 2>/dev/null | sort -V | tail -1 || true)"; [ -n "`$CUDA_DIR" ] && export PATH="`$CUDA_DIR/bin:`$PATH" && export LD_LIBRARY_PATH="`$CUDA_DIR/lib64:/usr/lib/wsl/lib:`${LD_LIBRARY_PATH:-}"
stamp() { date -u +%Y-%m-%dT%H:%M:%SZ; }
JOB='$jobW'; H=/opt/igneum-pv1/igneum-prove-host; X=/opt/igneum-pv1/igneum-prove-export; FX="`$HOME/igneum-prove-pv1/proving/fixtures"
EMPTY='$emptyW'; FULL="`$FX/block-338-shard1.json"
pkill -f sp1-gpu-server 2>/dev/null; sleep 2
echo "RESULT servers_before `$(pgrep -a sp1-gpu-server | tr '\n' ' ' || echo none)"
# the S_p/2 and S_p/4 cuts of block 344 (27 M pgas: 8 and 16 shards), from the package's own export
SEQ="`$HOME/igneum-prove-pv1/tools/prove-fixtures/seq.json"
if [ -f "`$SEQ" ]; then
`$X "`$SEQ" 344 "`$JOB/block-344-half.json" --budget 3750000 --source "block 344 cut at S_p/2 for the 12 GB sweep" 2>&1 | tail -1
`$X "`$SEQ" 344 "`$JOB/block-344-quarter.json" --budget 1875000 --source "block 344 cut at S_p/4 for the 12 GB sweep" 2>&1 | tail -1
fi
run() { # name fixture env...
local name="`$1" fx="`$2"; shift 2
pkill -f sp1-gpu-server 2>/dev/null; sleep 2
local csv="`$JOB/smi-`$name-`$(basename `$fx .json).csv"
nvidia-smi --query-gpu=timestamp,memory.used,utilization.gpu --format=csv,noheader,nounits -l 1 > "`$csv" 2>/dev/null &
local SMI=`$!
local t0=`$(date +%s)
env SP1_PROVER=cuda RUST_LOG=off "`$@" `$H "`$fx" --mode compressed --shard 0 --out "`$JOB/res-`$name-`$(basename `$fx .json).json" > "`$JOB/log-`$name-`$(basename `$fx .json).txt" 2>&1
local rc=`$?
local wall=`$(( `$(date +%s) - t0 ))
kill `$SMI 2>/dev/null; sleep 1
local peak=`$(awk -F', *' '{ if (`$2+0 > m) m=`$2+0 } END { print m+0 }' "`$csv")
local n=`$(wc -l < "`$csv")
local line=`$(grep -E "^RESULT compressed shard" "`$JOB/log-`$name-`$(basename `$fx .json).txt" | tail -1 | sed -E 's/.*prove ([0-9.]+) s, proof ([0-9]+) bytes, verify ([0-9.]+) s, ([A-Z ]+);.*/prove_s=\1 bytes=\2 verify_s=\3 \4/')
local cyc=`$(grep -E "^RESULT execute shard" "`$JOB/log-`$name-`$(basename `$fx .json).txt" | tail -1 | sed -E 's/.*: ([0-9]+) cycles.*/\1/')
local err=`$(grep -iE "error|panick|out of memory|OOM" "`$JOB/log-`$name-`$(basename `$fx .json).txt" | head -1 | cut -c1-160)
echo "RESULT sweep cfg=`$name fixture=`$(basename `$fx .json) peak_mib=`$peak samples=`$n wall_s=`$wall cycles=`${cyc:-na} `${line:-no_result} exit=`$rc env='`$*' `${err:+err=`$err}"
}
W1="SP1_WORKER_NUM_CORE_WORKERS=1 SP1_WORKER_CORE_BUFFER_SIZE=1 SP1_WORKER_NUM_RECURSION_PROVER_WORKERS=1 SP1_WORKER_RECURSION_PROVER_BUFFER_SIZE=1 SP1_WORKER_NUM_RECURSION_EXECUTOR_WORKERS=1 SP1_WORKER_RECURSION_EXECUTOR_BUFFER_SIZE=1 SP1_WORKER_NUM_PREPARE_REDUCE_WORKERS=1 SP1_WORKER_PREPARE_REDUCE_BUFFER_SIZE=1 SP1_WORKER_NUM_SETUP_WORKERS=1 SP1_WORKER_SETUP_BUFFER_SIZE=1 SP1_WORKER_NUM_DEFERRED_WORKERS=1 SP1_WORKER_DEFERRED_BUFFER_SIZE=1 SP1_WORKER_NUM_SPLICING_WORKERS=1 SP1_WORKER_SPLICING_BUFFER_SIZE=1"
W2="SP1_WORKER_NUM_CORE_WORKERS=2 SP1_WORKER_CORE_BUFFER_SIZE=2 SP1_WORKER_NUM_RECURSION_PROVER_WORKERS=2 SP1_WORKER_RECURSION_PROVER_BUFFER_SIZE=2 SP1_WORKER_NUM_RECURSION_EXECUTOR_WORKERS=2 SP1_WORKER_RECURSION_EXECUTOR_BUFFER_SIZE=2 SP1_WORKER_NUM_PREPARE_REDUCE_WORKERS=2 SP1_WORKER_PREPARE_REDUCE_BUFFER_SIZE=2"
run baseline "`$EMPTY"
run baseline "`$FULL"
run elem27 "`$FULL" ELEMENT_THRESHOLD=134217728
run elem26 "`$FULL" ELEMENT_THRESHOLD=67108864 HEIGHT_THRESHOLD=2097152
run workers1 "`$FULL" `$W1
run workers2 "`$FULL" `$W2
run w1elem27 "`$FULL" ELEMENT_THRESHOLD=134217728 `$W1
run w1elem26 "`$FULL" ELEMENT_THRESHOLD=67108864 HEIGHT_THRESHOLD=2097152 `$W1
run w1elem26chunk "`$FULL" ELEMENT_THRESHOLD=67108864 HEIGHT_THRESHOLD=2097152 MINIMAL_TRACE_CHUNK_THRESHOLD=4194304 TRACE_CHUNK_SLOTS=2 `$W1
run w1elem25 "`$FULL" ELEMENT_THRESHOLD=33554432 HEIGHT_THRESHOLD=1048576 `$W1
run w1elem26 "`$EMPTY" ELEMENT_THRESHOLD=67108864 HEIGHT_THRESHOLD=2097152 `$W1
[ -f "`$JOB/block-344-half.json" ] && run w1elem26 "`$JOB/block-344-half.json" ELEMENT_THRESHOLD=67108864 HEIGHT_THRESHOLD=2097152 `$W1
[ -f "`$JOB/block-344-quarter.json" ] && run w1elem26 "`$JOB/block-344-quarter.json" ELEMENT_THRESHOLD=67108864 HEIGHT_THRESHOLD=2097152 `$W1
[ -f "`$JOB/block-344-quarter.json" ] && run baseline "`$JOB/block-344-quarter.json"
pkill -f sp1-gpu-server 2>/dev/null
echo "RESULT sweep_end `$(stamp)"
"@
$bashFile = Join-Path $job 'sweep.sh'
[IO.File]::WriteAllText($bashFile, ($bash -replace "`r`n", "`n"), (New-Object System.Text.UTF8Encoding $false))
& wsl.exe -d Ubuntu-24.04 -u root -- bash (WslPath $bashFile) 2>&1 | ForEach-Object { ($_ -replace "`0", '') }
"RESULT end $(Stamp) prover back on: $(Prove $true)"

View file

@ -0,0 +1,111 @@
# Proving v1, step 1 (5 October 2026): what the prover costs a mining machine. A signed `run` job for PC 2
# (app/igneum-app/src/jobrun.rs, shell powershell, not elevated). Never stops the miners.
# 1. waits (up to 20 min) for the NVIDIA worker to report a hash rate, so both phases see the same miner
# 2. prover OFF (POST /api/prove {"on":false}): 5 min of samples every 15 s
# 3. prover ON: 5 min of samples; a 1-s nvidia-smi sampler runs underneath for the GPU memory peak
# 4. inside WSL2: the sp1-gpu-server's version and its compiled SM targets (cuobjdump, strings)
# 5. leaves the prover ON
# Every number is a RESULT line; the per-sample lines are SAMPLE lines. Read with `node tools/jobs.mjs <id>`.
$ErrorActionPreference = 'Continue'
$base = (Get-Content (Join-Path $env:LOCALAPPDATA 'igneum\app\app.url') -Raw).Trim().TrimEnd('/')
function Stamp { (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }
function State { try { Invoke-RestMethod -Uri "$base/api/state" -TimeoutSec 10 } catch { $null } }
function Prove($on) { try { (Invoke-RestMethod -Method Post -Uri "$base/api/prove" -ContentType 'application/json' -Body (@{on=$on} | ConvertTo-Json -Compress) -TimeoutSec 10) | ConvertTo-Json -Compress } catch { "error: $_" } }
function Rpc($port, $method, $params) {
try { (Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:$port" -ContentType 'application/json' -Body (@{jsonrpc='2.0'; id=1; method=$method; params=$params} | ConvertTo-Json -Compress -Depth 6) -TimeoutSec 20).result } catch { $null }
}
function Smi { try { (& nvidia-smi --query-gpu=index,name,memory.used,memory.total,utilization.gpu,power.draw --format=csv,noheader,nounits 2>$null) -join ' | ' } catch { 'nvidia-smi failed' } }
function Ram {
$os = Get-CimInstance Win32_OperatingSystem
$usedMb = [math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / 1024)
$vm = (Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -like 'vmmem*' } | Measure-Object WorkingSet64 -Sum).Sum
$vmMb = if ($vm) { [math]::Round($vm / 1MB) } else { 0 }
"host_used_mb=$usedMb total_mb=$([math]::Round($os.TotalVisibleMemorySize / 1024)) vmmem_mb=$vmMb"
}
function Card($st) { if ($null -eq $st) { return $null }; $st.mining.cards | Where-Object { $_.vendor -eq 'nvidia' } | Select-Object -First 1 }
function Sample($phase, $i) {
$st = State; $c = Card $st; $pv = if ($st) { $st.proving } else { $null }
$cards = if ($st) { ($st.mining.cards | ForEach-Object { "$($_.name):$($_.state):$([math]::Round($_.hash_now,1))" }) -join ',' } else { 'no state' }
"SAMPLE $phase $i $(Stamp) nvidia_hash_now=$(if ($c) { [math]::Round($c.hash_now,2) } else { 'na' }) nvidia_hash_avg=$(if ($c) { [math]::Round($c.hash_avg,2) } else { 'na' }) nvidia_state=$(if ($c) { $c.state } else { 'na' }) restarts=$(if ($c) { $c.restarts } else { 'na' }) cards=$cards proving=$(if ($pv) { "$($pv.status)/proved=$($pv.proved)/submitted=$($pv.submitted)/last_prove_s=$([math]::Round($pv.last_prove_s,1))/current='$($pv.current)'" } else { 'na' }) smi=[$(Smi)] $(Ram)"
}
$evmPort = $null
foreach ($p in 26790, 26800, 26810) { if (Rpc $p 'igneum_getProvingStatus' @()) { $evmPort = $p; break } }
$st0 = State
"RESULT start $(Stamp) app $($st0.version) machine $($st0.machine_id) node_evm_port=$evmPort prove_setting=$($st0.settings.prove) proving_status=$($st0.proving.status)/$($st0.proving.backend)"
"RESULT gpus $(Stamp) $(Smi)"
# 1. the NVIDIA worker must be hashing before anything is measured (5 October 2026, 18:35Z: the 5090 worker was
# exiting on a pack seed mismatch; a baseline of 0 MH/s is not a baseline)
$waited = 0
while ($waited -lt 1200) {
$c = Card (State)
if ($c -and $c.hash_now -gt 10) { break }
if ($waited % 120 -eq 0) { "WAIT $(Stamp) nvidia worker: state=$(if ($c) { $c.state } else { 'na' }) hash_now=$(if ($c) { $c.hash_now } else { 'na' }) restarts=$(if ($c) { $c.restarts } else { 'na' }) (waiting up to 20 min)" }
Start-Sleep -Seconds 20; $waited += 20
}
$c = Card (State)
$minerUp = ($c -and $c.hash_now -gt 10)
"RESULT miner $(Stamp) nvidia worker $(if ($minerUp) { 'hashing' } else { 'NOT hashing after 20 min: the mining-cost rows below are void' }) hash_now=$(if ($c) { $c.hash_now } else { 'na' }) after waiting $waited s"
$paid0 = Rpc $evmPort 'igneum_getProvingStatus' @()
# 2. prover off
"RESULT prove_off $(Stamp) $(Prove $false)"
Start-Sleep -Seconds 30
$off = @(); for ($i = 1; $i -le 20; $i++) { $line = Sample 'off' $i; $line; $off += (State); Start-Sleep -Seconds 15 }
$pvOffEnd = (State).proving
# 3. prover on, with a 1-s GPU memory sampler underneath
$smiFile = Join-Path $env:TEMP "igneum-pv1-smi-$(Get-Date -Format yyyyMMdd-HHmmss).csv"
$smiProc = Start-Process -FilePath 'nvidia-smi' -ArgumentList '--query-gpu=timestamp,index,memory.used,utilization.gpu,power.draw --format=csv,noheader,nounits -l 1' -RedirectStandardOutput $smiFile -NoNewWindow -PassThru
"RESULT prove_on $(Stamp) $(Prove $true) (gpu sampler pid $($smiProc.Id) -> $smiFile)"
$on = @(); for ($i = 1; $i -le 20; $i++) { $line = Sample 'on' $i; $line; $on += (State); Start-Sleep -Seconds 15 }
try { Stop-Process -Id $smiProc.Id -Force -ErrorAction SilentlyContinue } catch {}
Start-Sleep -Seconds 2
$pvOnEnd = (State).proving
$paid1 = Rpc $evmPort 'igneum_getProvingStatus' @()
function Stats($states) {
$h = @(); foreach ($s in $states) { $c = Card $s; if ($c) { $h += [double]$c.hash_now } }
if ($h.Count -eq 0) { return 'n=0' }
$m = ($h | Measure-Object -Average -Minimum -Maximum)
$sorted = $h | Sort-Object; $p50 = $sorted[[math]::Floor(($sorted.Count - 1) / 2)]
"n=$($h.Count) mean=$([math]::Round($m.Average,2)) p50=$([math]::Round($p50,2)) min=$([math]::Round($m.Minimum,2)) max=$([math]::Round($m.Maximum,2)) MH/s"
}
"RESULT mining_alone $(Stamp) nvidia hash_now over 5 min: $(Stats $off); proved during the phase: $($pvOffEnd.proved - $off[0].proving.proved)"
"RESULT mining_and_proving $(Stamp) nvidia hash_now over 5 min: $(Stats $on); proved during the phase: $($pvOnEnd.proved - $on[0].proving.proved) shards (submitted $($pvOnEnd.submitted - $on[0].proving.submitted)), last_prove_s=$($pvOnEnd.last_prove_s); node paidShards $($paid0.paidShards) -> $($paid1.paidShards)"
# the GPU memory peak from the 1-s sampler: per card index, max memory.used (MiB) and the sample count
try {
$rows = Get-Content $smiFile | Where-Object { $_ -match ',' } | ForEach-Object { $f = $_ -split ',\s*'; [pscustomobject]@{ t = $f[0]; idx = $f[1]; mem = [int]$f[2]; util = [int]$f[3]; power = [double]$f[4] } }
foreach ($g in ($rows | Group-Object idx)) {
$mx = ($g.Group | Measure-Object mem -Maximum).Maximum; $mn = ($g.Group | Measure-Object mem -Minimum).Minimum; $ut = ($g.Group | Measure-Object util -Average).Average; $pw = ($g.Group | Measure-Object power -Maximum).Maximum
"RESULT gpu_memory_peak $(Stamp) index=$($g.Name) samples=$($g.Count) memory_used_min_mib=$mn memory_used_max_mib=$mx util_mean_pct=$([math]::Round($ut,1)) power_max_w=$pw (1-s nvidia-smi samples while mining with the prover on)"
}
} catch { "RESULT gpu_memory_peak error: $_" }
# host RAM peak over both phases, from the 15-s samples (host used and the WSL2 VM's working set)
$ramPeakOff = 0; $vmPeakOff = 0; $ramPeakOn = 0; $vmPeakOn = 0
# (re-sampled here from the SAMPLE lines' fields was simpler; the per-sample Ram() strings are above; compute again from a fresh sample for the record)
"RESULT ram_now $(Stamp) $(Ram)"
# 4. the SP1 GPU server inside WSL2: version and compiled SM targets
$bash = @'
export PATH="$HOME/.cargo/bin:$HOME/.sp1/bin:$PATH"
CUDA_DIR="$(ls -d /usr/local/cuda-12.* 2>/dev/null | sort -V | tail -1 || true)"; [ -n "$CUDA_DIR" ] && export PATH="$CUDA_DIR/bin:$PATH"
f="$HOME/.sp1/bin/sp1-gpu-server"
echo "RESULT gpu_server_file $(ls -la "$f" 2>&1)"
echo "RESULT gpu_server_version $("$f" --version 2>&1 | head -2 | tr '\n' ' ')"
echo "RESULT gpu_server_sha256 $(sha256sum "$f" 2>/dev/null | cut -c1-64)"
echo "RESULT nvcc $(nvcc --version 2>&1 | tail -1)"
echo "RESULT wsl_nvidia_smi $(nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader 2>&1 | head -1)"
if command -v cuobjdump >/dev/null; then
echo "RESULT cuobjdump_elf $(cuobjdump --list-elf "$f" 2>&1 | grep -oE 'sm_[0-9]+' | sort | uniq -c | tr '\n' ' ')"
echo "RESULT cuobjdump_ptx $(cuobjdump --list-ptx "$f" 2>&1 | grep -oE 'sm_[0-9]+|compute_[0-9]+' | sort | uniq -c | tr '\n' ' ')"
echo "RESULT cuobjdump_head $(cuobjdump --list-elf "$f" 2>&1 | head -5 | tr '\n' ' ')"
else
echo "RESULT cuobjdump missing on PATH ($PATH)"
fi
echo "RESULT strings_sm $(strings "$f" 2>/dev/null | grep -oE '\b(sm|compute)_[0-9]{2,3}\b' | sort | uniq -c | tr '\n' ' ')"
echo "RESULT strings_arch_hints $(strings "$f" 2>/dev/null | grep -iE 'cuda_arch|gencode|arch=|--generate-code|nvcc' | sort -u | head -8 | tr '\n' ' ' | cut -c1-600)"
echo "RESULT host_elf_ids $(/opt/igneum/igneum-prove-host --mode id 2>&1 | tail -1)"
echo "RESULT free $(free -m | awk '/Mem:/ {print "wsl_total_mb="$2" used_mb="$3}')"
'@
$bashFile = Join-Path $env:TEMP 'igneum-pv1-arch.sh'
[IO.File]::WriteAllText($bashFile, ($bash -replace "`r`n", "`n"), (New-Object System.Text.UTF8Encoding $false))
$wslPath = (& wsl.exe -d Ubuntu-24.04 -u root -- wslpath -a ($bashFile -replace '\\', '/') 2>$null)
if (-not $wslPath) { $wslPath = '/mnt/c' + ($bashFile.Substring(2) -replace '\\', '/') }
& wsl.exe -d Ubuntu-24.04 -u root -- bash $wslPath 2>&1 | ForEach-Object { ($_ -replace "`0", '') }
"RESULT end $(Stamp) prover left ON: $(Prove $true); app proving status $((State).proving.status)"