Merge commit '22c2363' into release-0.3.11

# Conflicts:
#	docs/bench-log.md
#	docs/evidence.md
#	site/litepaper.html
This commit is contained in:
igneum-josh 2026-10-05 23:39:34 +01:00
commit 5cbb796051
40 changed files with 13079 additions and 29 deletions

View file

@ -73,6 +73,8 @@ jobs:
run: bash tools/ci/signer-pipe-check.sh run: bash tools/ci/signer-pipe-check.sh
- name: pinned guest programs match their manifest and are built only by pin-guests.sh - name: pinned guest programs match their manifest and are built only by pin-guests.sh
run: bash tools/ci/pinned-guests-check.sh run: bash tools/ci/pinned-guests-check.sh
- name: root prover playbooks kill the GPU server and unlink its socket (the root-socket class, 5 October 2026)
run: bash tools/ci/prover-socket-check.sh
- name: no secret file names and no 64-hex secrets in the tree (self-test first, then the tree) - name: no secret file names and no 64-hex secrets in the tree (self-test first, then the tree)
run: bash tools/ci/no-secrets-check.sh --self-test && bash tools/ci/no-secrets-check.sh run: bash tools/ci/no-secrets-check.sh --self-test && bash tools/ci/no-secrets-check.sh
- name: faucet unit tests (validation, the daily limits, the signed transaction; keccak, RLP and secp256k1 vectors) - name: faucet unit tests (validation, the daily limits, the signed transaction; keccak, RLP and secp256k1 vectors)

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. /// so it includes proof records it never verified (src/verifier.rs). Default off; a found verifier always wins.
#[serde(default)] #[serde(default)]
pub proof_verify_trust: bool, 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 { fn one() -> u32 {
@ -98,7 +102,7 @@ fn yes() -> bool {
impl Default for Settings { impl Default for Settings {
fn default() -> 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

@ -983,3 +983,19 @@ mod tests {
assert_eq!(big.identities, 8); assert_eq!(big.identities, 8);
} }
} }
/// The machine's RAM in MB (the prover default's RAM gate, src/provedefault.rs): Windows through
/// `Win32_OperatingSystem.TotalVisibleMemorySize` (KB), Linux through `/proc/meminfo`, macOS through `sysctl hw.memsize`;
/// None when unreadable (no gate).
pub fn total_ram_mb() -> Option<u64> {
if cfg!(windows) {
let out = run_timeout(Command::new(crate::platform::tool("powershell")).args(["-NoProfile", "-Command", "(Get-CimInstance Win32_OperatingSystem).TotalVisibleMemorySize"]), None, Duration::from_secs(20))?;
return out.replace('\0', "").trim().parse::<u64>().ok().map(|kb| kb / 1024);
}
if cfg!(target_os = "linux") {
let text = std::fs::read_to_string("/proc/meminfo").ok()?;
return text.lines().find(|l| l.starts_with("MemTotal:")).and_then(|l| l.split_whitespace().nth(1)).and_then(|kb| kb.parse::<u64>().ok()).map(|kb| kb / 1024);
}
let out = run_timeout(Command::new("sysctl").args(["-n", "hw.memsize"]), None, Duration::from_secs(5))?;
out.trim().parse::<u64>().ok().map(|b| b / (1024 * 1024))
}

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() })) 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, crate::detect::total_ram_mb());
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. /// 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> { pub fn set_prove(&self, on: bool) -> Result<Value, String> {
{ {
@ -415,6 +449,42 @@ fn jitter_secs(seed: u64) -> u64 {
5 + (seed.wrapping_mul(2654435761) >> 7) % 56 5 + (seed.wrapping_mul(2654435761) >> 7) % 56
} }
/// The `--prepare-packs` directory the miner gets, relative to the app data folder (its cwd), in the platform's
/// separator: `packs\prepare` on Windows, `packs/prepare` elsewhere. Every worker gets it (program class v3).
pub(crate) fn prepare_packs_arg() -> String {
if cfg!(windows) { "packs\\prepare".to_string() } else { "packs/prepare".to_string() }
}
/// Seconds after a resume before every enabled card must be mining (a worker takes 10 to 60 s to its first STATUS
/// line with a hash rate; the pack export before it a few seconds more).
pub(crate) const RESUME_CHECK_SECS: u64 = 90;
/// What the resume rule reads from a miner slot.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ResumeSlot {
/// the watchdog marked the card faulted
pub faulted: bool,
/// a worker process is alive on the slot
pub live: bool,
}
/// The resume rule: every slot without a live worker is re-armed, faulted or not. (The rule before 5 October 2026
/// re-armed only faulted slots; `stop_miners("paused")` had cleared every slot's `restart_at`, so a healthy paused
/// card never restarted: PC 2 at 21:25:11Z, the Mac that afternoon.)
pub(crate) fn slots_to_rearm_on_resume(slots: &[ResumeSlot]) -> Vec<usize> {
slots.iter().enumerate().filter(|(_, s)| !s.live).map(|(i, _)| i).collect()
}
/// The check RESUME_CHECK_SECS after a resume: every enabled, present, non-faulted card must report a hash rate
/// above 0 or its name is returned with its state (the caller logs one line per card).
pub(crate) fn resume_check(cards: &[CardState]) -> Vec<String> {
cards
.iter()
.filter(|c| c.enabled && c.present() && c.state != "faulted" && c.hash_now <= 0.0)
.map(|c| format!("{} is not mining {RESUME_CHECK_SECS} s after resume (state {}, pid {}{})", c.name, c.state, c.pid, if c.message.is_empty() { String::new() } else { format!(", {}", c.message) }))
.collect()
}
struct MinerSlot { struct MinerSlot {
card: usize, card: usize,
label: String, label: String,
@ -443,6 +513,8 @@ pub struct Engine {
node: Option<Proc>, node: Option<Proc>,
node_external: bool, node_external: bool,
node_restart_at: Option<Instant>, node_restart_at: Option<Instant>,
/// resume rule (5 October 2026): when due, every enabled card must be mining or its name goes to the log
resume_check_at: Option<Instant>,
node_started_at: Instant, node_started_at: Instant,
node_starts: u32, node_starts: u32,
node_restarts: u32, node_restarts: u32,
@ -545,6 +617,7 @@ impl Engine {
node: None, node: None,
node_external: false, node_external: false,
node_restart_at: None, node_restart_at: None,
resume_check_at: None,
node_started_at: now, node_started_at: now,
node_starts: 0, node_starts: 0,
node_restarts: 0, node_restarts: 0,
@ -711,6 +784,8 @@ impl Engine {
self.detect_again = false; self.detect_again = false;
self.shared.send(Cmd::Detect); 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::ApplyCards(choices) => self.apply_cards(choices),
Cmd::Start => self.start(), Cmd::Start => self.start(),
@ -729,13 +804,23 @@ impl Engine {
self.shared.save_settings(); self.shared.save_settings();
self.st().mining.paused = false; self.st().mining.paused = false;
self.shared.event("ok", "mining resumed"); self.shared.event("ok", "mining resumed");
// a user action: faulted cards try again // the resume rule (5 October 2026, PC 2 at 21:25:11Z and the Mac that afternoon): stop_miners("paused")
for m in self.miners.iter_mut() { // cleared every slot's restart_at and this arm re-armed only FAULTED slots, so a card whose worker had
// simply been stopped stayed "off" at 0 MH/s until the app was relaunched. Now every slot without a
// live worker is re-armed (a faulted one reset first), its pack is exported again before the start
// (prepared = false: the hour may have turned while paused), and a check 90 s later names any
// enabled card that is not mining (`resume_check`).
let views: Vec<ResumeSlot> = self.miners.iter().map(|m| ResumeSlot { faulted: m.watch.faulted().is_some(), live: m.proc.is_some() }).collect();
let now = Instant::now();
for i in slots_to_rearm_on_resume(&views) {
let m = &mut self.miners[i];
if m.watch.faulted().is_some() { if m.watch.faulted().is_some() {
m.watch.event(0.0, crate::watchdog::Event::Reset); m.watch.event(0.0, crate::watchdog::Event::Reset);
m.restart_at = Some(Instant::now());
} }
m.restart_at = Some(now);
m.prepared = false;
} }
self.resume_check_at = Some(now + Duration::from_secs(RESUME_CHECK_SECS));
if !self.running { if !self.running {
self.start(); self.start();
} }
@ -1385,13 +1470,16 @@ impl Engine {
a.push("--network".into()); a.push("--network".into());
a.push(r.network.clone()); a.push(r.network.clone());
} }
// The miner runs with the app data folder as its cwd: the pack paths stay relative (the miner splits
// --worker-args on spaces, and %LOCALAPPDATA% may carry a space in the user name). Every worker gets
// --prepare-packs (5 October 2026, program class v3: the Metal worker too compiles v3 only from a prepared
// pack, `prepare <e> <d> <dir> class=v3 era=<hex>`; before this the flag went to the CUDA and OpenCL
// workers only and a Mac would answer `need` lines at the first v3 epoch and stop mining, the 18:23Z class).
a.push("--prepare-packs".into());
a.push(prepare_packs_arg());
if card.worker != "Metal" { if card.worker != "Metal" {
// The miner runs with the app data folder as its cwd: the pack paths stay relative (the miner splits // The prebuilt workers take the exported pack with --pack and build the next program from
// --worker-args on spaces, and %LOCALAPPDATA% may carry a space in the user name). The prebuilt workers // --prepare-packs; a worker built from source has the program compiled in and exits 42 at the boundary.
// take the exported pack with --pack and build the next program from --prepare-packs; a worker built
// from source has the program compiled in and exits 42 at the boundary instead.
a.push("--prepare-packs".into());
a.push("packs\\prepare".into());
if card.worker == "OpenCL" { if card.worker == "OpenCL" {
a.push("--job-nonces".into()); a.push("--job-nonces".into());
a.push("2097152".into()); a.push("2097152".into());
@ -1426,7 +1514,9 @@ impl Engine {
self.miners[i].starts += 1; self.miners[i].starts += 1;
let seg = if self.miners[i].starts > 1 { format!("-r{}", self.miners[i].starts) } else { String::new() }; let seg = if self.miners[i].starts > 1 { format!("-r{}", self.miners[i].starts) } else { String::new() };
let log = self.shared.runtime.log_dir.join(format!("miner-{}-{}{seg}.log", self.miners[i].label, self.stamp)); let log = self.shared.runtime.log_dir.join(format!("miner-{}-{}{seg}.log", self.miners[i].label, self.stamp));
let cwd = if card.worker == "Metal" { None } else { Some(self.shared.runtime.app_dir.clone()) }; // every worker runs with the app data folder as its cwd, so the relative pack paths resolve (the Metal worker
// too, since it takes --prepare-packs now)
let cwd = Some(self.shared.runtime.app_dir.clone());
self.miners[i].prepared = false; self.miners[i].prepared = false;
// the fleet's per-card kernel tuning (from the signed manifest) reaches the GPU worker through the miner's environment // the fleet's per-card kernel tuning (from the signed manifest) reaches the GPU worker through the miner's environment
let envs: Vec<(String, String)> = self.ota.tuning_path().map(|p| vec![("IGNEUM_TUNING_FILE".to_string(), p.display().to_string())]).unwrap_or_default(); let envs: Vec<(String, String)> = self.ota.tuning_path().map(|p| vec![("IGNEUM_TUNING_FILE".to_string(), p.display().to_string())]).unwrap_or_default();
@ -2157,6 +2247,18 @@ impl Engine {
self.tick_node(now); self.tick_node(now);
self.tick_watch(now); self.tick_watch(now);
self.tick_miners(now); self.tick_miners(now);
if let Some(at) = self.resume_check_at {
if now >= at {
self.resume_check_at = None;
let (cards, paused) = { let st = self.st(); (st.mining.cards.clone(), st.mining.paused) };
if !paused {
for line in resume_check(&cards) {
self.shared.log(&format!("resume: {line}"));
self.shared.event("error", &format!("resume: {line}"));
}
}
}
}
self.tick_telemetry(now); self.tick_telemetry(now);
self.tick_sweep(now); self.tick_sweep(now);
} }
@ -3356,6 +3458,62 @@ pub fn parse_race(body: &str) -> Option<RaceParsed> {
Some(r) Some(r)
} }
#[cfg(test)]
mod prepare_packs_tests {
use super::*;
/// Program class v3 (5 October 2026): the Metal worker needs the prepare directory too, in the platform's
/// separator, relative to the app data folder the miner runs in.
#[test]
fn every_worker_gets_the_prepare_directory_in_the_platform_form() {
let arg = prepare_packs_arg();
if cfg!(windows) {
assert_eq!(arg, "packs\\prepare");
} else {
assert_eq!(arg, "packs/prepare", "the Mac's Metal worker gets a forward-slash path");
}
assert!(!arg.contains(' ') && !arg.starts_with('/'), "relative, no space: the miner splits --worker-args on spaces and the data folder may carry one");
}
}
#[cfg(test)]
mod resume_tests {
use super::*;
fn card(name: &str, enabled: bool, state: &str, hash: f64) -> CardState {
CardState { name: name.into(), vendor: "nvidia".into(), kind: "discrete".into(), enabled, state: state.into(), hash_now: hash, ..Default::default() }
}
/// The state machine: paused (every slot stopped, restart_at cleared) -> resumed -> every slot without a live
/// worker is re-armed, faulted or not.
#[test]
fn resume_rearms_every_stopped_slot() {
let slots = [ResumeSlot { faulted: false, live: false }, ResumeSlot { faulted: true, live: false }, ResumeSlot { faulted: false, live: true }];
assert_eq!(slots_to_rearm_on_resume(&slots), vec![0, 1], "the healthy stopped slot and the faulted one restart; the live one is left alone");
}
/// The known-failed case (PC 2, 5 October 2026, 21:25:11Z, app 0.3.9: `[ok] mining resumed`, then `0.00 MH/s,
/// waiting` for 20 minutes): one healthy slot, stopped by the pause, not faulted. The old rule re-armed only
/// faulted slots and returned nothing for it; the new rule returns it.
#[test]
fn the_pc2_resume_of_21_25_11z_restarts_under_the_new_rule_and_not_the_old() {
let old_rule = |slots: &[ResumeSlot]| -> Vec<usize> { slots.iter().enumerate().filter(|(_, s)| s.faulted).map(|(i, _)| i).collect() };
let pc2 = [ResumeSlot { faulted: false, live: false }];
assert!(old_rule(&pc2).is_empty(), "the 0.3.9 rule left the 5090's slot unarmed: this is the defect");
assert_eq!(slots_to_rearm_on_resume(&pc2), vec![0]);
}
/// The check 90 s after a resume names every enabled card without a hash rate, and nothing else.
#[test]
fn resume_check_names_the_cards_not_mining() {
let cards = [card("NVIDIA GeForce RTX 5090", true, "off", 0.0), card("AMD Radeon(TM) Graphics", true, "mining", 3.4), card("Intel UHD", false, "off", 0.0), card("RTX 3060", true, "faulted", 0.0)];
let lines = resume_check(&cards);
assert_eq!(lines.len(), 1, "{lines:?}");
assert!(lines[0].starts_with("NVIDIA GeForce RTX 5090 is not mining 90 s after resume (state off"), "{}", lines[0]);
assert!(resume_check(&[card("RTX 5090", true, "mining", 118.9)]).is_empty());
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{digest_from_line, parse_race, switches_of, sync_decision, Reading}; use super::{digest_from_line, parse_race, switches_of, sync_decision, Reading};

View file

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

View file

@ -0,0 +1,169 @@
//! Proving v1 step 1 (5 October 2026, Josh: "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 (bench-log 5 October 2026, "proving v1", the S_p curve on the RTX 5090, SP1 6.8.1's GPU prover) |
//! |---|---|---|
//! | NVIDIA card with 24 GB or more, mining or not, Windows with WSL2 (Ubuntu-24.04) answering or Linux | on | a full shard at the adopted v1 budget (30,000 pgas, 4.7 M cycles) peaks at 20,434 MiB alone and 22,210 beside the miner (measured on the 5090; approximate for a 24 GB card's own allocation); the prototype shard the devnet proves until its fee switch (6.75 M pgas) peaks at 28,307 MiB alone and 30,039 beside the miner, so until the switch only a 32 GB card proves it and a 24 GB card's prover waits for shards it can hold (the host refuses nothing; a proof that runs out of memory fails and the shard is left) |
//! | NVIDIA card of 16 to 24 GB | off, with the line saying why | the GPU prover's floor is 13,874 MiB for an EMPTY shard, 15,670 beside the miner; a 16 GB card holds no full shard |
//! | NVIDIA card under 16 GB | off | 13,874 MiB does not fit; Josh's 12 GB requirement is open until a prover build with a smaller floor is measured |
//! | Windows under 32 GB of RAM | off, with the line saying why | the WSL2 prover held 7.9 GB on a 63 GB PC; a 16 GB PC would swap |
//! | Windows with a qualifying card but WSL2 silent | off, with the Set up hint | nothing can prove until the distribution exists |
//! | Apple silicon | off | the M5 Max CPU took 41 to 55 s for an EMPTY shard's compressed proof under load and 272 s for a 200-pgas shard; a full shard was never under 60 s (bench-log 4 and 5 October 2026) |
//! | AMD-only (no NVIDIA card) | off, "mines and does not prove" | no zkVM proves on an AMD GPU today (docs/analysis/amd-proving.md); the SP1 CPU prover on PC 1 cost 82 to 87 s core plus 199 to 202 s compressed a shard at a 30 GB RSS whatever the shard size (bench-log, "the SP1 CPU prover on PC 1") |
//!
//! Decided 5 October 2026 (delegated by Josh: "deploy what is absolute best"), docs/plans/proving-v1.md. The default
//! never switches an explicit on back off, and Settings always wins afterwards.
use crate::state::CardState;
/// A card that proves, mining or not, needs this much: the adopted v1 shard peaks at 20,434 MiB alone (the S_p curve,
/// 5 October 2026) and 22,210 beside the miner; `nvidia-smi` reports MiB and a 24 GB card reports 24,564, so the
/// test is at 23 GB. (The same value for a mining and an idle card: the floor is the GPU server's, not the miner's.)
pub const MIN_VRAM_MB_MINING: u64 = 23_552;
pub const MIN_VRAM_MB_PROVE_ONLY: u64 = 23_552;
/// What a 32 GB card alone can do that a 24 GB one cannot: the prototype shard (28,307 MiB alone, 30,039 beside the
/// miner), the devnet's shard until its fee switch at DAA 210,000; `nvidia-smi` reports 32,607 for the RTX 5090.
pub const VRAM_MB_PROTOTYPE_SHARD: u64 = 31_000;
#[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
}
/// Windows machines under this much RAM stay off until measured (consequences review C4, 5 October 2026): PC 2 at
/// 63 GB had 25.6 GB in use with the WSL2 VM's working set at 7.9 GB while proving; a 16 GB PC would swap.
pub const MIN_RAM_MB_WINDOWS: u64 = 31_000;
/// The card an aggregation (the chained SP1 recursion, spec 7.8) may run on: 16,751 MiB measured with the miner
/// resident (13.4 GB alone, approximate), so a 24 GB card mining or not; the same gate as the shard prover.
pub fn aggregation_card(cards: &[CardState]) -> Option<&CardState> {
cards.iter().filter(|c| c.vendor == "nvidia" && c.vram_mb >= if c.enabled { MIN_VRAM_MB_MINING } else { MIN_VRAM_MB_PROVE_ONLY }).max_by_key(|c| c.vram_mb)
}
/// `os` is `std::env::consts::OS` ("windows", "linux", "macos"); `wsl_answers` is read on Windows only; `ram_mb` is the
/// machine's RAM when the platform reports it (None = unknown, no gate).
pub fn decide(cards: &[CardState], os: &str, wsl_answers: Option<bool>, ram_mb: Option<u64>) -> Decision {
let nvidia: Vec<&CardState> = cards.iter().filter(|c| c.vendor == "nvidia").collect();
// a mining card needs 20 GB (the measured mine-and-prove peak of 16.8 GB), a card that only proves 16 GB
let able: Vec<&CardState> = nvidia.iter().copied().filter(|c| c.vram_mb >= if c.enabled { MIN_VRAM_MB_MINING } else { MIN_VRAM_MB_PROVE_ONLY }).collect();
let off = |line: String| Decision { on: false, line };
if os == "macos" {
return off("proving stays off on Apple silicon: the M5 Max CPU took 41 to 55 s for an empty shard and minutes for a full one; 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), if c.enabled { ", mining" } else { "" })).collect::<Vec<_>>().join(", ")
};
let why = if nvidia.iter().any(|c| c.vram_mb >= 15_872) {
"a full shard needs a 24 GB card (measured 20.4 GB on the adopted shard size, 13.9 GB for an empty one); this card is under that, so Settings would switch proving on at your own risk"
} else if nvidia.is_empty() {
"this machine mines and does not prove: no zkVM proves on an AMD GPU today, and the CPU prover costs about 5 minutes a shard at a 30 GB RSS (bench-log, the SP1 CPU prover on PC 1); proving needs an NVIDIA card with 24 GB or more"
} else {
"no NVIDIA card with 24 GB or more (the GPU prover's floor is 13.9 GB for an empty shard and 20.4 GB for a full one)"
};
return off(format!("proving off by default: {why} ({seen})"));
};
let card = format!("{} ({} GB{})", best.name, gb(best.vram_mb), if best.enabled { ", mining too" } else { ", proving only" });
if os == "windows" {
if let Some(ram) = ram_mb {
if ram < MIN_RAM_MB_WINDOWS {
return off(format!("proving off by default: {card} qualifies but this PC has {} GB of RAM; proving needs 32 GB on Windows until a smaller PC is measured (the WSL2 prover held 7.9 GB on a 63 GB PC); Settings switches it on", gb(ram)));
}
}
}
let size_note = if best.vram_mb >= VRAM_MB_PROTOTYPE_SHARD { "" } else { "; until the devnet's fee switch its shards are the prototype size, which needs 32 GB, so this card proves from the switch on" };
match os {
"windows" => match wsl_answers {
Some(true) => Decision { on: true, line: format!("proving on by default: {card} with WSL2 (Ubuntu-24.04 answers){size_note}; 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){size_note}; 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, enabled: true, ..Default::default() }
}
fn idle(vendor: &str, name: &str, vram_mb: u64) -> CardState {
CardState { enabled: false, ..card(vendor, name, vram_mb) }
}
#[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), Some(63_132));
assert!(d.on);
assert!(d.line.starts_with("proving on by default: NVIDIA GeForce RTX 5090 (32 GB, mining too) 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), Some(65_000));
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, Some(65_000)).on, "an unread probe is not an answer");
}
#[test]
fn linux_needs_no_wsl2_and_the_memory_gates_hold() {
// the tiers of the S_p curve: 32 GB on with no note; 24 GB on with the prototype-size note; 16 GB and 12 GB off
let d = decide(&[card("nvidia", "NVIDIA GeForce RTX 5090", 32_607)], "linux", None, None);
assert!(d.on && !d.line.contains("fee switch"), "{}", d.line);
let d = decide(&[card("nvidia", "NVIDIA GeForce RTX 4090", 24_564)], "linux", None, None);
assert!(d.on && d.line.contains("needs 32 GB, so this card proves from the switch on"), "{}", d.line);
assert!(decide(&[idle("nvidia", "NVIDIA GeForce RTX 4090", 24_564)], "linux", None, None).on, "mining or not, 24 GB proves");
let d = decide(&[card("nvidia", "NVIDIA GeForce RTX 5080", 16_303)], "linux", None, None);
assert!(!d.on);
assert!(d.line.contains("a full shard needs a 24 GB card") && d.line.contains("RTX 5080 16 GB, mining"), "{}", d.line);
assert!(!decide(&[idle("nvidia", "NVIDIA GeForce RTX 5080", 16_303)], "linux", None, None).on, "16 GB holds no full shard even alone");
let d = decide(&[idle("nvidia", "NVIDIA GeForce RTX 3060", 12_288)], "linux", None, None);
assert!(!d.on);
assert!(d.line.contains("no NVIDIA card with 24 GB or more") && d.line.contains("RTX 3060 12 GB"), "{}", d.line);
assert!(!decide(&[card("nvidia", "NVIDIA GeForce RTX 3080", 10_240)], "linux", None, None).on);
let d = decide(&[card("amd", "Radeon RX 9070 XT", 16_384)], "linux", None, None);
assert!(!d.on && d.line.contains("mines and does not prove"), "{}", d.line);
assert!(decide(&[], "linux", None, None).line.contains("mines and does not prove"));
}
#[test]
fn apple_silicon_stays_off() {
let d = decide(&[card("apple", "Apple M5 Max", 65_536)], "macos", None, Some(65_536));
assert!(!d.on);
assert!(d.line.contains("Apple silicon"));
assert!(!decide(&[card("nvidia", "RTX 5090", 32_607)], "macos", Some(true), None).on, "the OS rule comes first");
}
#[test]
fn a_windows_pc_under_32_gb_stays_off_and_the_aggregation_card_follows_the_same_gate() {
let d = decide(&[card("nvidia", "NVIDIA GeForce RTX 4090", 24_564)], "windows", Some(true), Some(16_300));
assert!(!d.on);
assert!(d.line.contains("16 GB of RAM") && d.line.contains("needs 32 GB on Windows"), "{}", d.line);
assert!(decide(&[card("nvidia", "NVIDIA GeForce RTX 4090", 24_564)], "windows", Some(true), None).on, "unknown RAM is not a gate");
assert!(decide(&[card("nvidia", "NVIDIA GeForce RTX 4090", 24_564)], "linux", None, Some(16_300)).on, "the RAM gate is Windows only (the WSL2 VM)");
let cards = [card("nvidia", "RTX 5080", 16_303), idle("nvidia", "RTX 4070 Ti", 12_282)];
assert!(aggregation_card(&cards).is_none(), "a mining 16 GB card and an idle 12 GB card cannot aggregate");
let cards = [card("nvidia", "RTX 5080", 16_303), idle("nvidia", "RTX 4090", 24_564)];
assert_eq!(aggregation_card(&cards).map(|c| c.name.as_str()), Some("RTX 4090"));
let cards = [card("nvidia", "RTX 5090", 32_607)];
assert_eq!(aggregation_card(&cards).map(|c| c.vram_mb), Some(32_607));
}
#[test]
fn the_biggest_qualifying_card_is_named() {
let d = decide(&[idle("nvidia", "RTX 4090", 24_564), card("nvidia", "RTX 5090", 32_607)], "linux", None, None);
assert!(d.line.contains("RTX 5090 (32 GB, mining too)"), "{}", d.line);
}
}

View file

@ -339,7 +339,9 @@ pub fn start(shared: Arc<Shared>, bin_dir: PathBuf) {
fn loop_forever(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: HashSet<(String, u32)> = HashSet::new();
let mut attempted_segments: HashSet<u64> = HashSet::new();
let mut tools: Option<Tools> = None; let mut tools: Option<Tools> = None;
let ram_mb = crate::detect::total_ram_mb();
let mut last_probe = Instant::now() - Duration::from_secs(600); let mut last_probe = Instant::now() - Duration::from_secs(600);
let mut submitted: Vec<(u64, String, u32, u128)> = Vec::new(); let mut submitted: Vec<(u64, String, u32, u128)> = Vec::new();
let mut last_verifier_read = Instant::now() - Duration::from_secs(600); let mut last_verifier_read = Instant::now() - Duration::from_secs(600);
@ -406,6 +408,20 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
} }
} }
let Some(t) = tools.as_ref() else { continue }; let Some(t) = tools.as_ref() else { continue };
// consequences review C22 (5 October 2026): the SP1 CPU prover takes 29.5 to 30.5 GB of RSS and about five
// minutes a shard whatever the shard size (PC 1, bench-log "the SP1 CPU prover on PC 1"); on a machine under
// 32 GB it would swap the node out, so the CPU path is refused here, Settings or not, with the reason
if !t.cuda {
if let Some(ram) = ram_mb {
if ram < crate::provedefault::MIN_RAM_MB_WINDOWS {
set(&shared, |p| {
p.status = "off".into();
p.message = format!("the CPU prover needs 32 GB of RAM (30 GB measured on PC 1); this machine has {} GB, so proving stays off here", (ram + 512) / 1024);
});
continue;
}
}
}
set(&shared, |p| { set(&shared, |p| {
p.enabled = true; p.enabled = true;
p.available = true; p.available = true;
@ -466,6 +482,20 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
p.assigned = assigned; p.assigned = assigned;
p.keys = keys.len() as u32; 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 { let Some(w) = choose(&work, &attempted) else {
set(&shared, |p| { set(&shared, |p| {
p.status = if submitted.is_empty() { "idle".into() } else { "submitted".into() }; p.status = if submitted.is_empty() { "idle".into() } else { "submitted".into() };
@ -505,11 +535,15 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
if !ok || !fixture.exists() { if !ok || !fixture.exists() {
return Err(format!("exporter: {}", out.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("failed"))); return Err(format!("exporter: {}", out.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("failed")));
} }
set(&shared, |p| p.message = if t.cuda { "proving on the GPU".into() } else { "proving on the CPU (slow)".into() }); set(&shared, |p| p.message = if t.cuda { "proving on the GPU".into() } else { "CPU prover: about five minutes a shard, 30 GB of RAM, paid only when no card proves first".into() });
let prover_env = if t.cuda { "cuda" } else { "cpu" }; let prover_env = if t.cuda { "cuda" } else { "cpu" };
let (ok, out) = run_tool(&shared, t, &t.host, &[fix_p, "--mode".into(), "compressed".into(), "--shard".into(), w.shard.to_string(), "--prover".into(), payout.clone(), "--out".into(), res_p], &[("SP1_PROVER", prover_env), ("RUST_LOG", "off")], Duration::from_secs(3 * 3600), &dir.join(format!("prove-{}-{}.log", w.number, w.shard))); let (ok, out) = run_tool(&shared, t, &t.host, &[fix_p, "--mode".into(), "compressed".into(), "--shard".into(), w.shard.to_string(), "--prover".into(), payout.clone(), "--out".into(), res_p], &[("SP1_PROVER", prover_env), ("RUST_LOG", "off")], Duration::from_secs(3 * 3600), &dir.join(format!("prove-{}-{}.log", w.number, w.shard)));
if !ok || !results.exists() { if !ok || !results.exists() {
return Err(format!("prover: {}", out.lines().rev().find(|l| l.contains("RESULT") || l.contains("rror")).unwrap_or("failed"))); let last = out.lines().rev().find(|l| l.contains("RESULT") || l.contains("rror")).unwrap_or("failed").to_string();
// the root-socket class (5 October 2026, PC 2 at 20:00Z and 21:25Z): a job that ran the host as root
// inside WSL2 left /tmp/sp1-cuda-0.sock owned by root, and this user's client cannot open it
let hint = if last.contains("PermissionDenied") { " (a GPU-server socket /tmp/sp1-cuda-*.sock owned by another user, left by a job that ran the prover as root: remove it as that user, or run the socket-fix job)" } else { "" };
return Err(format!("prover: {last}{hint}"));
} }
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 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 statement = res["statement"].as_str().ok_or("no statement in the results")?.to_string(); let statement = res["statement"].as_str().ok_or("no statement in the results")?.to_string();
@ -558,6 +592,124 @@ 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);
}
// the card gate (consequences review C2): a chained aggregation peaked at 16,751 MiB with the miner resident; the
// prover's own 24 GB gate applies; without such a card this machine proves shards and never aggregates
{
let cards = shared.state.lock().unwrap().mining.cards.clone();
if crate::provedefault::aggregation_card(&cards).is_none() {
return Ok(Some("no aggregation on this machine: it needs a 24 GB card (16.8 GB measured with the miner resident); shards still prove".into()));
}
}
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; /// 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. /// the user watches it and reboots when it asks. Elsewhere there is nothing to set up.
pub fn setup(shared: &Shared) -> Result<Value, String> { 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 /// the pinned guests' ids (`igneum-prove-host --mode id`): the shard program and the aggregator; empty until read
pub program_id: String, pub program_id: String,
pub aggregator_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)] #[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. /// 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 { pub fn candidates_text(bin_dir: &Path) -> String {
candidates(bin_dir).join(", ") 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', w.word + (pv.backend && enabled && pv.available ? ' (' + pv.backend.toUpperCase() + ')' : ''));
setText('pv-state-sub', w.sub); 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'); 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-assigned', String(pv.assigned || 0));
setText('pv-submitted', String(pv.submitted || 0)); setText('pv-submitted', String(pv.submitted || 0));
setText('pv-paid', String(pv.paid || 0)); setText('pv-paid', String(pv.paid || 0));

View file

@ -206,7 +206,7 @@
<div class="lead-row"> <div class="lead-row">
<div class="lead-text"> <div class="lead-text">
<h3>Prove shards on this machine</h3> <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 24 GB or more (a full shard needs 20.4 GB of GPU memory, measured); off on a Mac, whose CPU prover is slow.</p>
</div> </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> <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> </div>

View file

@ -1775,3 +1775,111 @@ CPU verifier, one M5 Max core at load average 5.5 (the fixed crate, ca2-mixer 1a
**The user tiers.** AMD RDNA 4 sits at about a seventh of a 5090 on this hash (its dependent-read rate: 2.4 G against 17.5 G per second), 2.2x worse per pound at list prices and 4.9x worse per watt (approximate); the card's memory system, not a tuning gap. The integrated tier on the CUDA and OpenCL one-click workers mines v3 with a restart per epoch until per-day dataset reuse lands (0.3.12). Card lifetime under the step schedule: a 4 GB card to year 4, 8 GB to year 12, 12 GB to year 28 with the cache freed after the daily build. **The user tiers.** AMD RDNA 4 sits at about a seventh of a 5090 on this hash (its dependent-read rate: 2.4 G against 17.5 G per second), 2.2x worse per pound at list prices and 4.9x worse per watt (approximate); the card's memory system, not a tuning gap. The integrated tier on the CUDA and OpenCL one-click workers mines v3 with a restart per epoch until per-day dataset reuse lands (0.3.12). Card lifetime under the step schedule: a 4 GB card to year 4, 8 GB to year 12, 12 GB to year 28 with the cache freed after the daily build.
**The bounty.** A bounty for any chip design beating a GPU by more than 2x on the published model, with a leaderboard by card model, follows the external review (spec O-1.17, January 2027); it is named publicly only once escrowed (`docs/plans/funding.md`, rule 3), which it is not yet. **The bounty.** A bounty for any chip design beating a GPU by more than 2x on the published model, with a leaderboard by card model, follows the external review (spec O-1.17, January 2027); it is named publicly only once escrowed (`docs/plans/funding.md`, rule 3), which it is not yet.
## 5 October 2026 (evening), proving v1: segment records, the chain rule, the unproven rule; what was measured tonight (proving engineer)
Branches `proving-v1` (main repository, worktree `igneum-wt-proving-v1`; fork `vendor/igneum-node-pv1` from a24ab01a). Rules: spec 7.8; plan `docs/plans/proving-v1.md`. Every row names its command. The live devnet was in a degraded state the whole evening: from 18:35Z the RTX 5090 workers on PC 1 and PC 2 exited at start on a pack seed mismatch (`the epoch seed bytes do not give the pack's IGNEUM_SEEDW_INIT`, restart 60+ on PC 2 by 19:05Z, another agent's branch `pack-loop`), the Mac app node was down from 17:45Z, so PC 2 mined 3.4 MH/s from its iGPU and PC 2's prover was the only prover; the coordinator held every PC 2 measurement at 19:00Z until the fleet mines again.
### Step 1, the prover default and its cost
| What | Measured |
|---|---|
| The default rule (`app/igneum-app/src/provedefault.rs`) | `cargo test --release -p igneum-app provedefault` on this Mac (the app crate, build lock, 19:05Z): 5 passed (a 5090 with WSL2 on Windows is on; Windows without WSL2 off with the Set up hint; Linux needs no WSL2 and the 12 GB gate holds, a 10 GB 3080 and a 16 GB AMD card stay off; Apple silicon off; the biggest qualifying card is named) |
| Mining alone against mining with the prover, first try (PC 2 job `prover-cost-pc2-pv1`, `tools/proving-v1/pc2-prover-cost.ps1`, published 18:40:48Z, ran 18:41:13Z) | VOID: the job waited 20 min for the 5090 worker to hash and it never did (the pack fault above); "mining alone" was 0 MH/s |
| Mining alone against mining with the prover, the re-run after the coordinator's go (job `prover-cost-pc2-pv1b`, ran 19:20:36Z to 19:51:17Z; the 5090 worker restored at 19:16Z and hashing throughout; prover OFF by `POST /api/prove {"on":false}` 19:40:39Z, back ON 19:46:09Z, left on). The job's own `/api/state` samples stayed empty on PC 2 (`Invoke-RestMethod` returns an object PowerShell 5.1 cannot walk, `cards=0`, the fix is for the next run), so the hash rate is read from the miner's own STATUS lines (`miner-nvidia-1ccfe586-1` uploads, `now=... MH/s wall`, one every 30 s, the intake table `miner_logs`) | prover OFF, 19:41:09 to 19:46:09Z: n 10, mean 124.72 MH/s, p50 124.81, min 124.10, max 125.38. Prover ON, 19:46:39 to 19:51:13Z: n 9, mean 119.74, p50 118.87, min 118.08, max 123.42. The 15 min before the job with the prover on (19:25 to 19:40Z): n 30, mean 119.88, p50 118.79. So the prover costs the 5090 5.0 MH/s, 4.0% of its hash rate, while it proves the devnet's empty shards one after another (1.4 a minute here: the node's paidShards 559 -> 566 over the 5-min phase). A full shard at `S_p` keeps the card busier (the 4 October run proved one in 10.9 s); the cost at that load is the chain job's row |
| GPU memory during proving, first try (phase B of the first job: the prover on for 5 min, 298 one-second `nvidia-smi --query-gpu=memory.used` samples, the 5090 worker dead so the card held nothing else) | memory.used min 1,654 MiB, max 13,816 MiB, utilisation mean 2.7%, power max 190.6 W: the prover alone on empty shards |
| GPU memory with the miner AND the prover on the card (the re-run's phase B, 298 one-second samples, 19:46 to 19:51Z) | memory.used min 3,396 MiB (the miner's dataset and program resident), max 15,590 MiB, utilisation mean 92.9%, power max 328.6 W. So the prover's own peak is about 12.2 GB on an empty shard (15,590 minus the miner's 3,396), and the two together need 15.6 GB: a 16 GB card (5080, 9070 XT class, if it had a CUDA path) sits 0.4 GB under tonight's peak with no room for a full shard, a 24 GB 4090 has 8.4 GB of headroom, a 12 GB card cannot mine and prove at once on this build. The full-shard peak is the chain job's row |
| Shards per minute with the mining worker dead | the node's `paidShards` 510 -> 518 over the 5-min phase: 1.6 shards a minute from one 5090 through the app's loop (export, cut, prove, sign, submit) |
| Host RAM (Windows `Win32_OperatingSystem` and the `vmmem` working set, sampled every 15 s) | host used 25,550 MB of 63,132 MB at the end; the WSL2 VM's working set 7,915 MB (2,334 MB used of 30,914 MB inside the distribution) |
| The SP1 GPU server's compiled targets (`cuobjdump --list-elf /root/.sp1/bin/sp1-gpu-server` inside PC 2's Ubuntu-24.04, CUDA 12.8, driver 610.47) | `sp1-gpu-server` 6.8.1 (251,306,680 bytes, sha256 c2642ad1c42e85d8525159cf0c7cd5200d8766c9be1283f452a1f9bf9fea725c, the asset `sp1_gpu_server_v6.8.1_x86_64.tar.gz` the SDK downloads, `sp1-cuda-6.8.1/src/server.rs`): one ELF each for sm_80, sm_86, sm_89, sm_90, sm_100 and sm_120; `strings` finds compute_120 PTX as well. So sm_89 (Ada: RTX 4090, 4080) is compiled in natively, no JIT; so are Ampere (3090, 3060), Hopper, Blackwell datacentre (sm_100) and consumer (sm_120, the 5090). Nothing for AMD (no HIP path in SP1) |
### Step 2, aggregated chains
| What | Measured |
|---|---|
| The new host (`--mode chain`, `aggregate`, `verify-segment`) against every fixture natively | `igneum-prove-host <f> --mode native` on the Mac for the 12 fixtures of `proving/fixtures/` (9 block, 3 fee-switch), host built from this branch 19:06Z: every one MATCHES (the package gate's native half); `--mode id`: shard `0x2b1a81cb...`, aggregator `0x474678f3...`, the 0.3.9 pin, unchanged |
| Eight consecutive live fixtures | `igneum_exportSegments 0x0..0x13cb4` on node 1's exec RPC (127.0.0.1:26790, read-only, 20:06 BST, tip 81,076): 71,042,616 bytes, 81,077 segments, 28 accounts, 0.5 s; `igneum-prove-export export.json <n> block-<n>.json` for 81046..81053: replayed 81,077 segments from genesis in 1.8 s each, every state root equal to the node's; one shard a block, 0 pgas (no transactions on the devnet tonight), `proving/fixtures/chain/` |
| Chain of 2 on the Mac CPU (the known-finished case of `--mode chain` before the GPU; M5 Max under the live nodes, the harness and two builds) | `SP1_PROVER=cpu igneum-prove-host --mode chain --chain block-81046.json,block-81047.json --out results.json` under the run lock, 19:07:48Z to 19:11:28Z: setup 12.2 s; block 81046: shard 0 compressed 55.4 s (1,272,897 bytes, verify 0.036 s), aggregate 52.0 s (1,272,909 bytes, verify 0.031 s), chain_len 1, agg_vk zero; block 81047: shard 41.3 s, aggregate WITH the previous block proof 59.1 s, chain_len 2, agg_vk = the pinned aggregator id; end to end 207.9 s; final proof 1,272,909 bytes, statement 0x232276f4... The recursion over the previous proof cost 7 s more than the first aggregation on this CPU |
| `--mode verify-segment` on that proof (the node's path: SP1 light verifier, pinned aggregator key) | VERIFIED in 0.032 s (0.27 s wall, three runs: 0.033, 0.032, 0.032); known-failed: a wrong statement NOT VERIFIED (0.032 s); the shard verifier (`--mode verify`) on the segment proof NOT VERIFIED, "program id 0x474678f3... IS NOT OURS 0x2b1a81cb..." |
| Chain of 8 on the RTX 5090 (N = 2, 4, 8), job `chain-pc2-pv1b` (`tools/proving-v1/pc2-chain.ps1`; the package `igneum-prove-wsl2-pv1.zip` eb6dccf8..., 1.5 MB, fetched by `fetch-prove-pv1` 19:51Z; the first try `chain-pc2-pv1` died in its own export step, fixed) | Ran 19:58:37Z: the export from PC 2's node (72,901,414 bytes, 1.4 s), the host built in WSL2 against the live build's warm target dir in 6 s and installed to `/opt/igneum-pv1` (the live `/opt/igneum` host untouched, sha 29cc4768...), `--mode id` the pinned pair; eight consecutive fixtures 83346..83353 cut, every one MATCHES natively. The chain on the GPU (SP1_PROVER=cuda, the miner mining on the same card at 119 MH/s): setup 12.7 s; block 83346: shard 7.4 s, aggregate 7.6 s (chain_len 1), 15.1 s; block 83347: shard 7.2 s, aggregate WITH the previous proof 9.5 s (chain_len 2, agg_vk the pinned aggregator id), 16.8 s, cumulative 31.8 s over 2 blocks; block 83348: shard 7.0 s, then at 20:01:09Z the app quit and aborted the job ("quit: stopping the miners, then the node", then "job chain-pc2-pv1b: aborted (the app is quitting)"; NOT an update: nothing of 0.3.10 was published; the log gives the quit no source; 20 s earlier the efficiency sweep's administrator prompt had been cancelled at the keyboard, and 13 s earlier the live prover had failed with "CudaClientError: Connect(PermissionDenied)", the root-owned socket my job had left, below). So N = 2 measured: 31.8 s of GPU time for two empty blocks, the chained aggregation 1.9 s dearer than the first; N = 4 and 8 are the re-run `chain-pc2-pv1c` after the restart. An empty shard's compressed proof on the 5090 is 7.0 to 7.4 s (the 200-pgas shard of 4 October took 2.7 s with the card to itself; tonight the miner held it at 92% utilisation) |
| The chain of 8, the third run `chain-pc2-pv1c` (20:05:21Z to 20:08:33Z, after the app restart; blocks 83616..83623 from PC 2's node at tip 83646, the same script; results `tools/proving-v1/chain-pc2-2026-10-05.json`) | Build 5 s (warm), eight fixtures cut and MATCHING natively, setup 15.7 s, then on the GPU with the miner mining on the same card: shard proofs 7.3 to 7.7 s each (8 x, 59.5 s), aggregations 7.9 s for the first block and 9.6 to 9.7 s for every chained one (75.5 s), every proof VERIFIED, end to end 135.6 s for 8 blocks (17.0 s a block from the second on). Cumulative: N = 2 at 32.6 s, N = 4 at 66.8 s, N = 8 at 135.6 s. The final proof is 1,272,909 bytes whatever N (chain_len 8, agg_vk the pinned aggregator id), the record 586 bytes; `--mode verify-segment` on it: VERIFIED in 0.039, 0.037, 0.040 s after a 0.26-s light-verifier setup, the same three runs each time. GPU memory over the chain (152 one-second samples): max 16,751 MiB with the miner's 3.4 GB resident, so the chained aggregation holds about 13.4 GB, 1.2 GB over the shard-only peak; WSL used 2,456 MB |
Reading the chain numbers. Aggregation is a fixed cost per block (9.7 s here), not per segment: the recursion verifies one more proof whatever `chain_len`, so the record for N blocks costs N aggregations and the verifier one. Against 4 October with the miner stopped (aggregate 2.2 to 2.5 s, a 200-pgas shard 2.7 s), tonight's 9.7 s and 7.3 s say the miner's 92% utilisation slows the prover about 3 to 4x while the prover slows the miner 4%: the card is shared, and the lottery wins the arbitration. A machine that mines and proves at once delivers one empty block's proof and aggregation in 17 s; one that only proves, about 5 s (approximate, from the 4 October stages).
### Step 3, coverage
| What | Measured |
|---|---|
| A 3-minute window at 18:57Z on node 1 (`node tools/proving-v1/coverage.mjs --minutes 3`, chain blocks 80754..80839, 86 blocks) | 4 blocks with a paid shard (4.7%), 4 fully proven, 4 of 86 shards; on-chain latency (carrier timestamp minus block timestamp) n 4: min 36 s, p50 39 s, max 44 s; 0 content blocks. One prover (PC 2), the Mac verifier node down, PC 2 producing few blocks (3.4 MH/s): the degraded state above, not the fleet's number |
| A 30-minute window, 19:13 to 19:43Z, the degraded fleet (PC 2 the only prover, its 5090 worker restored at 19:16Z, the Mac app node down by decision: the Mac app is attached to node 1) | `node tools/proving-v1/coverage.mjs --minutes 30 --watch` on node 1: chain blocks 81236..82668, 1,433 blocks; 38 with a paid shard (2.7%), all 38 fully proven (one shard a block, 0 content blocks); on-chain latency n 38: min 36, p50 44, p90 52, p99 62, max 65 s. The live page's 10-minute proving object read 0 shards and 0 provers at 19:42Z (it counts what its own node verified; that node is the Mac app node, down), so the chain's own count is the number |
| A 30-minute window with the fleet mining (PC 2 at 119 MH/s from 19:16Z, PC 1 at 128.8 from 19:18Z; PC 2 still the only prover, its prover OFF for the 5 min of the cost job's phase A inside this window; the Mac app node down by decision) | `coverage.mjs --minutes 30 --watch`, 19:21 to 19:51Z on node 1: chain blocks 81644..83069, 1,426 blocks; 34 with a paid shard (2.4%), all fully proven (one shard a block, no content); on-chain latency n 34: min 38, p50 44, p90 51, p99 52, max 53 s. One 5090 through the app's loop as it is covers 2.4 to 2.7% of the blocks; the latency from block to carried record is 44 s at the median, under the litepaper's minute, and would be the same for every block if the fleet were 40 cards (the table below) |
### Step 3, the fleet size (arithmetic from measured inputs; every input names its entry)
Inputs, all RTX 5090 (PC 2), SP1 6.8.1 cuda: a full shard at the provisional `S_p` (6.75 M pgas) compressed in 10.9 s and the four shards of a near-`B_p` block in 10.2 to 10.7 s each (bench-log 4 October 2026, "shard proving on the RTX 5090", runs run-20261004-173115 and run-20261004-r3-shards); one aggregation 2.2 s (two shards) to 2.5 s (four shards), the same entry; tonight's chain of 2 on the Mac CPU shows the recursion over the previous block proof costs the same order as a first aggregation (52.0 s against 59.1 s), so the GPU figure for a chained aggregation is taken as 2.5 s, approximate, until the held PC 2 chain job measures it; the app's live loop tonight: 1.6 shards a minute per card on empty shards (export, cut, key setup, prove, sign, submit: about 37 s a shard, of which the proof is a few seconds), bench-log step 1 above. A 5090 proves one thing at a time.
| Block content at 1 block/s | Shard proofs a second (fleet) | Card-seconds a second for shards | Aggregations a second | Card-seconds a second for aggregation | 5090-class cards for 100% | Rule |
|---|---|---|---|---|---|---|
| empty blocks (tonight's devnet), the app's loop as it is, the card also mining | 1 | 37 | 1 | 9.7 (measured, `chain-pc2-pv1c`) | 47 | one shard per block, the loop's 37 s each plus a chained aggregation |
| empty blocks, the chain mode's shape (one key setup per process, proofs back to back), the card also mining | 1 | 7.4 (measured) | 1 | 9.7 (measured) | 18 | 17.1 card-seconds a block, `chain-pc2-pv1c` |
| empty blocks, cards that only prove | 1 | 2.7 (4 October, a 200-pgas shard) | 1 | 2.5 (4 October) | 6 (approximate) | the miner's 92% utilisation costs the prover 3 to 4x |
| one full shard a block (`S_p`, 6.75 M pgas), cards that only prove | 1 | 10.9 | 1 | 2.5 | 14 | 4 October's stages |
| one full shard a block, the card also mining | 1 | about 35 (approximate: 10.9 x 3.2, tonight's ratio) | 1 | 9.7 | about 45 (approximate) | the full-shard proof with the miner on the card is not measured |
| blocks at `B_p` (four full shards), cards that only prove | 4 | 42.5 | 1 | 2.5 | 45 | 4 x 10.6 + 2.5 |
| at the adopted v1 budgets (`B_p` 120,000 pgas, `S_p` 30,000, from DAA 210,000 on the devnet): a v1 shard of transfers ran at 213 to 236 cycles per pgas (bench-log 5 October, "the prover carries both fee tables"), 7 M cycles a shard against 60 M for the prototype shard | 4 | under 42.5 (the 5090 time for a 7 M-cycle shard is not measured; scaling 10.9 s by cycles gives about 1.3 s, approximate) | 1 | 2.5 to 9.7 | 8 to 15 (approximate) | measure before the switch lands |
Reading. The card count is the sum of card-seconds of work per block-second, rounded up, with no slack for the exclusive window, the relay or a card's idle gaps; the devnet's own numbers tonight (one card, 1.4 to 1.6 shards a minute, 2.4 to 4.7% of blocks) are the first row. Two levers, both measured tonight: the loop (a shard's carriage through export, cut and a 12-s key setup is 25 s on top of a 7-s proof; the host's `--mode aggregate` and `--mode chain` hold one key setup per process and the prover loop should do the same, the 0.3.11 item in the plan) and the card's other job (a mining card proves 3 to 4x slower than an idle one, `chain-pc2-pv1c` against 4 October; the prover's cost to mining is 4%). A fleet of 18 mining 5090s, or 6 proving-only ones, covers an empty-block chain at 1 block/s through the chain mode; the mandatory rule waits for the measured share to reach one, not for these rows.
### The 12 GB requirement (Josh, 20:1xZ: "make sure we can prove on 12gb cards"): the GPU memory peak against SP1's knobs
Job `memsweep-pc2-pv1` (`tools/proving-v1/pc2-memory-sweep.ps1`), PC 2's RTX 5090 (32,607 MiB), the miners STOPPED by the job and the live prover switched off (its `sp1-gpu-server` would otherwise be the one the client connects to), every row: the server killed first, a 1-s `nvidia-smi memory.used` sampler, one `--mode compressed --shard 0` run of the pv1 host (`/opt/igneum-pv1`, SP1 6.8.1 cuda, `sp1-gpu-server` 6.8.1), 20:19 to 20:25Z. The knobs are the environment the GPU server inherits from the host process (`sp1-core-executor-6.8.1/src/opts.rs`: `SHARD_SIZE`, `ELEMENT_THRESHOLD`, `HEIGHT_THRESHOLD`, `MINIMAL_TRACE_CHUNK_THRESHOLD`, `TRACE_CHUNK_SLOTS`; `sp1-prover-6.8.1/src/worker/config.rs`: the `SP1_WORKER_NUM_*` and `*_BUFFER_SIZE` counts, defaults 4 core workers, 8 recursion prover workers). Idle card before the sweep: 1,732 MiB.
| Config (environment) | Fixture | Cycles | Peak MiB | Compressed prove s | Verified |
|---|---|---|---|---|---|
| baseline (no knob) | block-338-shard1, a full shard at `S_p` (6.75 M pgas) | 60,415,376 | **28,295** | 11.4 | yes |
| baseline | block-83616, an empty live shard | 280,706 | **13,863** | 2.3 | yes |
| ELEMENT_THRESHOLD 2^27 | full shard | 60.4 M | 28,326 | 10.9 | yes |
| ELEMENT_THRESHOLD 2^26, HEIGHT_THRESHOLD 2^21 | full shard | 60.4 M | 28,326 | 10.7 | yes |
| every worker count and buffer 1 | full shard | 60.4 M | 28,326 | 20.8 | yes |
| every worker count and buffer 2 | full shard | 60.4 M | 28,327 | 12.9 | yes |
| workers 1 + ELEMENT 2^27 | full shard | 60.4 M | 28,263 | 20.3 | yes |
| workers 1 + ELEMENT 2^26 + HEIGHT 2^21 | full shard | 60.4 M | 28,326 | 20.6 | yes |
| workers 1 + ELEMENT 2^26 + HEIGHT 2^21 + trace chunks 4 M x 2 slots | full shard | 60.4 M | 28,358 | 22.6 | yes |
| workers 1 + ELEMENT 2^25 + HEIGHT 2^20 | full shard | 60.4 M | 22,919 | 22.2 | yes |
| workers 1 + ELEMENT 2^26 + HEIGHT 2^21 | empty shard | 280,706 | 13,861 | 2.6 | yes |
Reading. The GPU memory of a compressed shard proof is **13.9 GB for a shard of 280,000 cycles and 28.3 GB for one of 60 M cycles**, and no knob the environment carries moves the floor: the worker counts only slow the proof (11.4 s to 20.8 s), the trace thresholds at 2^26 and 2^27 change nothing, and the smallest trace threshold tried (2^25 elements, 2^20 rows) takes 5.4 GB off the full shard (22.9 GB) at twice the time. The floor sits in the GPU server's own allocation, not in the shard: an empty shard with every knob at its minimum still takes 13.9 GB. So on SP1 6.8.1's `sp1-gpu-server` as shipped, **a 12 GB card cannot prove even an empty shard** (13.9 GB), and the 11.0 GB target of tonight's requirement is out of reach from the environment. The S_p/2 and S_p/4 cuts of block 344 did not run: the package carries no `tools/prove-fixtures/seq.json` (the cut rows need the export; they would sit between the two measured points, and the floor is the binding number anyway). What is left to try, in order: the server's own options (its `--help` and the option names in its strings: the miner-on job prints them), SP1's core-only proof (the node needs the compressed proof, so this changes the protocol), and an SP1 release built for smaller cards (the 6.8.1 release notes are not read here; approximate: the project's documentation names 24 GB as the GPU requirement, `proving/windows-wsl2/setup-wsl.sh` quotes it).
### The same shard with the miner running (the 16 GB requirement), and the GPU server's own options
Job `memminer-pc2-pv1` (`tools/proving-v1/pc2-memory-miner-on.ps1`), 20:28 to 20:30Z, the miner at full rate on the card, the live prover off for the run, the same 1-s sampler: the full shard at `S_p` (60.4 M cycles) peaked at **30,039 MiB** and took 33.0 s (28,295 MiB and 11.4 s with the card to itself: the miner costs the prover 2.9x in time and 1.7 GB of memory); the empty shard **15,670 MiB** and 7.7 s (13,863 and 2.3 s alone). So a 32 GB card mines and proves the prototype shard with 2.5 GB to spare; a 24 GB card cannot prove it even alone (28.3 GB); a 16 GB card cannot hold even the empty shard beside the miner (15.7 GB, the display and driver on top). `sp1-gpu-server --help` prints only `--version`: it has no options of its own, and its strings carry no memory setting (`CUDA_OUT_OF_MEMORY` is an error name). The shard SIZE is therefore the only lever left on this build, measured next as the S_p curve.
The root-socket fault (the class, fixed the same evening). The chain and memory jobs ran the host as root inside WSL2; the first `sp1-gpu-server` they started left `/tmp/sp1-cuda-0.sock` owned by root, and the live prover (the app's own WSL user) then failed every shard with `CudaClientError: Connect(Os { code: 13, kind: PermissionDenied })` (PC 2 app log 1791230456, 20:00:56Z) until the socket was gone. Every pv1 playbook now kills the server and unlinks `/tmp/sp1-cuda-*.sock` at its start and end, `tools/ci/prover-socket-check.sh` fails CI on any playbook that runs a prove mode as root without both lines (shown failing on `pc2-prover-cost.ps1` before its `--mode id`-only exemption, passing after), and the plan carries the rule: a prover job on a shared card runs as the app's user or cleans its socket. It recurred at 21:25Z from another agent's job (agg-cost-pc2-1, the same root-run shape) and survived the 0.3.10 restart at 21:49:41Z; the fix job `socketfix-pc2-pv1` (`tools/proving-v1/pc2-socket-fix.ps1`, 22:01:14 to 22:02:12Z) found `/tmp/sp1-cuda-0.sock` owned by root, removed it, switched the prover off and on, and the app's next shard (block 89011 shard 0) was proven and submitted in 34 s and paid 0.93 IGN at 22:02:24Z. Playbooks that run the host: `tools/proving-v1/pc2-chain.ps1`, `pc2-memory-sweep.ps1`, `pc2-memory-miner-on.ps1`, `pc2-sp-curve.ps1` (all root, all with the cleanup now; the first two chain and sweep runs had none), `pc2-prover-cost.ps1` (`--mode id` only), `relay/playbooks/shard-test.ps1` and `proving/windows-wsl2/prove-shard.sh`, `prove-block.sh` (the app's user, not root), `tools/proving-v0/run.mjs` (the Mac, no server).
### The S_p curve: peak GPU memory against shard size against time, the card to itself (the first of the two curve jobs)
Job `spcurve-stopped-pc2-pv1` (`tools/proving-v1/pc2-sp-curve.ps1`, the miners stopped by the job, the live prover off, the server killed and its socket unlinked around every point, a 1-s `nvidia-smi` sampler), 20:33 to 20:37Z, PC 2's RTX 5090, the pv1 host (this run's `--budget` points were ignored by the pv1 host, so its block-344 rows are the fixture's own 6.75 M-pgas shard 0 twice; the pv1b host's re-plans at 2.25 M and 4.5 M pgas are the next job's rows). Idle card 1,743 MiB.
| Shard | pgas | Witness bytes | SP1 cycles | Peak MiB, card alone | Compressed prove s | Knob |
|---|---|---|---|---|---|---|
| block 83616, an empty live shard | 0 | 13,964 | 280,706 | **13,874** | 2.2 | none |
| block 56, one transfer | 600 | 4,902 | 556,369 | 13,907 | 3.2 | none |
| fees-v1-shards2 shard 0, a shard at the ADOPTED v1 budget (`S_p` 30,000; 4 transactions, 2 shards a block) | 22,172 | 18,390 | 4,717,439 | **20,434** | 4.3 | none |
| the same | 22,172 | 18,390 | 4.7 M | 20,435 | 3.7 | ELEMENT_THRESHOLD 2^25, HEIGHT 2^20 |
| block 338 shard 0, the full PROTOTYPE shard (`S_p` 7.5 M) | 6,751,568 | 21,611 | 60,415,376 | **28,307** | 10.8 | none |
| the same | 6.75 M | 21,611 | 60.4 M | 22,963 | 11.5 | ELEMENT_THRESHOLD 2^25, HEIGHT 2^20 |
| block 344 shard 0 (the fixture's own cut, 6.75 M pgas, modexp) | 6,748,392 | 18,535 | 59,678,420 | 28,275 and 28,307 | 11.5 and 11.0 | none |
The second job (`spcurve-stopped-pc2-pv1b`, the pv1b host whose `--budget` re-plans a fixture, 20:43 to 20:47Z, the same conditions) repeats the points (empty 13,875 MiB 2.1 s; one transfer 13,907 MiB 3.3 s; the v1 shard 20,435 MiB 4.2 s; the prototype shard 28,275 MiB 11.2 s) and adds the re-plans of block 344 (27 M pgas of modexp): at 2.25 M pgas (one transaction, 16 shards a block, 19,987,938 cycles) **28,371 MiB** and 6.6 s; at 4.5 M pgas (7 shards a block, 40,011,108 cycles) 28,307 MiB and 8.5 s; with the 2^25 trace threshold the 2.25 M shard 22,835 MiB and 6.2 s. So the peak is flat at 28.3 GB from 20 M cycles to 60 M (the server's buffers step up between 4.7 M and 20 M cycles and not after), and cutting the prototype shard smaller buys nothing until the v1 size.
The third job (`spcurve-miner-pc2-pv1`, the same points WITH THE MINER RUNNING on the card, 20:49Z on, the live prover off): empty shard 15,585 MiB and 7.5 s; one transfer 15,745 MiB and 12.7 s; **the v1 shard 22,210 MiB and 13.2 s** (20,435 and 4.2 s alone: the miner adds 1.8 GB and 3.1x); the 2.25 M shard 30,049 MiB and 17.9 s; the 4.5 M shard 29,954 MiB and 26.3 s; the prototype shard 30,083 MiB and 33.3 s. With the 2^25 trace threshold beside the miner: the 2.25 M shard 24,642 MiB and 21.5 s, the prototype shard 24,739 MiB and 38.8 s (24.7 GB: over a 24 GB card by the display's share, and 3.6x slower than the card alone). So beside the miner the adopted shard needs 22.2 GB: a 24 GB card (24,564 MiB) has 2.3 GB spare for it (the number for a 24 GB card is the 5090's allocation pattern on a 32 GB card, so approximate for the card itself), and the prototype shard needs 30.1 GB, the 32 GB card alone.
Reading, with the miner-on pairs above (empty shard 15,670 MiB, full prototype shard 30,039 MiB). The witness is never the binding term (4.9 to 21.6 KB a shard); the GPU server's working set is: a floor of 13.9 GB for any shard, 20.4 GB at 4.7 M cycles, 28.3 GB at 60 M cycles (23.0 GB with the smallest trace threshold, at the same time). By card: a **12 GB card proves nothing** on this build (the floor is 13.9 GB alone); a **16 GB card proves only empty and near-empty shards, alone** (13.9 GB; 15.7 GB beside the miner leaves nothing for the display); a **24 GB card proves the adopted v1 shard alone** (20.4 GB) and, at the miner's measured 1.7 GB extra, about 22.1 GB beside it (approximate: not measured on a 24 GB card), and never the prototype shard (28.3 GB); a **32 GB card proves the prototype shard beside the miner with 2.5 GB spare** (30.0 of 32.6 GB). The devnet is on the prototype table until H = 210,000 (6 October, about 19:50Z) and on the adopted v1 table (`S_p` 30,000 pgas) after it, so from H the 24 GB tier joins the provers and the shard that binds the memory is the 4.7 M-cycle one. Shards per block at each size: 1 at the prototype `S_p`, 4 at `B_p`; at the v1 budget 1 to 4 (one a block on tonight's chain, 2 to 3 on the txgen blocks).
### Step 4, the rule
| What | Measured |
|---|---|
| Unit tests | `cargo test --release -p kaspa-consensus-core -p igneum-exec --lib -- proving config::params::tests::override_params_carry_the_proving_v1 config::params::tests::consensus_digest` on this Mac (target `vendor/igneum-node/target-pv1`, 19:09Z): consensus core 13 passed (the segment record round trip, signature and the three nested sections; the credit split; the params switch and the digest that moves only once the switch is set), exec 8 passed (the segment grid and the split; the record checks: alignment, block, chain length, the veto naming the field, the deadline, the window; the chain rule both ways; the unproven restart; the shard side at 90%; the pool offering the segment section). The six full node suites go to PC 2 as a build job when the fleet is back |
| The fast-time 3-node harness (`tools/proving-v1/net.mjs`, 29950+, suffix 956, every node in trust mode, three vmine voters, v0 at DAA 60, v1 at DAA 120, 4 blocks a segment, unproven after 60 DAA, a tenth to the aggregator; fork b177718e built on this Mac) | run 2, 19:13:01Z to 19:16:19Z, under the run lock: PASSED, 21 checks in 197.3 s (`tools/proving-v1/report-2026-10-05.json`). v1 start = chain block 119 on all three nodes; the native statement identical on all three. Known-finished: segment 119..122's fresh-chain record submitted to n1 at t=131.1 s, relayed, verified (trust) and PAID on n0 1.0 s later at chain block 129, 253,611,648,000,000,000 wei = a tenth of the four credits, the same on every node, the payout address holding it. Chain rule: segment 123..126's fresh-chain record refused ("does not chain to segment 119..122 ... proven (record paid at chain block 129)"), the continuing one (chain_len 8) accepted and paid. Known-failed: segment 127..130 left without a record: a fresh-chain record for 131..134 refused while 127..130 was pending ("pending until DAA 191"); at DAA 192 the status read unproven, a late record for 127..130 refused ("unproven: carried after the deadline"), the fresh-chain record for 131..134 accepted and paid with chain_len 4; `segmentsInWindow` proven 3, unproven 1. The shard side: a v1 shard's `shardWei` = 90% of its block's credit. Run 1 (19:10Z) failed in its own tooling (the signer's argument order), fixed. Run 3 on the FINAL fork tree (ece42979 on the 0.3.10 commit 21d4c73c, protocol 15, N = 8 both in the params default and `--segment 8`, the fast-time file's four fields), 20:52:41Z to 20:56:45Z: PASSED, 21 checks in 244.4 s (segments of 8: 119..126 paid in 1.0 s after submission, 127..134 refused fresh and paid continuing with chain_len 16, 135..142 left unproven and skipped, 143..150 restarted the chain) |

View file

@ -39,8 +39,8 @@ Versions in the table: `igneum-pow` is the Rust crate at `igneum-pow/Cargo.toml`
| 12 | The difficulty rule recovers from a hashrate step within minutes, where Kaspa's sampled rule never settles. A step inside an epoch set the rule oscillating on the live devnet on 4 October 2026; rule v2 removes it in the simulator and on a test network and is built but not yet rolled out | Spec 2.3; litepaper Speed (implied); bench page | tested by the team | repo `e9328c6`, `abb5a5d` (attacks), `67bf226` (rule v2); fork `difficulty` branch (timestamp fix) and `devnet-v4` `a21ff239` (`difficulty_v2_activation_daa`, `REF_WINDOW_V2 = 600`); `sim/difficulty/sim.py --live` | The live record `sim/difficulty/records/live-2026-10-04.csv` (8,090 headers, `pull_live.py`) and the hash-rate record beside it; `sim/difficulty/sim.py` on the synthetic set and the DAG replay; `sim/difficulty/attacks/attacks.py`; `sim/difficulty/testnet_v2.py` (3 nodes, activation at DAA 900); `cargo test --release -p kaspa-consensus --lib difficulty` (15 pass); bench-log "difficulty controller", "difficulty rule under attack", "timestamp attack fixed", "difficulty rule v2" | Live devnet v4, 4 October 2026 (UTC): a second RTX 5090 joining 7 minutes into an epoch (about 152 to 280 MH/s) hardened the difficulty 70M to 144M in 90 s and then swung by about a third for 40 minutes around the true level of 139M while the epoch-long reference lane carried the join; that card leaving for 4 minutes eased 116M to 67M and back to 106M; the epoch boundary with both PCs restarting took 152M to 77M in 3 minutes, after which the rule held within 1.3% per minute with no flips. Cause: the reference lane covered the whole epoch, so a mid-epoch step polluted it for the hour and the 25% trigger flipped on the short lane's noise. The DAG replay reproduces the record (std of log difficulty 0.115 against 0.134, 4.3 peaks against 4). Rule v2 (reference window 600 DAA) on the replay: std 0.026, 0 flips, mean 142.6M against 139M true; on a 3-node test network the v2 nodes eased a leave with no peak and held a rejoin within 3% after 60 s, and a node without the activation height forked off at it as designed. Rule v2 rolled onto the 12-node cloud network on 4 October (all nodes crossed the height on one chain; a hash-rate step then settled in 160 to 270 s with no swing) and activates on the devnet at DAA 33,000 the same evening. Timestamp forging (ledger M23) fixed the same day: a 50% forger drifts the rate under 1.1% where the 3 October rule gave it a 9.9x difficulty. Simulator, settled seconds: x50 step 62 to 66 (Kaspa 1,542), /50 step 657 to 753 (Kaspa 12,296). Apple M5 Max under load 7 to 442; the DAG model is fitted on one scale; the pool hopper's 0.7-point excess over Kaspa's rule stays open | none yet | | 12 | The difficulty rule recovers from a hashrate step within minutes, where Kaspa's sampled rule never settles. A step inside an epoch set the rule oscillating on the live devnet on 4 October 2026; rule v2 removes it in the simulator and on a test network and is built but not yet rolled out | Spec 2.3; litepaper Speed (implied); bench page | tested by the team | repo `e9328c6`, `abb5a5d` (attacks), `67bf226` (rule v2); fork `difficulty` branch (timestamp fix) and `devnet-v4` `a21ff239` (`difficulty_v2_activation_daa`, `REF_WINDOW_V2 = 600`); `sim/difficulty/sim.py --live` | The live record `sim/difficulty/records/live-2026-10-04.csv` (8,090 headers, `pull_live.py`) and the hash-rate record beside it; `sim/difficulty/sim.py` on the synthetic set and the DAG replay; `sim/difficulty/attacks/attacks.py`; `sim/difficulty/testnet_v2.py` (3 nodes, activation at DAA 900); `cargo test --release -p kaspa-consensus --lib difficulty` (15 pass); bench-log "difficulty controller", "difficulty rule under attack", "timestamp attack fixed", "difficulty rule v2" | Live devnet v4, 4 October 2026 (UTC): a second RTX 5090 joining 7 minutes into an epoch (about 152 to 280 MH/s) hardened the difficulty 70M to 144M in 90 s and then swung by about a third for 40 minutes around the true level of 139M while the epoch-long reference lane carried the join; that card leaving for 4 minutes eased 116M to 67M and back to 106M; the epoch boundary with both PCs restarting took 152M to 77M in 3 minutes, after which the rule held within 1.3% per minute with no flips. Cause: the reference lane covered the whole epoch, so a mid-epoch step polluted it for the hour and the 25% trigger flipped on the short lane's noise. The DAG replay reproduces the record (std of log difficulty 0.115 against 0.134, 4.3 peaks against 4). Rule v2 (reference window 600 DAA) on the replay: std 0.026, 0 flips, mean 142.6M against 139M true; on a 3-node test network the v2 nodes eased a leave with no peak and held a rejoin within 3% after 60 s, and a node without the activation height forked off at it as designed. Rule v2 rolled onto the 12-node cloud network on 4 October (all nodes crossed the height on one chain; a hash-rate step then settled in 160 to 270 s with no swing) and activates on the devnet at DAA 33,000 the same evening. Timestamp forging (ledger M23) fixed the same day: a 50% forger drifts the rate under 1.1% where the 3 October rule gave it a 9.9x difficulty. Simulator, settled seconds: x50 step 62 to 66 (Kaspa 1,542), /50 step 657 to 753 (Kaspa 12,296). Apple M5 Max under load 7 to 442; the DAG model is fitted on one scale; the pool hopper's 0.7-point excess over Kaspa's rule stays open | none yet |
| 13 | Every node executes the ordered transactions natively and reaches the same state root | Litepaper Proving ("Every node executes ... natively"), Building ("runs on Igneum unchanged") | tested by the team | repo `f5f8c80`, `8dae48b`; fork `devnet-v4` `dc749905`; revm 43.0.3 | `node tools/evm-smoke/smoke.mjs` against a 3-node `igneumd`; `igneum-exec-diff seq.json`; bench-log "execution layer devnet v3" and "devnet-v4 integration" | Simnet, 3 October 2026: 87 of 87 viem checks, state roots identical on 3 nodes at four heights, 57 executed and 19 skipped transactions agree with plain revm, 0 mismatches. Merged node on real proof of work, 4 October 2026: 84 of 85 checks (the miss needs parallel blocks the network did not produce in 36 s), 59 transfers in 10 chain blocks, state roots identical on 3 nodes, `igneum-exec-diff` 0 mismatches over 59 transactions; the live devnet v4 runs this execution layer. Apple M5 Max. The prover is a stub; state is rebuilt from genesis at start; no EVM transaction relay between nodes | none yet | | 13 | Every node executes the ordered transactions natively and reaches the same state root | Litepaper Proving ("Every node executes ... natively"), Building ("runs on Igneum unchanged") | tested by the team | repo `f5f8c80`, `8dae48b`; fork `devnet-v4` `dc749905`; revm 43.0.3 | `node tools/evm-smoke/smoke.mjs` against a 3-node `igneumd`; `igneum-exec-diff seq.json`; bench-log "execution layer devnet v3" and "devnet-v4 integration" | Simnet, 3 October 2026: 87 of 87 viem checks, state roots identical on 3 nodes at four heights, 57 executed and 19 skipped transactions agree with plain revm, 0 mismatches. Merged node on real proof of work, 4 October 2026: 84 of 85 checks (the miss needs parallel blocks the network did not produce in 36 s), 59 transfers in 10 chain blocks, state roots identical on 3 nodes, `igneum-exec-diff` 0 mismatches over 59 transactions; the live devnet v4 runs this execution layer. Apple M5 Max. The prover is a stub; state is rebuilt from genesis at start; no EVM transaction relay between nodes | none yet |
| 14 | Ethereum bytecode runs unchanged, with the documented differences of spec 7.1 | Homepage Build card; litepaper Building | tested by the team | as row 13; fixes `F-exec-A`, `F-exec-B` (spec 7.5) | `tools/evm-smoke/smoke.mjs`: deploy via viem, `increment`, `hashLoop`, `eth_estimateGas`, `eth_getLogs`; `tools/exec-attacks` scenarios 1 and 3; bench-log "execution layer attack fixes" | Deployment, calls, reverts, logs and gas estimates behave as viem expects; chain id 4463; the prototype pgas table gives 0.0095 to 0.028 pgas per gas, below the design's band before calibration, 3 October 2026. 4 October 2026: a transaction that would cross the block's proving budget is refused by the mempool and, if forced in, aborted and charged with its nonce advanced (25 of 25 checks; 30 of 30 malformed cases). Apple M5 Max. The `Prover` precompile, proof records and the shard planner are not in the node | none yet | | 14 | Ethereum bytecode runs unchanged, with the documented differences of spec 7.1 | Homepage Build card; litepaper Building | tested by the team | as row 13; fixes `F-exec-A`, `F-exec-B` (spec 7.5) | `tools/evm-smoke/smoke.mjs`: deploy via viem, `increment`, `hashLoop`, `eth_estimateGas`, `eth_getLogs`; `tools/exec-attacks` scenarios 1 and 3; bench-log "execution layer attack fixes" | Deployment, calls, reverts, logs and gas estimates behave as viem expects; chain id 4463; the prototype pgas table gives 0.0095 to 0.028 pgas per gas, below the design's band before calibration, 3 October 2026. 4 October 2026: a transaction that would cross the block's proving budget is refused by the mempool and, if forced in, aborted and charged with its nonce advanced (25 of 25 checks; 30 of 30 malformed cases). Apple M5 Max. The `Prover` precompile, proof records and the shard planner are not in the node | none yet |
| 15 | Every block is proven, with the proof landing within about a minute at launch | Homepage stats ("~60 s to a proof"); litepaper Proving; roadmap phase 3 gate | implemented | repo `d7e1f89` (GPU proof), `e01a3cc`, `292e800`, `eedd136` (`proving/igneum-prove`: shard cutter, MPT witnesses, shard and aggregator guests); SP1 6.8.1; spec 7.2, 7.6 | `proving/windows-wsl2` (SETUP-PROVER, PROVE-BLOCK) on the RTX 5090; `igneum-prove-host --mode block` on `proving/fixtures/`; bench-log "proving v0 on the RTX 5090" and "proving: devnet v4 shards" | First GPU proof of an Igneum block, 4 October 2026, RTX 5090 (WSL2, SP1 cuda, mining paused): fixture `block-78-increment` (2 transactions), core proof 1.4 s (7.3 MB, verify 0.221 s), compressed proof 2.7 s (1.27 MB, verify 0.038 s), post-state and receipts roots identical to the node's; 15.7x and 20.6x faster than a loaded M5 Max CPU. The same day on that CPU (load 38 to 47): a three-shard block proved shard by shard and aggregated by recursion, 19 min (1,139 s) end to end, 245 to 337 s per compressed shard proof, every proof verified. What is not there: no proof is produced, carried or checked on the chain (the devnet prover is a stub that signs claims), the proving pool pays nobody (row 21), the block proven is far below one shard, and the 60-second figure remains a design target; the pass mark is the standard in `docs/benchmarks/proving-e2e.md`. Second RTX 5090 run, 4 October 2026 evening (job run-20261004-173115): a full shard at the provisional S_p (6.75 M pgas, 60.8 M cycles) executed in 1.63 s, core proof 8.3 s (18.1 MB), compressed proof 10.9 s (1.27 MB, verify 0.040 s); a two-shard block (13.5 M pgas) proved shard by shard (11.7 s and 10.0 s) and aggregated in 2.2 s, 24 s of GPU stages end to end, every proof verified, six tampered witnesses rejected. The two host defects (an abort after the upload, an idle wait that turned out to be an unbuffered 18 MB proof save through the WSL2 file bridge, 24 minutes) are fixed (ledger P20) 5 October 2026, live devnet with real transactions (bench-log "real transactions, the first non-empty shard proven and paid"): block 72704 shard 0, 29 transfers, 5,800 pgas, proven on PC 2 in 34 s, verified on the Mac in 0.297 s and paid 1.7623 IGN, 53 s after the chain block executed; of about 1,400 blocks in the 20-minute window 36 were proven (the one prover takes the newest shard assigned to it), so "every block" is not yet true; a second content shard (72803, all copies skipped) failed the native-execution veto on the exporter's block structure, fixed with fixtures the same day, the node side pending the 0.3.9 rollout | none yet | | 15 | Every block is proven, with the proof landing within about a minute at launch | Homepage stats ("~60 s to a proof"); litepaper Proving; roadmap phase 3 gate | implemented | repo `d7e1f89` (GPU proof), `e01a3cc`, `292e800`, `eedd136` (`proving/igneum-prove`: shard cutter, MPT witnesses, shard and aggregator guests); SP1 6.8.1; spec 7.2, 7.6 | `proving/windows-wsl2` (SETUP-PROVER, PROVE-BLOCK) on the RTX 5090; `igneum-prove-host --mode block` on `proving/fixtures/`; bench-log "proving v0 on the RTX 5090" and "proving: devnet v4 shards" | First GPU proof of an Igneum block, 4 October 2026, RTX 5090 (WSL2, SP1 cuda, mining paused): fixture `block-78-increment` (2 transactions), core proof 1.4 s (7.3 MB, verify 0.221 s), compressed proof 2.7 s (1.27 MB, verify 0.038 s), post-state and receipts roots identical to the node's; 15.7x and 20.6x faster than a loaded M5 Max CPU. The same day on that CPU (load 38 to 47): a three-shard block proved shard by shard and aggregated by recursion, 19 min (1,139 s) end to end, 245 to 337 s per compressed shard proof, every proof verified. What is not there: no proof is produced, carried or checked on the chain (the devnet prover is a stub that signs claims), the proving pool pays nobody (row 21), the block proven is far below one shard, and the 60-second figure remains a design target; the pass mark is the standard in `docs/benchmarks/proving-e2e.md`. Second RTX 5090 run, 4 October 2026 evening (job run-20261004-173115): a full shard at the provisional S_p (6.75 M pgas, 60.8 M cycles) executed in 1.63 s, core proof 8.3 s (18.1 MB), compressed proof 10.9 s (1.27 MB, verify 0.040 s); a two-shard block (13.5 M pgas) proved shard by shard (11.7 s and 10.0 s) and aggregated in 2.2 s, 24 s of GPU stages end to end, every proof verified, six tampered witnesses rejected. The two host defects (an abort after the upload, an idle wait that turned out to be an unbuffered 18 MB proof save through the WSL2 file bridge, 24 minutes) are fixed (ledger P20) 5 October 2026, live devnet with real transactions (bench-log "real transactions, the first non-empty shard proven and paid"): block 72704 shard 0, 29 transfers, 5,800 pgas, proven on PC 2 in 34 s, verified on the Mac in 0.297 s and paid 1.7623 IGN, 53 s after the chain block executed; of about 1,400 blocks in the 20-minute window 36 were proven (the one prover takes the newest shard assigned to it), so "every block" is not yet true; a second content shard (72803, all copies skipped) failed the native-execution veto on the exporter's block structure, fixed with fixtures the same day, the node side pending the 0.3.9 rollout 5 October 2026, evening (bench-log "proving v1"): the aggregated segment record, the chain rule and the unproven rule are implemented behind `proving_v1_activation_daa` (branch proving-v1, not on the devnet before 0.3.11); on the RTX 5090 a chain of 8 consecutive live blocks proved and aggregated by recursion in 135.6 s with the miner on the card (17 s a block, one proof of 1,272,909 bytes attesting all 8, verified in 0.04 s); the 3-node fast-time harness paid a segment record 1.0 s after submission and refused a late one after its deadline (21 checks); the devnet itself, with one prover, carried proofs for 2.4% of blocks over 30 minutes at a block-to-record latency p50 44 s, p99 52 s. The "within about a minute" holds per proven block; "every block" needs 18 mining 5090s or 6 proving-only cards at empty blocks on the measured rates, and the mandatory rule stays off until the share is one | none yet |
| 16 | A 12 GB card proves one shard in about 20 s | Litepaper Proving ("The proving budget"); roadmap gate 2 | designed | spec 5.1 (Target), 7.6 (`S_p` provisional, 7,500,000 pgas = `B_p` / 4) | `PROVE-SHARD.bat` on the RTX 5090 (pending); the end-to-end standard in `docs/benchmarks/proving-e2e.md`; bench-log "proving: devnet v4 shards" | Measured on a 32 GB card, not yet on a 12 GB card. A shard at the provisional `S_p` is 60.8 M SP1 cycles on the prototype pgas table (9 cycles per pgas, 44 per EVM gas; the modexp entry about 100x its SP1 cost); on an RTX 5090 (4 October 2026 evening, job run-20261004-173115) it executed in 1.63 s and its compressed proof took 10.9 s, verified in 0.040 s, so the 32 GB card is inside the 20 s target with margin. Whether a 12 GB card proves it at all, and in what time, is the next measurement (an RTX 3060 and an RTX 5060 Ti 16 GB are on order). A per-shard time can be met by shrinking the shard, so the project does not use it as a pass mark | none yet | | 16 | A 12 GB card proves one shard in about 20 s (WITHDRAWN 5 October 2026: a 24 GB card proves a full shard at the adopted size in 4.3 s; 32 GB mines and proves) | Litepaper Proving ("The proving budget"); roadmap gate 2 | designed | spec 5.1 (Target), 7.6 (`S_p` provisional, 7,500,000 pgas = `B_p` / 4) | `PROVE-SHARD.bat` on the RTX 5090 (pending); the end-to-end standard in `docs/benchmarks/proving-e2e.md`; bench-log "proving: devnet v4 shards" | Measured on a 32 GB card, not yet on a 12 GB card. A shard at the provisional `S_p` is 60.8 M SP1 cycles on the prototype pgas table (9 cycles per pgas, 44 per EVM gas; the modexp entry about 100x its SP1 cost); on an RTX 5090 (4 October 2026 evening, job run-20261004-173115) it executed in 1.63 s and its compressed proof took 10.9 s, verified in 0.040 s, so the 32 GB card is inside the 20 s target with margin. Whether a 12 GB card proves it at all, and in what time, is the next measurement (an RTX 3060 and an RTX 5060 Ti 16 GB are on order). A per-shard time can be met by shrinking the shard, so the project does not use it as a pass mark 5 October 2026, evening (bench-log "proving v1", the S_p curve): measured on the RTX 5090 with SP1 6.8.1's GPU prover, the card to itself, 1-s nvidia-smi samples: an empty shard 13,874 MiB and 2.2 s; a full shard at the ADOPTED v1 budget (30,000 pgas, 4.7 M cycles) 20,434 MiB and 4.3 s; the full prototype shard (6.75 M pgas, 60 M cycles) 28,307 MiB and 10.8 s; beside the miner 15,670 and 30,039 MiB. No environment knob of SP1 moves the 13.9 GB floor and the GPU server has no options of its own, so on this build a 12 GB card proves nothing, a 16 GB card only empty shards, a 24 GB card the adopted full shard alone and beside the miner (22,210 MiB and 13.2 s, measured on the 32 GB card: the 5090's allocation pattern, not yet a run on a 24 GB card) and a 32 GB card the prototype shard beside the miner with 2.5 GB spare. The litepaper line now says so; the 12 GB gate returns when a prover build with a smaller floor is measured on a 12 GB card | none yet |
| 17 | The chip resistance target: a chip gains under 2x over a GPU | Homepage hero and litepaper abstract ("a custom chip gains under 2x, and the model and the bounty are public"), litepaper "What Igneum does not claim" | tested by the team (the model), designed (the target) | program class v3 (Counter ASIC 2.0, 5 October 2026): branches ca2-v3 d233fa1 and after, ca2-mixer 1ab8b21, ca2-era 78c0ee4; `docs/analysis/chip-model-v3.md`, `docs/analysis/sram-mirror.md`, `docs/analysis/scratch-soundness.md` | The m16 recompute model re-run on the measured v3 rates and verifier times; the on-die-cache chip row | The on-die-cache recompute chip against the RTX 5090's measured 136.1 MH/s: class v2 2.4x; class v3 (mixer x8) 0.31x bare, 0.92x with a 3x fixed-function allowance (approximate), 0.76x at equal silicon; margin 8% on the allowance, 9% on the budget. 5 October 2026, M5 Max, RTX 5090, RX 9070 XT. The 2x target is a target: no chip has been built; the bounty stands (O-1.17) | none yet | | 17 | The chip resistance target: a chip gains under 2x over a GPU | Homepage hero and litepaper abstract ("a custom chip gains under 2x, and the model and the bounty are public"), litepaper "What Igneum does not claim" | tested by the team (the model), designed (the target) | program class v3 (Counter ASIC 2.0, 5 October 2026): branches ca2-v3 d233fa1 and after, ca2-mixer 1ab8b21, ca2-era 78c0ee4; `docs/analysis/chip-model-v3.md`, `docs/analysis/sram-mirror.md`, `docs/analysis/scratch-soundness.md` | The m16 recompute model re-run on the measured v3 rates and verifier times; the on-die-cache chip row | The on-die-cache recompute chip against the RTX 5090's measured 136.1 MH/s: class v2 2.4x; class v3 (mixer x8) 0.31x bare, 0.92x with a 3x fixed-function allowance (approximate), 0.76x at equal silicon; margin 8% on the allowance, 9% on the budget. 5 October 2026, M5 Max, RTX 5090, RX 9070 XT. The 2x target is a target: no chip has been built; the bounty stands (O-1.17) | none yet |
| 18 | The chip resistance measurements: the program is latency-bound (random reads), not bandwidth-bound, on every card we own, and sits beyond a card's on-chip cache | Litepaper Mining ("waits on memory latency, not on maths or bandwidth"), vs RandomX; the numbers page | tested by the team | readwidth e752fc7 (`docs/plans/read-width.md`), ca2-era 78c0ee4, ca2-cache 2de19e5 (`docs/plans/hot-table.md`) | The dependent-read probes at 32 to 1,024 MiB and the hash rate per class on the three cards; the latency-bound share = rate over the probe ceiling per load | Latency-bound share at the 1 GiB dataset: RTX 5090 0.96 (v2) and 1.01 (v3), RX 9070 XT 0.87 and 0.95, M5 Max 1.01 and 1.06; wider reads do not close the AMD gap (the 9070 XT does 2.4 G dependent reads per second at every width; the 5090 goes bandwidth-bound at 64 B, share 0.58); a 32 to 96 MiB hot table is not kept resident by any card while the dataset streams (g 0.80 to 0.87 in the added form). 5 October 2026 | none yet | | 18 | The chip resistance measurements: the program is latency-bound (random reads), not bandwidth-bound, on every card we own, and sits beyond a card's on-chip cache | Litepaper Mining ("waits on memory latency, not on maths or bandwidth"), vs RandomX; the numbers page | tested by the team | readwidth e752fc7 (`docs/plans/read-width.md`), ca2-era 78c0ee4, ca2-cache 2de19e5 (`docs/plans/hot-table.md`) | The dependent-read probes at 32 to 1,024 MiB and the hash rate per class on the three cards; the latency-bound share = rate over the probe ceiling per load | Latency-bound share at the 1 GiB dataset: RTX 5090 0.96 (v2) and 1.01 (v3), RX 9070 XT 0.87 and 0.95, M5 Max 1.01 and 1.06; wider reads do not close the AMD gap (the 9070 XT does 2.4 G dependent reads per second at every width; the 5090 goes bandwidth-bound at 64 B, share 0.58); a 32 to 96 MiB hot table is not kept resident by any card while the dataset streams (g 0.80 to 0.87 in the added form). 5 October 2026 | none yet |
| 19 | The lottery hash is sound as a hash: uniform output, deterministic, no out-of-bounds read, fuzzed; class v3 bit-exact on the three vendors | Litepaper vs RandomX ("Every number above is measured and logged"), the numbers page | tested by the team | ca2-mixer 1ab8b21 (`tests/mixer.rs`, `tests/scratch.rs`), ca2-era 78c0ee4, ca2-soundness a465881 (`docs/analysis/scratch-soundness.md`), `igneum-pow/tests/packs.rs` | The crate suite (53 + 4 + 19 + 7), the Metal fuzz, edge, stats and determinism runs on the v3 construction, the pack vectors and 2^24 fingerprints on Metal, Apple OpenCL, the RTX 5090 and the RX 9070 XT, the 1,024-hash CPU re-check per card | Class v3 (mixer x8 + era): 200-program fuzz 200 of 200 on Metal, every tenth on Apple OpenCL; the pinned v3 packs 3/3 + 3/3 and 96 of 96 lanes on Metal and Apple OpenCL; the six era packs' fingerprints equal on the three vendors (PC 1 job run-ca2-era-pc1-20261005, 5 October 2026); the v2 exports byte-identical on the v3 crate; the final-class PC rows and the G2 re-check: job run-ca2-era-pc1b-20261005 (pending at the time of writing) | none yet | | 19 | The lottery hash is sound as a hash: uniform output, deterministic, no out-of-bounds read, fuzzed; class v3 bit-exact on the three vendors | Litepaper vs RandomX ("Every number above is measured and logged"), the numbers page | tested by the team | ca2-mixer 1ab8b21 (`tests/mixer.rs`, `tests/scratch.rs`), ca2-era 78c0ee4, ca2-soundness a465881 (`docs/analysis/scratch-soundness.md`), `igneum-pow/tests/packs.rs` | The crate suite (53 + 4 + 19 + 7), the Metal fuzz, edge, stats and determinism runs on the v3 construction, the pack vectors and 2^24 fingerprints on Metal, Apple OpenCL, the RTX 5090 and the RX 9070 XT, the 1,024-hash CPU re-check per card | Class v3 (mixer x8 + era): 200-program fuzz 200 of 200 on Metal, every tenth on Apple OpenCL; the pinned v3 packs 3/3 + 3/3 and 96 of 96 lanes on Metal and Apple OpenCL; the six era packs' fingerprints equal on the three vendors (PC 1 job run-ca2-era-pc1-20261005, 5 October 2026); the v2 exports byte-identical on the v3 crate; the final-class PC rows and the G2 re-check: job run-ca2-era-pc1b-20261005 (pending at the time of writing) | none yet |

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

@ -0,0 +1,143 @@
# Proving v1: segment records, the chain rule, the unproven rule; the 0.3.11 rollout
5 October 2026, from 18:55 UTC (Josh: "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 75 at protocol 15 (14 went to the EVM transaction relay in 0.3.10), 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 Josh's decisions | Written below |
## Numbers (every one from `docs/bench-log.md`, "proving v1: segment records ...", 5 October 2026 evening)
| What | Number |
|---|---|
| The prover's cost to a mining 5090 (the re-run with the fleet mining, hash rate from the miner's own STATUS lines) | 124.7 MH/s alone, 119.7 MH/s with the prover on: 5.0 MH/s, 4.0%, on empty shards at 1.4 a minute |
| GPU memory on the 5090: the prover alone (empty shards) / the miner and the prover together | max 13,816 MiB / max 15,590 MiB (the miner holds 3,396 MiB); a 24 GB 4090 has 8.4 GB of headroom, a 16 GB card 0.4 GB, a 12 GB card cannot do both on this build; the full-shard peak is the chain job's row |
| Coverage, 30-min window with the fleet mining, one prover | 2.4% of blocks, latency p50 44 s, p99 52 s |
| Host RAM | host used 25.6 GB of 63 GB; the WSL2 VM 7.9 GB working set |
| sp1-gpu-server 6.8.1 compiled targets (cuobjdump) | sm_80, sm_86, sm_89, sm_90, sm_100, sm_120 and compute_120 PTX: Ada (4090) is native, no JIT; nothing for AMD |
| Shards a minute, one 5090 through the app's loop (empty shards) | 1.6 |
| Chain of 2 live blocks on the Mac CPU (`--mode chain`) | shard 55.4 and 41.3 s, aggregate 52.0 s then 59.1 s with the previous proof, chain_len 2, final proof 1,272,909 bytes, `verify-segment` 0.032 s |
| Unit tests | consensus core 13, exec 8, app 5, all passing on the Mac |
| The harness (3 nodes, fast time, trust mode) | PASSED, 21 checks in 197 s on b177718e (N = 4) and 244 s on the final tree ece42979 (N = 8): paid 1.0 s after submit, every node agreeing; the fresh chain refused after a proven segment; the unproven segment skipped after its deadline; shards at 90% |
| Coverage, 3-min window, the degraded fleet (one card, the Mac verifier down) | 4.7% of blocks proven, on-chain latency p50 39 s |
| The chain of 8 live blocks on the 5090, the card also mining (`chain-pc2-pv1c`) | shard 7.3 to 7.7 s, first aggregation 7.9 s, every chained one 9.6 to 9.7 s; N = 2 in 32.6 s, N = 4 in 66.8 s, N = 8 in 135.6 s (17.0 s a block); the final proof 1,272,909 bytes whatever N, the record 586 bytes, `verify-segment` 0.037 to 0.040 s; GPU peak 16,751 MiB with the miner resident. Against 4 October with the miner stopped (aggregate 2.2 s): the miner slows the prover 3 to 4x |
| 5090-class cards for 100% at 1 block/s, measured rows | 47 with the loop as it is, 18 through the chain mode on mining cards, 6 (approximate) on proving-only cards, at empty blocks; 14 proving-only at one full shard a block; 45 at `B_p`: the table in the bench log |
## 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.
## The decisions (Decided 5 October 2026, delegated: Josh, "I have no idea for most of this stuff so do a lot of research and deploy what is absolute best")
Each with its rule, its number and its evidence. The deploy is the DEVNET through 0.3.11 (not the public testnet).
### What the other networks do (read 5 October 2026, 20:05 to 20:15 UTC; every figure from the page named, else labelled approximate)
| Network | Unit proven | Deadline | What a miss costs | Who is paid what | Measured latency |
|---|---|---|---|---|---|
| Taiko Alethia (L2BEAT page, protocol v2.1.0 notes) | a batch of blocks | proving window 2 h, cooldown 2 h (v2.1.0, February 2025); the Shasta inbox targets a 4-h proof submission cadence | the proposer's liveness bond is credited back in full when the batch is proved inside the window, half when outside; mainnet currently sets minBond and livenessBond to 0 | the prover earns the proving fee; two of four proofs needed (SGX Geth, SGX Reth, SP1, RISC0, at least one ZK) | 100% ZK coverage of mainnet blocks reached December 2025 (blockchain.news); preconfirmations 2 s |
| Boundless (docs.boundless.network, proof lifecycle) | one request | the requester's timeout (example 3,600 s) and a lock timeout (example 2,700 s); a reverse Dutch auction ramps the price from the minimum to the maximum over a ramp-up (example 300 s) | the locked collateral (example 5 ZKC) is slashed and used to pay another prover who fulfils the request | the prover's fee = the bid minus the market fee | not stated on the page |
| Succinct Prover Network (docs.succinct.xyz, SPN architecture and quickstart) | one request | the requester's deadline (the quickstart example: 10 minutes, 50 PROVE staked to bid, 100 PROVE maximum fee) | part or all of the winning prover's collateral slashed "according to protocol rules" | a reverse auction: the lowest bidder is assigned | "real-time", no number on the page |
| Aztec (docs.aztec.network economics; L2BEAT; forum) | an epoch of 32 blocks (38 min 24 s), a proof may cover one checkpoint (1 min 12 s) up to one epoch; maximum proof window 1 h 16 min | the epoch is declared failed only when its submission window expires | an unproven epoch is reorged out (no reward); proposals under discussion remove bonds and pay every prover that delivers on time | 400 AZTEC a slot: 70% sequencers, 30% provers (120 AZTEC), provers' share by an activity score | the public testnet proved by community provers (zkcloud blog), no page number |
| zkSync Era, Linea, Scroll (eco.com comparisons) | a batch | none on chain (the operator proves) | none | the operator | proof latency about 30 min (zkSync Era), 75 min (Linea), 90 min (Scroll), approximate |
Reading. Nobody pays an aggregator as a separate role: Aztec's 30% goes to whoever delivers the epoch proof, Taiko's fee to whoever proves the batch, the markets to the request's winner. Deadlines run from 10 minutes (Succinct's example) through 1 h (Boundless' example) to 2 h (Taiko) and 1 h 16 min (Aztec's maximum window); a miss forfeits the reward or part of a bond, and the slashed value goes to the prover who steps in (Boundless). Igneum has no bond on shards by decision (spec 7.2 item 4), so the forfeit here is the reward only.
### The decisions
| Decision | Decided | Rule and number | Evidence |
|---|---|---|---|
| `proving_v1_segment_blocks` (N) | **8** | Aggregation is a fixed cost per block, not per segment: 9.6 to 9.7 s for every chained block on a mining 5090, 7.9 s unchained (`chain-pc2-pv1c`), so N buys nothing in card time; it sets the record cadence and the forfeit. At N = 8 and 1 block/s a record every 8 s, a 1.27 MB proof gossiped every 8 s (159 KB/s per path, half of N = 4's 318 KB/s), and a missed segment forfeits 8 blocks' aggregator share (8 x 0.088 IGN at today's credit). The chain for 8 blocks cost 135.6 s cold on a mining card (66.8 s for 4), a fifth of T; pipelined per block it is 17 s after the last block. Aztec proves 32 blocks (38 min) as one; 8 blocks at 1 block/s is 8 s of chain, so the record lands well inside the minute the litepaper promises | bench-log "proving v1" chain rows; Aztec economics page |
| `proving_v1_unproven_daa` (T) | **600** DAA s (10 min) | T = p99 x 10: the measured block-to-carried-record latency of a shard record is p99 52 to 62 s (two 30-min windows), a cold chain of 8 adds 136 s and relay plus inclusion 10 to 40 s, about 240 s worst case; 600 leaves 2.5x on that and equals the 600-block record window of v0, so nothing is payable past it either way. Succinct's example deadline is the same 10 minutes; Boundless' example 1 h, Taiko 2 h, Aztec up to 1 h 16 min: Igneum's blocks are 1 s and its proofs seconds, so the shortest of the field. The forfeited aggregator share of an unproven segment STAYS IN THE POOL ESCROW (it is never paid, as an unproven shard's part today): no burn and no roll-over, the rule the pool already has, and the escrow is what later proofs are paid from | coverage rows; `chain-pc2-pv1c`; the table above |
| `proving_v1_aggregator_share_bps` | **1,000** (a tenth) | The aggregator's card time per block is 9.7 s on a mining card against 4 x 10.6 s of shard proofs at `B_p` (19% of the card time) and 2.5 s against 42.5 s with the card to itself (6%); on tonight's empty blocks it is half the card time. A tenth of every attested block's pool credit sits between the two full-block ratios, pays a role no other network pays separately (Aztec pays its 30% to whoever delivers the epoch; the markets pay the winner), and leaves the shard provers 90%, which the fast-time harness showed paid exactly (shardWei 90% of the credit). The pool's 20% emission share itself is unchanged (spec 2.5, 5.3) | `chain-pc2-pv1c`; bench-log 4 October 5090 rows; the harness |
| `proving_v1_activation_daa` (H) | **the devnet tip + 14,400 at publish** (4 h at 1 block/s), set by the 0.3.11 publisher in the same override object as `program_class_v3` | tonight's rule for consensus switches (the coordinator, 5 October 2026); the digest moves only once H is set, so the rolling update does not partition |
| Aggregator sortition | **none in v1**: the first valid record carried wins | design 5.3's VRF draw (O-7.3) with one or two aggregators on the devnet changes nothing; the segment grid and the deadline already bound the race; revisit when a second aggregator exists |
| Apple silicon default | **off** | the gate was "a shard under 60 s with the miner running": the M5 Max CPU took 41.3 and 55.4 s for EMPTY shards under tonight's load and 272 s for a 200-pgas shard on 4 October; a full shard at `S_p` was never under 60 s. Settings switches it on | bench-log "proving v1" CPU chain row; 4 October CPU rows |
| The prover profile per card and the 12 GB and 16 GB gates (Josh: "make sure we can prove on 12gb cards"; "is there any way we can make 12gb cards mine and prove?") | **measured on PC 2, the rows below** | the SP1 6.8.1 GPU server reads `ELEMENT_THRESHOLD`, `HEIGHT_THRESHOLD`, `SHARD_SIZE` and the `SP1_WORKER_NUM_*`/`BUFFER_SIZE` knobs from the environment it inherits (`sp1-core-executor-6.8.1/src/opts.rs`, `sp1-prover-6.8.1/src/worker/config.rs`); the app passes a profile per card (`provedefault.rs`) and the host forwards it | the sweep job `memsweep-pc2-pv1` and the miner-on run |
The resume path (5 October 2026, the 0.3.11 app): `POST /api/resume` on 0.3.9 re-armed only FAULTED cards (`stop_miners("paused")` clears every slot's `restart_at`), so a healthy paused card stayed "off" at 0 MH/s until the app was relaunched: PC 2 at 21:25:11Z (the aggregation-cost job's pause and resume; `[ok] mining resumed` then `0.00 MH/s, waiting` for 20 minutes), the Mac that afternoon. Now every slot without a live worker is re-armed and its pack exported again before the start, and 90 s later `resume_check` logs `resume: <card> is not mining 90 s after resume (state ..., pid ...)` for every enabled card without a hash rate (`engine.rs`, three unit tests: the state machine, the 21:25:11Z case against the old rule, the check).
### A self-built CUDA server (the 12 GB path), before 0.3.12 (consequences C26)
If the prover-floor agent's rebuilt `sp1-gpu-server` (the Setup sizes cut, built on PC 2 under WSL2) proves a shard under 11 GB, it becomes a shipped artefact and needs its own row of rules before 0.3.12: it is built from a pinned SP1 source tag with `CUDA_ARCHS` covering sm_86, sm_89 and sm_120 (the 12 and 16 GB tiers are Ampere and Ada, not only the 5090's Blackwell; one card family per measured row), by the packaging path that builds the Windows payload (PC 1's build job for the Linux binary, the Mac signs the manifest as it does the DMG), lands in the DMG and the WSL2 package beside the host as `wsl2/bin/sp1-gpu-server` with its sha256 in `payload-inputs.json`, is named in `evidence.md` beside the prover rows ("prover built from SP1 <tag> at <sha>"), is rebuilt and re-measured at every SP1 upgrade, and ships only after `--mode verify-segment` and `--mode verify` on proofs it made show the pinned verifying keys unchanged (the server changes allocation, not the circuit; the ids `0x2b1a81cb...` and `0x474678f3...` must still verify them). The 12 GB claim itself waits for the on-order RTX 3060 to run that server on the same fixtures and recipe as the curve; until then the public line stays at 24 GB.
The root-socket class on PC 2, the two times: 20:00:56Z (my chain job's root run; the live prover failed with Connect(PermissionDenied) until the socket was gone) and 21:25:24Z (the aggregation-cost job's root run; the prover stayed dark through the 0.3.10 restart at 21:49:41Z until `socketfix-pc2-pv1` removed the root-owned `/tmp/sp1-cuda-0.sock` at 22:01:16Z; the next shard, block 89011, was proven at 22:02:13Z and paid, and every shard since). The permanent fix in the 0.3.11 app tree: every committed playbook that runs a prove mode as root carries `pkill -f sp1-gpu-server; rm -f /tmp/sp1-cuda-*.sock` at its start and end, `tools/ci/prover-socket-check.sh` (in `ci.yml`) fails a playbook without them, and the app's prover names the cause in its log line when the host reports PermissionDenied. The app itself cannot remove a socket another user owns, so a job written outside the tree must still follow the rule.
A prover job on a shared card runs as the app's user or cleans its socket (`pkill -f sp1-gpu-server; rm -f /tmp/sp1-cuda-*.sock` at the start and the end; `tools/ci/prover-socket-check.sh`): the root-socket fault of 20:00Z, bench-log.
### The prover profiles: the tiers from the S_p curve (bench-log, "proving v1", the sweep, the miner-on pair and the curve)
The GPU server of SP1 6.8.1 sets the memory, not the shard: a floor of 13.9 GB for an empty shard, 20.4 GB for a full shard at the adopted v1 budget (30,000 pgas, 4.7 M cycles), 28.3 GB for the prototype shard (6.75 M pgas, 60 M cycles); the miner adds 1.7 GB when it shares the card; no environment knob moves the floor and the server has no options of its own; the witness is 5 to 22 KB a shard and never binds.
| Card | Alone | Beside the miner | Default (`provedefault.rs`) |
|---|---|---|---|
| 32 GB (RTX 5090) | the prototype shard, 28.3 GB, 10.8 s; the v1 shard 20.4 GB, 4.3 s | the prototype shard 30.1 GB, 33 s; the v1 shard 22.2 GB, 13.2 s | on, mine and prove, today |
| 24 GB (RTX 4090, 3090) | the v1 shard 20.4 GB; the prototype shard does NOT fit (28.3 GB) | the v1 shard 22.2 GB measured on the 5090's allocation (2.3 GB spare on a 24 GB card; approximate for the card itself) | on, mine and prove, with the line "until the devnet's fee switch its shards are the prototype size, which needs 32 GB, so this card proves from the switch on" |
| 16 GB (RTX 5080, 4080) | an empty shard only (13.9 GB) | nothing (15.7 GB for an empty shard, no room for the display) | off, with the line |
| 12 GB (RTX 3060, 4070) | nothing: the floor is 13.9 GB, and the shipped server refuses the card outright | nothing | off; Josh's "make sure we can prove on 12 GB cards" is OPEN and in work: the prover-floor agent (branch prover-floor, 5 October night) read SP1 v6.8.1's GPU server source (`sp1-gpu/crates/prover_components/src/builder.rs` lines 35 to 39): it reads the card's memory, adds 4 and panics under 24 ("Unsupported GPU memory ... must be at least 24GB"), and builds its core (ELEMENT_THRESHOLD 2^28 + 2^27 elements + 2^21), recursion (2^27), shrink (2^25) and wrap (85 M element) provers at Setup whatever the mode, which is the 13.9 GB floor; no knob reaches them, so the fix is a server rebuilt from source on PC 2 (WSL2, nvcc 12.8, CUDA_ARCHS=120) with those sizes cut, measured on the same fixtures and recipe as the curve above (D2 carries the curve) |
| under 12 GB | nothing | nothing | off, mine only |
| AMD-only and Apple machines | nothing on the GPU: no zkVM proves on an AMD GPU today (`docs/analysis/amd-proving.md`, branch amd-prove); the CPU prover is about 5 minutes a shard at a 30 GB RSS whatever the shard size (PC 1, bench-log "the SP1 CPU prover on PC 1") | | off, "mines and does not prove"; the only non-NVIDIA path with a shipped backend is RISC Zero's Metal prover behind the `ProofSystem` seam (a second guest and pinned id, a verifier for both formats, no shared aggregation): an open item, not 0.3.11 |
The three profile numbers the coordinator asked for, as measured: under 9.0 GB does not exist on this build (floor 13.9); under 15.0 GB mine-and-prove does not exist for any full shard (the v1 shard alone is 20.4); the full profile is the 32 GB card. The fleet table's "proving-only" rows therefore read 24 GB cards at the v1 budget and 32 GB cards at the prototype budget. Shards per block at the v1 budget: 1 on tonight's empty chain, 2 to 4 on blocks with transactions (`B_p` 120,000 = 4 x `S_p`); the aggregation count is one per block whatever the shard count (the chained recursion), so the aggregation-cost agent's target is per block.
The aggregation-cost agent's first rows (branch agg-cost, 5 October 2026 night, the same four live blocks on PC 2): with the miners paused an empty shard proves in 1.9 to 2.2 s and an aggregation in 1.7 to 2.2 s with the card 15.8% busy; mining, 7.4 to 7.8 s and 7.9 to 9.8 s at 93.9% busy, so the miner's kernels take the card and the prover runs 3.6x (shards) to 4.5x (aggregations) slower beside them; its batch-size curve is still open. That puts a proving-only card at about 4 s per empty block (one shard and one aggregation), 4 cards for an empty-block chain at 1 block/s, against 18 mining cards.
The re-plans of block 344 at 2.25 M and 4.5 M pgas peak at 28.3 to 28.4 GB alone (the server's buffers step up between 4.7 M and 20 M cycles and are flat to 60 M), so no shard size between the v1 budget and the prototype one changes a tier; with the miner the adopted shard proves 3.1x slower (13.2 s against 4.2 s) and the chained aggregation 9.7 s against 2.5 s: a mining 24 GB card delivers one adopted-size shard plus one aggregation in about 23 s, inside T by 25x.

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). 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, Josh'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 ## 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: 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) | | 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 | | 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 | | 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 75 (protocol 15) | 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`, 8 | Implemented, value Decided (5 October 2026, delegated; `docs/plans/proving-v1.md`) |
| Unproven deadline `T` | `proving_v1_unproven_daa`, 600 DAA s after the segment's last chain block | Implemented, value Decided (5 October 2026, delegated) |
| 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 Decided (5 October 2026, delegated) |
| 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 ## 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. 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`. 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 (Josh: "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 75, protocol version 15; peers at 14 and below never receive it; 14 is the EVM transaction relay of 0.3.10) 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 Josh 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

@ -61,5 +61,9 @@
"proving_v1_unproven_daa": 10, "proving_v1_unproven_daa": 10,
"proving_v1_aggregator_share_bps": 1000, "proving_v1_aggregator_share_bps": 1000,
"fees_v1_activation_daa": 0, "fees_v1_activation_daa": 0,
"proving_v1_activation_daa": 18446744073709551615,
"proving_v1_segment_blocks": 8,
"proving_v1_unproven_daa": 10,
"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} "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,28 @@ fn run() -> Result<()> {
// proving v0 (spec 7.7): the node's proof pool verifies a submitted shard proof off the consensus path // 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>")?); 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")?; if mode == "verify-segment" {
let shard_index: usize = arg("--shard").map(|s| s.parse()).transpose()?.unwrap_or(0); // 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 prover: Address = arg("--prover").map(|s| s.parse()).transpose()?.unwrap_or_else(|| Address::from_slice(&[0x19; 20]));
let out_path = arg("--out"); 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] [--budget <test pgas>] [--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);
// `--budget <pgas>`: re-plan the fixture's block at a TEST budget (the S_p curve of 5 October 2026); the fixture's
// own per-shard plan is then not compared (the chain and the sums still are), and `--out` records the cut
let test_budget: Option<u64> = arg("--budget").map(|s| s.parse()).transpose()?;
let fixture: Fixture = serde_json::from_str(&std::fs::read_to_string(path).with_context(|| format!("read {path}"))?)?; 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 { 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); bail!("fixture format {} is not {} (regenerate with igneum-prove-export)", fixture.format, igneum_prove_core::fixture::FORMAT);
@ -92,7 +110,10 @@ fn run() -> Result<()> {
// The fee set that meters this block: the fixture's schedule read at the block's DAA score (5 October 2026, // The fee set that meters this block: the fixture's schedule read at the block's DAA score (5 October 2026,
// `fees_v1_activation_daa`); the plan's budget must be that set's S_p unless the fixture says test cut. // `fees_v1_activation_daa`); the plan's budget must be that set's S_p unless the fixture says test cut.
let fee_set = block.fees.at(block.env.daa_score); let fee_set = block.fees.at(block.env.daa_score);
if fixture.plan.consensus && fixture.plan.shard_budget != fee_set.shard_proving_gas_budget { if let Some(b) = test_budget {
println!("TEST CUT: the block is re-planned at a budget of {b} pgas (the fixture's plan at {} is not compared)", fixture.plan.shard_budget);
}
if test_budget.is_none() && fixture.plan.consensus && fixture.plan.shard_budget != fee_set.shard_proving_gas_budget {
bail!("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, block.env.daa_score, fee_set.name(), fee_set.shard_proving_gas_budget); bail!("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, block.env.daa_score, fee_set.name(), fee_set.shard_proving_gas_budget);
} }
println!( println!(
@ -128,8 +149,11 @@ fn run() -> Result<()> {
// 1. Native: the cut, the witnesses, every shard statement, the chain and the sums, against the fixture. // 1. Native: the cut, the witnesses, every shard statement, the chain and the sums, against the fixture.
stage("native"); stage("native");
let t = Instant::now(); let t = Instant::now();
let (outcome, pre_root, shards) = build_shards(block, fixture.plan.shard_budget, prover); let budget = test_budget.unwrap_or(fixture.plan.shard_budget);
let (outcome, pre_root, shards) = build_shards(block, budget, prover);
let native_s = t.elapsed().as_secs_f64(); let native_s = t.elapsed().as_secs_f64();
results.insert("budget".into(), budget.into());
results.insert("test_cut".into(), test_budget.is_some().into());
let e = &fixture.expected; let e = &fixture.expected;
let same = pre_root == e.pre_state_root && outcome.state_root == e.post_state_root && outcome.receipts_root == e.receipts_root && outcome.tx_commitment == e.tx_commitment && outcome.gas_used == e.gas_used && outcome.pgas_used == e.pgas_used; let same = pre_root == e.pre_state_root && outcome.state_root == e.post_state_root && outcome.receipts_root == e.receipts_root && outcome.tx_commitment == e.tx_commitment && outcome.gas_used == e.gas_used && outcome.pgas_used == e.pgas_used;
println!( println!(
@ -151,18 +175,20 @@ fn run() -> Result<()> {
if e.node_state_root != e.post_state_root { if e.node_state_root != e.post_state_root {
bail!("the fixture's node state root differs from its expected post-state root; the exporter must not have produced this file"); bail!("the fixture's node state root differs from its expected post-state root; the exporter must not have produced this file");
} }
if shards.len() != fixture.plan.shards.len() { if test_budget.is_none() && shards.len() != fixture.plan.shards.len() {
bail!("the plan has {} shards here and {} in the fixture", shards.len(), fixture.plan.shards.len()); bail!("the plan has {} shards here and {} in the fixture", shards.len(), fixture.plan.shards.len());
} }
let (mut sum_gas, mut sum_pgas) = (0u64, 0u64); let (mut sum_gas, mut sum_pgas) = (0u64, 0u64);
let mut prev_root = pre_root; let mut prev_root = pre_root;
let mut prev_link = igneum_prove_core::Carry::default().link(); let mut prev_link = igneum_prove_core::Carry::default().link();
for (s, x) in shards.iter().zip(&fixture.plan.shards) { for (i, s) in shards.iter().enumerate() {
let o = &s.output; let o = &s.output;
let (accounts, slots, leaves, hashes) = s.input.witness.stats(); let (accounts, slots, leaves, hashes) = s.input.witness.stats();
let bytes = bincode::serialize(&s.input)?.len(); let bytes = bincode::serialize(&s.input)?.len();
if o.pre_root != x.pre_root || o.post_root != x.post_root || o.receipts_root != x.receipts_root || o.link_in != x.link_in || o.link_out != x.link_out || o.gas_used != x.gas_used || o.pgas_used != x.pgas_used { if let (None, Some(x)) = (test_budget, fixture.plan.shards.get(i)) {
bail!("shard {}: the native statement differs from the fixture's plan", o.shard_index); if o.pre_root != x.pre_root || o.post_root != x.post_root || o.receipts_root != x.receipts_root || o.link_in != x.link_in || o.link_out != x.link_out || o.gas_used != x.gas_used || o.pgas_used != x.pgas_used {
bail!("shard {}: the native statement differs from the fixture's plan", o.shard_index);
}
} }
if o.pre_root != prev_root || o.link_in != prev_link { if o.pre_root != prev_root || o.link_in != prev_link {
bail!("shard {}: does not continue the previous shard", o.shard_index); bail!("shard {}: does not continue the previous shard", o.shard_index);
@ -171,6 +197,7 @@ fn run() -> Result<()> {
prev_link = o.link_out; prev_link = o.link_out;
sum_gas += o.gas_used; sum_gas += o.gas_used;
sum_pgas += o.pgas_used; sum_pgas += o.pgas_used;
results.entry("shard_witness_bytes").or_insert_with(|| serde_json::Value::Array(Vec::new())).as_array_mut().unwrap().push(serde_json::Value::from(bytes as u64));
println!( println!(
"RESULT shard {} native: txs {}..{} ({} executed, {} skipped), gas {}, pgas {}{}, witness {accounts} accounts {slots} slots {leaves} leaves {hashes} hashes, input {bytes} bytes, pre {} post {}", "RESULT shard {} native: txs {}..{} ({} executed, {} skipped), gas {}, pgas {}{}, witness {accounts} accounts {slots} slots {leaves} leaves {hashes} hashes, input {bytes} bytes, pre {} post {}",
o.shard_index, o.shard_index,
@ -531,6 +558,314 @@ fn run_block(sp1: &Sp1ProofSystem, shards: &[BuiltShard], claim: &SegmentClaim,
Ok(()) 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<()> { fn check_shard_output(out: &ShardOutput, native: &ShardOutput) -> Result<()> {
if out != native { if out != native {
bail!("the guest's public values differ from the native run:\n guest {out:?}\n native {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 struct Pinned {
pub manifest: Manifest, pub manifest: Manifest,
pub shard_vk: SP1VerifyingKey, pub shard_vk: SP1VerifyingKey,
/// For the aggregated record's verifier (proving v0 next step); checked against the manifest today. /// The aggregated segment record's verifier (`--mode verify-segment`, proving v1).
#[allow(dead_code)]
pub agg_vk: SP1VerifyingKey, pub agg_vk: SP1VerifyingKey,
pub shard_id: B256, pub shard_id: B256,
pub agg_id: B256, pub agg_id: B256,

View file

@ -451,7 +451,7 @@ body.all .pager{display:none}
<p>Blocks carry transactions only and make no claim about state. Every node executes the ordered transactions natively at once, so users see their transaction land in about a second. The execution is then split into shards of a fixed proving cost. Shards are assigned by lot to eight provers for ten seconds, then open to anyone; there is no bond. Provers run them on consumer cards, and the shard proofs are folded by recursive aggregation into one proof for the block. That proof lands on-chain within about a minute at launch. Because the proof computes the state from the ordered sequence, no node accepts a block with a wrong state root. Full nodes also execute every block natively and reject a proof record whose result differs from their own execution, so a forged proof is a light-client problem and never a chain split. Implemented: the native-execution check on every carried proof record, proving v0 on the devnet (specification section 7). The emergency path for a soundness bug in the proof system is a human one: a new proof-system version is written by people and activates only on miner signalling. Invalid transactions are skipped by rule, the way Kaspa skips conflicting spends.</p> <p>Blocks carry transactions only and make no claim about state. Every node executes the ordered transactions natively at once, so users see their transaction land in about a second. The execution is then split into shards of a fixed proving cost. Shards are assigned by lot to eight provers for ten seconds, then open to anyone; there is no bond. Provers run them on consumer cards, and the shard proofs are folded by recursive aggregation into one proof for the block. That proof lands on-chain within about a minute at launch. Because the proof computes the state from the ordered sequence, no node accepts a block with a wrong state root. Full nodes also execute every block natively and reject a proof record whose result differs from their own execution, so a forged proof is a light-client problem and never a chain split. Implemented: the native-execution check on every carried proof record, proving v0 on the devnet (specification section 7). The emergency path for a soundness bug in the proof system is a human one: a new proof-system version is written by people and activates only on miner signalling. Invalid transactions are skipped by rule, the way Kaspa skips conflicting spends.</p>
<p>Proving needs an NVIDIA card with 24 GB or more (32 GB until the fee switch of 6 October 2026; from it a 24 GB card mines and proves on the same card: 22.2 GB peak measured with the miner on, 5 October 2026). AMD and Apple cards mine. A prover for them lands when a zkVM ships one.</p> <p>Proving needs an NVIDIA card with 24 GB or more (32 GB until the fee switch of 6 October 2026; from it a 24 GB card mines and proves on the same card: 22.2 GB peak measured with the miner on, 5 October 2026). AMD and Apple cards mine. A prover for them lands when a zkVM ships one.</p>
<h3>The proving budget</h3> <h3>The proving budget</h3>
<p>Gas prices execution. Proving cost is a different number, so Igneum meters it separately: every transaction pays in both dimensions, and each block has a proving-cost budget set in consensus from measured prover throughput. A transaction that is cheap to run and expensive to prove pays for what it costs the provers. Target: shard size will be set so a 12 GB card proves one shard in about 20 seconds. That number is the phase 2 gate on the roadmap and is not measured yet on the card the gate names. The first proofs exist: on 4 October 2026 an RTX 5090 proved a small two-transaction block in 1.4 seconds (2.7 seconds compressed), verified in 0.22 and 0.038 seconds, and a laptop CPU proved a three-shard block end to end in 19 minutes. Later that day the same card proved a full shard at the provisional size, 6.75 million prover gas, which executed in 60.8 million cycles: core proof 8.3 seconds, compressed proof 10.9 seconds, verified in 0.040 seconds; a four-shard block took 44.5 seconds of GPU stages end to end. Since 5 October 2026 shards are assigned and proven on the live devnet. The gate asks for a mid-range card, and an RTX 5090 is not one, so the gate stands open. Once the gate is measured, the budget rises by schedule as hardware improves. The proof system is hash-based, which is what runs on consumer cards, and sits behind a versioned interface, so Igneum can adopt a better proof system when one exists by a miner-signalled release, and runs for ever on the current one if none is adopted.</p> <p>Gas prices execution. Proving cost is a different number, so Igneum meters it separately: every transaction pays in both dimensions, and each block has a proving-cost budget set in consensus from measured prover throughput. A transaction that is cheap to run and expensive to prove pays for what it costs the provers. Measured on 5 October 2026 (an RTX 5090 under SP1 6.8.1's GPU prover, the shard size the chain adopts from its fee switch, 30,000 proving gas, about 4.7 million prover cycles): one full shard proves in 4.3 seconds and needs 20.4 GB of GPU memory with the card to itself, so a 24 GB card proves full shards and a 12 GB or 16 GB card does not on this prover build, whose floor is 13.9 GB for even an empty shard; mining and proving on one card needs 32 GB today (the prototype-size shard beside the miner peaked at 30.1 GB) and 24 GB once the adopted shard size is live (22.2 GB beside the miner, 13.2 seconds a shard, measured on the 32 GB card; a 24 GB card has not run it yet). The old 12 GB gate on the roadmap is withdrawn until a prover build with a smaller floor is measured. The first proofs exist: on 4 October 2026 an RTX 5090 proved a small two-transaction block in 1.4 seconds (2.7 seconds compressed), verified in 0.22 and 0.038 seconds, and a laptop CPU proved a three-shard block end to end in 19 minutes. Later that day the same card proved a full shard at the provisional size, 6.75 million prover gas, which executed in 60.8 million cycles: core proof 8.3 seconds, compressed proof 10.9 seconds, verified in 0.040 seconds; a four-shard block took 44.5 seconds of GPU stages end to end. Since 5 October 2026 shards are assigned and proven on the live devnet. The gate asks for a mid-range card, and an RTX 5090 is not one, so the gate stands open. Once the gate is measured, the budget rises by schedule as hardware improves. The proof system is hash-based, which is what runs on consumer cards, and sits behind a versioned interface, so Igneum can adopt a better proof system when one exists by a miner-signalled release, and runs for ever on the current one if none is adopted.</p>
<h3>Proving for everyone else</h3> <h3>Proving for everyone else</h3>
<p>The same miners accept proving jobs from other chains. Rollups post a job, a miner wins it, proves it, and is paid. The job market is permissionless and is Designed, not yet built. At launch a job is paid on the customer's own chain, in the customer's currency, to a payout contract keyed by miner address, because Igneum cannot yet see Ethereum. Settlement in IGN, with 10% of each fee burned, follows when the proof bridge lets Igneum see the payment, in phase two. The Igneum miner client can also bid on other proving networks and take the best price, where a miner chooses to hold their collateral: Boundless provers post ZKC and Succinct provers stake PROVE (approximate, from their documentation). The proving market is small today. Igneum does not depend on it. We know of no other proof-of-work chain selling proofs to other chains.</p> <p>The same miners accept proving jobs from other chains. Rollups post a job, a miner wins it, proves it, and is paid. The job market is permissionless and is Designed, not yet built. At launch a job is paid on the customer's own chain, in the customer's currency, to a payout contract keyed by miner address, because Igneum cannot yet see Ethereum. Settlement in IGN, with 10% of each fee burned, follows when the proof bridge lets Igneum see the payment, in phase two. The Igneum miner client can also bid on other proving networks and take the best price, where a miner chooses to hold their collateral: Boundless provers post ZKC and Succinct provers stake PROVE (approximate, from their documentation). The proving market is small today. Igneum does not depend on it. We know of no other proof-of-work chain selling proofs to other chains.</p>
</section> </section>
@ -559,7 +559,7 @@ body.all .pager{display:none}
</table></div> </table></div>
<p>The honest bear-market case rests on cost. A miner's card is already running and the power is often domestic, so Igneum miners' marginal cost in the proving market is close to power, which is an edge over data-centre provers and nothing more.</p> <p>The honest bear-market case rests on cost. A miner's card is already running and the power is often domestic, so Igneum miners' marginal cost in the proving market is close to power, which is an edge over data-centre provers and nothing more.</p>
<h3>Hardware</h3> <h3>Hardware</h3>
<p>The dataset starts at 2 GB and grows (the proposed schedule, fixed at the testnet genesis: 2 GB, doubling at years 4, 12 and 28, the average of half a gigabyte a year), so a 4 GB card mines for about four years and an 8 GB card for about twelve, approximate. NVIDIA and AMD both work, because the mining program is generated for the architecture both share and the proof system is hash-based. Apple's chips are GPUs with unified memory, so Macs mine too, at about a fifth of a flagship card: Measured, 26.7 against 123 million hashes a second, an Apple M5 Max beside an RTX 5090 on the live devnet, 4 October 2026. A Mac is a poor miner per dollar. There is no CPU mining lane, on purpose, because CPU mining is what botnets farm. Nodes, wallets and exchanges need no GPU at all.</p> <p>The dataset starts at 2 GB and grows (the proposed schedule, fixed at the testnet genesis: 2 GB, doubling at years 4, 12 and 28, the average of half a gigabyte a year), so a 4 GB card mines for about four years and an 8 GB card for about twelve, approximate. 24 GB proves full shards (measured on a 32 GB card's allocation; a 24 GB card has not run it yet) and 32 GB mines and proves on one card, measured 5 October 2026 on this prover build (a 12 GB card does not prove on it: the GPU prover's floor is 13.9 GB). NVIDIA and AMD both work, because the mining program is generated for the architecture both share and the proof system is hash-based. Apple's chips are GPUs with unified memory, so Macs mine too, at about a fifth of a flagship card: Measured, 26.7 against 123 million hashes a second, an Apple M5 Max beside an RTX 5090 on the live devnet, 4 October 2026. A Mac is a poor miner per dollar. There is no CPU mining lane, on purpose, because CPU mining is what botnets farm. Nodes, wallets and exchanges need no GPU at all.</p>
<h3>What a miner's hour looks like</h3> <h3>What a miner's hour looks like</h3>
<p>The card hashes the lottery continuously. When the client sees a shard or an external job it can win, it switches the card to proving for a few seconds, posts the proof, and goes back to hashing. The client does the switching and the miner sees one balance.</p> <p>The card hashes the lottery continuously. When the client sees a shard or an external job it can win, it switches the card to proving for a few seconds, posts the proof, and goes back to hashing. The client does the switching and the miner sees one balance.</p>
<p>The protocol carries no fee: no dev fund, no cut to any team. Ember, the miner software, takes an optional 1% dev fee, the way other GPU miners do. One block template in 100 is requested with the dev address instead of yours, by a counter, not a random draw, so it is exactly 1 in 100 and anyone can check it from the source or from the chain. One flag turns it off (<code>--dev-fee 0</code>, a switch in the app, a line in the HiveOS config). The miner prints the fee and the address when it starts. Any other client is welcome.</p> <p>The protocol carries no fee: no dev fund, no cut to any team. Ember, the miner software, takes an optional 1% dev fee, the way other GPU miners do. One block template in 100 is requested with the dev address instead of yours, by a counter, not a random draw, so it is exactly 1 in 100 and anyone can check it from the source or from the chain. One flag turns it off (<code>--dev-fee 0</code>, a switch in the app, a line in the HiveOS config). The miner prints the fee and the address when it starts. Any other client is welcome.</p>

19
tools/ci/prover-socket-check.sh Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bash
# The root-socket class (5 October 2026, 20:00Z): a PC 2 job ran igneum-prove-host as root inside WSL2, which started
# an sp1-gpu-server whose socket /tmp/sp1-cuda-0.sock stayed root-owned after the job; the live prover (the app's user)
# then failed every shard with "CudaClientError: Connect(PermissionDenied)" until the socket was gone. Rule: every
# playbook that runs the prover host as root on a shared card kills the server AND unlinks its socket (at the start
# and at the end), or runs as the app's user. This check fails CI when a script runs `igneum-prove-host` under a
# `-u root` WSL session without both lines.
set -euo pipefail
cd "$(dirname "$0")/../.."
fail=0
while IFS= read -r f; do
# a playbook that only runs `--mode id` or `--mode verify` starts no GPU server; the prove modes do
if grep -qE 'igneum-prove-host' "$f" && grep -qE -- '--mode (compressed|chain|aggregate|block|shard|all)\b' "$f" && grep -qE -- '-u root' "$f"; then
if ! grep -qE 'pkill -f sp1-gpu-server' "$f"; then echo "prover-socket: $f runs the prover host as root without killing sp1-gpu-server"; fail=1; fi
if ! grep -qE 'rm -f /tmp/sp1-cuda-' "$f"; then echo "prover-socket: $f runs the prover host as root without unlinking /tmp/sp1-cuda-*.sock"; fail=1; fi
fi
done < <(git ls-files 'tools/**' 'relay/playbooks/**' 'proving/**' 'packaging/**' | grep -E '\.(sh|ps1|mjs)$')
[ "$fail" = 0 ] && echo "prover-socket: every root prover playbook kills the GPU server and unlinks its socket"
exit $fail

View file

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

View file

@ -0,0 +1,154 @@
{
"aggregate_prove_seconds_total": 75.492162475,
"aggregator_id": "0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896",
"blocks": [
{
"aggregate_prove_seconds": 7.903165861,
"aggregate_verify_seconds": 0.037313475,
"block_seconds": 15.53678578,
"chain_len": 1,
"cumulative_seconds": 15.53678733,
"number": 83616,
"post_root": "0x85e67dcea1453afbc17cc0c83ddee0ae12e43d000a4821b4f1c6b2351d677fbc",
"proof_bytes": 1272909,
"shard_prove_seconds": [
7.557706469
],
"shards": 1,
"statement": "0x1f3c209362cc0e6a8b0f4c988728e073331f00d4a2ebdc58cc5cfe078cdf42df"
},
{
"aggregate_prove_seconds": 9.625608758,
"aggregate_verify_seconds": 0.037767114,
"block_seconds": 17.043759121,
"chain_len": 2,
"cumulative_seconds": 32.580617039,
"number": 83617,
"post_root": "0x6e85cac1c9c7973c2cb4d0ff38f96edc054c092fa414f26bf53b237e08e81b77",
"proof_bytes": 1272909,
"shard_prove_seconds": [
7.344317298
],
"shards": 1,
"statement": "0x903b6bf10915403c9ed8a7bc87951c6bc975ba992f947c3b02af48c7e9331306"
},
{
"aggregate_prove_seconds": 9.554146543,
"aggregate_verify_seconds": 0.037548738,
"block_seconds": 16.93764002,
"chain_len": 3,
"cumulative_seconds": 49.518392437,
"number": 83618,
"post_root": "0x4a386002c4df5c9a0ea0494442d252c02860bf2c44bc626321c8b4bb11c5b7d5",
"proof_bytes": 1272909,
"shard_prove_seconds": [
7.307433102
],
"shards": 1,
"statement": "0x0913be6ca9c2671c53e202a144e9a57ecb9b45224ad764031b5e01487dccfa94"
},
{
"aggregate_prove_seconds": 9.725408301,
"aggregate_verify_seconds": 0.038074529,
"block_seconds": 17.316632646,
"chain_len": 4,
"cumulative_seconds": 66.835133749,
"number": 83619,
"post_root": "0x2d46b11d9f257e3e6a6d6e3ebd947a3498c3c5d9a80ef2651caf72f5d2af47ce",
"proof_bytes": 1272909,
"shard_prove_seconds": [
7.516456343
],
"shards": 1,
"statement": "0x00e0a2c9f1e16e06491ce8d26f457b35c4ac60cd3b609e8a87134a31a06a1fec"
},
{
"aggregate_prove_seconds": 9.694312635,
"aggregate_verify_seconds": 0.038877759,
"block_seconds": 17.159775091,
"chain_len": 5,
"cumulative_seconds": 83.995023181,
"number": 83620,
"post_root": "0x209d61c1fa5d410a252a829f186b22b01840729619b69177647ad6d73c87af5d",
"proof_bytes": 1272909,
"shard_prove_seconds": [
7.389889021
],
"shards": 1,
"statement": "0xf26202b09c544d042d78d4844e6589d467f8621d9c4f13d61685fa36d58444d9"
},
{
"aggregate_prove_seconds": 9.664895146,
"aggregate_verify_seconds": 0.035052656,
"block_seconds": 17.453177945,
"chain_len": 6,
"cumulative_seconds": 101.448329353,
"number": 83621,
"post_root": "0x78034f6b455e956da74f6e685b89ead732a45b665ff4706be6deea7cbb402c53",
"proof_bytes": 1272909,
"shard_prove_seconds": [
7.71706568
],
"shards": 1,
"statement": "0x53b07bfc69d17ca7504590bfe52f1d0915e39b2e3e497c57117d871ab275d2c9"
},
{
"aggregate_prove_seconds": 9.634209172,
"aggregate_verify_seconds": 0.037378761,
"block_seconds": 17.047352194,
"chain_len": 7,
"cumulative_seconds": 118.49578211,
"number": 83622,
"post_root": "0x258fbe0b700833451b4bde8139cc6eb60da4fd959dafdd2784572ead6e39d908",
"proof_bytes": 1272909,
"shard_prove_seconds": [
7.338077294
],
"shards": 1,
"statement": "0xc425c86b6569c80481691e6c89abfbd1481cc555030b791178ef536b1b3264a7"
},
{
"aggregate_prove_seconds": 9.690416059,
"aggregate_verify_seconds": 0.036976436,
"block_seconds": 17.115775928,
"chain_len": 8,
"cumulative_seconds": 135.611679366,
"number": 83623,
"post_root": "0x8840c082c4406252bcd65a31ed0102a2eba1b59130582bbba0f87d64ebbc80ed",
"proof_bytes": 1272909,
"shard_prove_seconds": [
7.347491181
],
"shards": 1,
"statement": "0xa99aba5397bae3fd8323dcbc4a93105ae531356fff92feaccca0c3284168ca49"
}
],
"chain_seconds": 135.611788663,
"first": 83616,
"fixtures": [
"/mnt/c/Users/Admin/AppData/Local/igneum/app/jobs/chain-pc2-pv1c/block-83616.json",
"/mnt/c/Users/Admin/AppData/Local/igneum/app/jobs/chain-pc2-pv1c/block-83617.json",
"/mnt/c/Users/Admin/AppData/Local/igneum/app/jobs/chain-pc2-pv1c/block-83618.json",
"/mnt/c/Users/Admin/AppData/Local/igneum/app/jobs/chain-pc2-pv1c/block-83619.json",
"/mnt/c/Users/Admin/AppData/Local/igneum/app/jobs/chain-pc2-pv1c/block-83620.json",
"/mnt/c/Users/Admin/AppData/Local/igneum/app/jobs/chain-pc2-pv1c/block-83621.json",
"/mnt/c/Users/Admin/AppData/Local/igneum/app/jobs/chain-pc2-pv1c/block-83622.json",
"/mnt/c/Users/Admin/AppData/Local/igneum/app/jobs/chain-pc2-pv1c/block-83623.json"
],
"last": 83623,
"mode": "chain",
"prover": "cuda",
"segment_block_hash": "0x6a3295e7481cb3d5db67758d95280debb72b000a3a0c8a99fe30bf4f3d66aed1",
"segment_chain_len": 8,
"segment_number": 83623,
"segment_proof_bytes": 1272909,
"segment_proof_file": "/mnt/c/Users/Admin/AppData/Local/igneum/app/jobs/chain-pc2-pv1c/segment-83623-aggregated.bin",
"segment_proof_sha256": "0xc28c2c1e8054523c1cff5e38cef8d86498bc004b9beeff010a1d2acc7ef1435c",
"segment_provers": "0x1805afcd50a68f5237f8a5e2e41a4270457be031d1158f5247af6f68808b0d11",
"segment_public_values": "0x000000000000116f00000000000146a76a3295e7481cb3d5db67758d95280debb72b000a3a0c8a99fe30bf4f3d66aed16cad584f6a86085d410c13728d11fa5c4daf583ebbc443512999174b9f7d3b68000000010000000000000000000000000000000000000000000000000000000000000000258fbe0b700833451b4bde8139cc6eb60da4fd959dafdd2784572ead6e39d9088840c082c4406252bcd65a31ed0102a2eba1b59130582bbba0f87d64ebbc80ed6fcd3e8e97da273711ccefb79abdd246c5663c7d61057f82bae745ceac5dcc750000000000000000000000000000000000000000000000001805afcd50a68f5237f8a5e2e41a4270457be031d1158f5247af6f68808b0d112b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b091238960000000000000008",
"segment_statement": "0xa99aba5397bae3fd8323dcbc4a93105ae531356fff92feaccca0c3284168ca49",
"setup_seconds": 15.734044172,
"shard_program_id": "0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a",
"shard_prove_seconds_total": 59.518436388000005,
"shards": 8
}

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, String(first), String(last), hash, 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,91 @@
# 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'
$bodyFile = Join-Path $job 'export-request.json'
[IO.File]::WriteAllText($bodyFile, $body, (New-Object System.Text.UTF8Encoding $false))
# curl.exe (Windows 10+ ships it) streams the 70 MB reply to a file; Invoke-WebRequest's Content is a string there
# and WriteAllBytes refused it (the first run, 19:52Z)
& curl.exe -s -S -m 600 -X POST "http://127.0.0.1:$evmPort" -H 'Content-Type: application/json' --data-binary "@$bodyFile" -o $seqFile 2>&1 | ForEach-Object { "curl: $_" }
if (-not (Test-Path $seqFile) -or (Get-Item $seqFile).Length -lt 1000) { "RESULT export FAILED: no reply file"; 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
pkill -f sp1-gpu-server 2>/dev/null; sleep 1; rm -f /tmp/sp1-cuda-*.sock
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,56 @@
# Proving v1, the 12 GB and 16 GB requirements, part 2 (5 October 2026): the GPU memory peak of one shard proof on PC 2's
# RTX 5090 WITH THE MINER RUNNING (the mine-and-prove case), the live prover switched off for the run; the full shard
# at S_p and the empty live shard; then the sp1-gpu-server's own option names (strings, --help) to find any memory knob
# the environment did not reach in part 1. Leaves the prover ON. Never stops 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 run: $(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-mem2' }; 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; 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; rm -f /tmp/sp1-cuda-*.sock
echo "RESULT miner_resident_mib `$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits | head -1) (the miner's working set before the prover starts)"
run() {
local name="`$1" fx="`$2"; shift 2
pkill -f sp1-gpu-server 2>/dev/null; sleep 2; rm -f /tmp/sp1-cuda-*.sock
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 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 err=`$(grep -iE "error|panick|out of memory|OOM|CUDA" "`$JOB/log-`$name-`$(basename `$fx .json).txt" | head -1 | cut -c1-200)
echo "RESULT mineprove cfg=`$name fixture=`$(basename `$fx .json) peak_mib=`$peak samples=`$(wc -l < "`$csv") wall_s=`$wall `${line:-no_result} exit=`$rc `${err:+err=`$err}"
}
run baseline "`$EMPTY"
run baseline "`$FULL"
run baseline "`$EMPTY"
pkill -f sp1-gpu-server 2>/dev/null; sleep 1; rm -f /tmp/sp1-cuda-*.sock
S="`$HOME/.sp1/bin/sp1-gpu-server"
echo "RESULT server_help `$(`$S --help 2>&1 | tr '\n' ' ' | cut -c1-900)"
echo "RESULT server_env_names `$(strings `$S | grep -oE '^(SP1|MOONGATE|CUDA|GPU|NVIDIA|MEM|TRACE|SHARD|ELEMENT|HEIGHT|FULL)[A-Z0-9_]{3,}$' | sort -u | tr '\n' ' ' | cut -c1-1200)"
echo "RESULT server_words `$(strings `$S | grep -iE 'gpu memory|vram|out of memory|memory pool|pool size|GiB|24 ?GB|16 ?GB|12 ?GB' | sort -u | head -20 | tr '\n' '|' | cut -c1-1200)"
echo "RESULT end_wsl `$(stamp)"
"@
$bashFile = Join-Path $job 'mem2.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,76 @@
# Proving v1, the 12 GB requirement (5 October 2026, Josh: "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; rm -f /tmp/sp1-cuda-*.sock
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; rm -f /tmp/sp1-cuda-*.sock
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; sleep 1; rm -f /tmp/sp1-cuda-*.sock
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,117 @@
# 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'
$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') }
$script:stateErr = ''
function State { try { Invoke-RestMethod -Uri "$base/api/state" -TimeoutSec 20 } catch { $script:stateErr = "$_"; $null } }
"RESULT app_url_file $(Stamp) $urlFile exists=$(Test-Path $urlFile) base_len=$($base.Length)"
$probe = State
"RESULT state_probe $(Stamp) ok=$($null -ne $probe) version=$($probe.version) cards=$(($probe.mining.cards | Measure-Object).Count) err=$script:stateErr"
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)"

View file

@ -0,0 +1,31 @@
# The root-socket fix for PC 2 (5 October 2026): a measurement job that ran igneum-prove-host as root inside WSL2 left
# /tmp/sp1-cuda-0.sock owned by root (and possibly a root sp1-gpu-server), so the app's prover (its own WSL user) fails
# every shard with "CudaClientError: Connect(PermissionDenied)". This job, as root: kills every sp1-gpu-server, removes
# the sockets, prints their owners before and after, then switches the app's prover off and on so its next shard
# starts a fresh server under the app's user. Never stops the miners. About 60 s.
$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)"
$bash = @'
echo "RESULT sockets_before $(ls -la /tmp/sp1-cuda-*.sock 2>&1 | tr '\n' ' ')"
echo "RESULT servers_before $(ps -eo user,pid,cmd | grep -E '[s]p1-gpu-server' | tr '\n' ' ' || echo none)"
pkill -f sp1-gpu-server 2>/dev/null; sleep 2; rm -f /tmp/sp1-cuda-*.sock
echo "RESULT sockets_after $(ls -la /tmp/sp1-cuda-*.sock 2>&1 | tr '\n' ' ')"
echo "RESULT servers_after $(ps -eo user,pid,cmd | grep -E '[s]p1-gpu-server' | tr '\n' ' ' || echo none)"
'@
$job = $env:IGNEUM_JOB_DIR; if (-not $job) { $job = $env:TEMP }
$bashFile = Join-Path $job 'socket-fix.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 -replace "`0", '').Trim()) 2>&1 | ForEach-Object { ($_ -replace "`0", '') }
"RESULT prove_off $(Stamp) $(Prove $false)"
Start-Sleep -Seconds 15
"RESULT prove_on $(Stamp) $(Prove $true)"
Start-Sleep -Seconds 40
$st = $null; try { $st = Invoke-RestMethod -Uri "$base/api/state" -TimeoutSec 20 } catch {}
"RESULT end $(Stamp) proving status: $($st.proving.status) message: $($st.proving.message)"

View file

@ -0,0 +1,78 @@
# Proving v1, the S_p curve (5 October 2026, the coordinator: peak GPU memory against shard size against time, full
# shards): one compressed shard proof per point on PC 2's RTX 5090, the pv1b host (--budget re-plans a fixture at a
# test budget), the live prover off for the run, the GPU server killed and its socket unlinked around every point.
# Points: the empty live shard (280 k cycles); block-56 (200 pgas transfer); the v1-budget shard (fees-v1-shards2,
# 30,000 pgas, about 4.7 M cycles); block 344 (27 M pgas, modexp) cut at one transaction (2.25 M pgas, about 20 M
# cycles) and two (about 40 M); the full shard at S_p (block-338-shard1, 60 M). Set MINERS=stopped when published with
# --stop-miners (the title says which); the script only reports what nvidia-smi sees before it starts.
$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 run: $(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-sp' }; 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')
$pkg = Join-Path $env:LOCALAPPDATA 'igneum\prove\igneum-prove-wsl2-pv1b\igneum-prove-wsl2'
if (-not (Test-Path $pkg)) { $pkg = Join-Path $env:LOCALAPPDATA 'igneum\prove\igneum-prove-wsl2-pv1b' }
$pkgW = WslPath $pkg
$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'; PKG='$pkgW'; DEST="`$HOME/igneum-prove-pv1"; LIVE_TARGET="`$HOME/igneum-prove/proving/igneum-prove/target"
pkill -f sp1-gpu-server 2>/dev/null; sleep 2; rm -f /tmp/sp1-cuda-*.sock
# the pv1b host (--budget) from the fetched package, built against the warm target dir, into /opt/igneum-pv1
rsync -a --delete --exclude target "`$PKG/package/" "`$DEST/"
find "`$DEST" -name target -prune -o -type f -exec touch {} + 2>/dev/null
cd "`$DEST/proving/igneum-prove"
t0=`$(date +%s)
if ! CARGO_TARGET_DIR="`$LIVE_TARGET" cargo build --release -p igneum-prove-host --features igneum-prove-host/cuda 2>&1 | tail -2; then echo "RESULT build FAILED"; exit 1; fi
cp "`$LIVE_TARGET/release/igneum-prove-host" /opt/igneum-pv1/
echo "RESULT build `$(stamp) exit 0 in `$(( `$(date +%s) - t0 )) s; host `$(sha256sum /opt/igneum-pv1/igneum-prove-host | cut -c1-16); live /opt/igneum untouched `$(sha256sum /opt/igneum/igneum-prove-host | cut -c1-16)"
H=/opt/igneum-pv1/igneum-prove-host; FX="`$DEST/proving/fixtures"
echo "RESULT miner_resident_mib `$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits | head -1)"
run() { # name fixture budget env...
local name="`$1" fx="`$2" budget="`$3"; shift 3
pkill -f sp1-gpu-server 2>/dev/null; sleep 2; rm -f /tmp/sp1-cuda-*.sock
local tag="`$name-`$(basename `$fx .json)"
local csv="`$JOB/smi-`$tag.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)
local barg=""; [ "`$budget" != "0" ] && barg="--budget `$budget"
env SP1_PROVER=cuda RUST_LOG=off "`$@" `$H "`$fx" --mode compressed --shard 0 `$barg --out "`$JOB/res-`$tag.json" > "`$JOB/log-`$tag.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 line=`$(grep -E "^RESULT compressed shard" "`$JOB/log-`$tag.txt" | tail -1 | sed -E 's/.*prove ([0-9.]+) s, proof ([0-9]+) bytes, verify ([0-9.]+) s, ([A-Z ]+);.*/prove_s=\1 verify_s=\3 \4/')
local cyc=`$(grep -E "^RESULT execute shard" "`$JOB/log-`$tag.txt" | tail -1 | sed -E 's/.*: ([0-9]+) cycles.*/\1/')
local plan=`$(grep -E "^RESULT plan:" "`$JOB/log-`$tag.txt" | sed -E 's/RESULT plan: ([0-9]+) shard.*/shards_per_block=\1/')
local sh=`$(grep -E "^RESULT shard 0 native" "`$JOB/log-`$tag.txt" | sed -E 's/.*pgas ([0-9]+).*input ([0-9]+) bytes.*/pgas=\1 witness_bytes=\2/')
local err=`$(grep -iE "^Error|panick|out of memory|OOM" "`$JOB/log-`$tag.txt" | head -1 | cut -c1-160)
echo "RESULT curve cfg=`$name fixture=`$(basename `$fx .json) budget=`$budget `$plan `$sh cycles=`${cyc:-na} peak_mib=`$peak samples=`$(wc -l < "`$csv") wall_s=`$wall `${line:-no_result} exit=`$rc env='`$*' `${err:+err=`$err}"
}
E25="ELEMENT_THRESHOLD=33554432 HEIGHT_THRESHOLD=1048576"
run base '$emptyW' 0
run base "`$FX/block-56-transfers.json" 0
run base "`$FX/fees-v1-shards2.json" 0
run base "`$FX/block-344-shards4.json" 2249264
run base "`$FX/block-344-shards4.json" 4500000
run base "`$FX/block-338-shard1.json" 0
run e25 "`$FX/fees-v1-shards2.json" 0 `$E25
run e25 "`$FX/block-344-shards4.json" 2249264 `$E25
run e25 "`$FX/block-338-shard1.json" 0 `$E25
pkill -f sp1-gpu-server 2>/dev/null; sleep 1; rm -f /tmp/sp1-cuda-*.sock
echo "RESULT curve_end `$(stamp)"
"@
$bashFile = Join-Path $job 'curve.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,332 @@
{
"v0": 60,
"v1": 120,
"segment": 4,
"unproven": 60,
"share_bps": 1000,
"steps": {
"status": {
"v0": "0x3c",
"v1": {
"activationDaa": "0x78",
"active": false,
"aggregatorId": null,
"aggregatorShareBps": "0x3e8",
"paidSegmentWei": "0x0",
"paidSegments": 0,
"pool": {
"entries": 0,
"failed": 0,
"pending": 0,
"verified": 0
},
"segmentBlocks": "0x4",
"segmentsInWindow": {
"pending": 0,
"proven": 0,
"unproven": 0
},
"shardProgramId": null,
"start": null,
"unprovenDaa": "0x3c"
}
},
"v1_start": {
"start": 119,
"tipDaa": 128
},
"case1": {
"segment": [
119,
122
],
"wei": "253611648000000000",
"paidSecs": 1.008,
"carrier": "0x81"
},
"case2": {
"segment": [
123,
126
],
"refused": "segment 123..126 does not chain to segment 119..122 (chain_len 4), which is proven (record paid at chain block 129)",
"paid": {
"carrierNumber": "0x87",
"chainLen": "0x8",
"keyHash": "0xc379ba2cdb239d2884d6bdb391d243ddfa869ab95ec4e72372e0e31fc9025fa0",
"payout": "0x4343434343434343434343434343434343434343",
"wei": "0x38505a6ce4a8000"
}
},
"case3": {
"unproven": [
127,
130
],
"restarted": [
131,
134
],
"status": {
"activationDaa": "0x78",
"active": true,
"aggregatorId": null,
"aggregatorShareBps": "0x3e8",
"paidSegmentWei": "0xa8f1428e9a42800",
"paidSegments": 3,
"pool": {
"entries": 3,
"failed": 0,
"pending": 0,
"verified": 0
},
"segmentBlocks": "0x4",
"segmentsInWindow": {
"pending": 15,
"proven": 3,
"unproven": 1
},
"shardProgramId": null,
"start": "0x77",
"unprovenDaa": "0x3c"
}
}
},
"checks": [
{
"name": "the node carries the v1 parameters",
"ok": true,
"detail": {
"activationDaa": "0x78",
"active": false,
"aggregatorId": null,
"aggregatorShareBps": "0x3e8",
"paidSegmentWei": "0x0",
"paidSegments": 0,
"pool": {
"entries": 0,
"failed": 0,
"pending": 0,
"verified": 0
},
"segmentBlocks": "0x4",
"segmentsInWindow": {
"pending": 0,
"proven": 0,
"unproven": 0
},
"shardProgramId": null,
"start": null,
"unprovenDaa": "0x3c"
}
},
{
"name": "every node agrees on the v1 start",
"ok": true,
"detail": 119
},
{
"name": "segment 0 is aligned at the start and pending",
"ok": true,
"detail": {
"first": "0x77",
"last": "0x7a",
"status": {
"deadline_daa": 183,
"status": "pending"
}
}
},
{
"name": "the native statement is the same on every node",
"ok": true,
"detail": "0x000000000000116f000000"
},
{
"name": "the aggregator share is a tenth of the segment credits",
"ok": true,
"detail": {
"expectedWei0": "253611648000000000",
"creditSum0": "2536116480000000000"
}
},
{
"name": "known-finished: segment 0 record accepted by n1",
"ok": true,
"detail": {
"accepted": true,
"block": "0xe03d6896eff5476fffb90d066efd77feb0e2eb9c527b967423b74bb5948f36d2",
"first": "0x77",
"keyHash": "0xc379ba2cdb239d2884d6bdb391d243ddfa869ab95ec4e72372e0e31fc9025fa0",
"last": "0x7a",
"new": true,
"reason": "accepted"
}
},
{
"name": "known-finished: segment 0 paid on n0 (relayed over p2p, verified in trust mode, carried)",
"ok": true,
"detail": {
"paid": {
"carrierNumber": "0x81",
"chainLen": "0x4",
"keyHash": "0xc379ba2cdb239d2884d6bdb391d243ddfa869ab95ec4e72372e0e31fc9025fa0",
"payout": "0x4343434343434343434343434343434343434343",
"wei": "0x38502733df10000"
},
"secs": 1.008
}
},
{
"name": "every node paid segment 0 the same",
"ok": true,
"detail": [
"0x38502733df10000",
"0x38502733df10000",
"0x38502733df10000"
]
},
{
"name": "the payout address holds the aggregator share",
"ok": true,
"detail": {
"balance": "253611648000000000"
}
},
{
"name": "segment 1 names segment 0 as its proven previous",
"ok": true,
"detail": {
"carrierNumber": "0x81",
"chainLen": "0x4",
"first": "0x77",
"keyHash": "0xc379ba2cdb239d2884d6bdb391d243ddfa869ab95ec4e72372e0e31fc9025fa0",
"last": "0x7a",
"proofHash": "0xed42fb45a37c81f151b2fde1b2b9b35ce0c76f7b59145eaa428f6760b739cae4",
"proofInPool": true
}
},
{
"name": "chain rule: a fresh-chain record for segment 1 is refused while segment 0 is proven",
"ok": true,
"detail": {
"accepted": false,
"block": "0x43b605649b1d40550fc280a520950a328b25e733b4efe9cc0a204e31e9351022",
"first": "0x7b",
"keyHash": "0xc379ba2cdb239d2884d6bdb391d243ddfa869ab95ec4e72372e0e31fc9025fa0",
"last": "0x7e",
"new": false,
"reason": "segment 123..126 does not chain to segment 119..122 (chain_len 4), which is proven (record paid at chain block 129)"
}
},
{
"name": "chain rule: the continuing record (chain_len 8) is accepted",
"ok": true,
"detail": {
"accepted": true,
"block": "0x43b605649b1d40550fc280a520950a328b25e733b4efe9cc0a204e31e9351022",
"first": "0x7b",
"keyHash": "0xc379ba2cdb239d2884d6bdb391d243ddfa869ab95ec4e72372e0e31fc9025fa0",
"last": "0x7e",
"new": true,
"reason": "accepted"
}
},
{
"name": "segment 1 paid with chain_len 8",
"ok": true,
"detail": {
"carrierNumber": "0x87",
"chainLen": "0x8",
"keyHash": "0xc379ba2cdb239d2884d6bdb391d243ddfa869ab95ec4e72372e0e31fc9025fa0",
"payout": "0x4343434343434343434343434343434343434343",
"wei": "0x38505a6ce4a8000"
}
},
{
"name": "segment 2 is pending with a deadline",
"ok": true,
"detail": {
"deadline_daa": 191,
"status": "pending"
}
},
{
"name": "known-failed: segment 3 cannot start a fresh chain while segment 2 is pending",
"ok": true,
"detail": {
"accepted": false,
"block": "0x4f502ffcbe86530352096707a93074ee8e1d43e3d2e94b06b8c71735c9eb0b46",
"first": "0x83",
"keyHash": "0xc379ba2cdb239d2884d6bdb391d243ddfa869ab95ec4e72372e0e31fc9025fa0",
"last": "0x86",
"new": false,
"reason": "segment 131..134 does not chain to segment 127..130 (chain_len 4), which is pending until DAA 191"
}
},
{
"name": "known-failed: segment 2 is unproven after its deadline",
"ok": true,
"detail": {
"deadline_daa": 191,
"status": "unproven"
}
},
{
"name": "known-failed: a late record for segment 2 pays nothing (refused as unproven)",
"ok": true,
"detail": {
"accepted": false,
"block": "0xb46575c11a6e42efd2cdbc8a450c0f169c069409a1f6e107c7418cea9c594623",
"first": "0x7f",
"keyHash": "0xc379ba2cdb239d2884d6bdb391d243ddfa869ab95ec4e72372e0e31fc9025fa0",
"last": "0x82",
"new": false,
"reason": "unproven: the record is carried at DAA 194, after the segment's deadline DAA 191"
}
},
{
"name": "segment 3 restarts the chain with a fresh-chain record after the unproven segment",
"ok": true,
"detail": {
"accepted": true,
"block": "0x4f502ffcbe86530352096707a93074ee8e1d43e3d2e94b06b8c71735c9eb0b46",
"first": "0x83",
"keyHash": "0xc379ba2cdb239d2884d6bdb391d243ddfa869ab95ec4e72372e0e31fc9025fa0",
"last": "0x86",
"new": true,
"reason": "accepted"
}
},
{
"name": "segment 3 paid with chain_len 4",
"ok": true,
"detail": {
"carrierNumber": "0xc1",
"chainLen": "0x4",
"keyHash": "0xc379ba2cdb239d2884d6bdb391d243ddfa869ab95ec4e72372e0e31fc9025fa0",
"payout": "0x4343434343434343434343434343434343434343",
"wei": "0x3850c0edd68a800"
}
},
{
"name": "the status counts proven and unproven segments",
"ok": true,
"detail": {
"pending": 15,
"proven": 3,
"unproven": 1
}
},
{
"name": "a v1 shard is paid 90% of its block share (one shard a block here)",
"ok": true,
"detail": {
"number": "0xc3",
"shardWei": "0x7ebcd81877b9c00",
"credit": "0x8cd1d3a96895800"
}
}
],
"ok": true
}