diff --git a/app/igneum-app/Cargo.toml b/app/igneum-app/Cargo.toml index 1c03ca9d3..deb95690c 100644 --- a/app/igneum-app/Cargo.toml +++ b/app/igneum-app/Cargo.toml @@ -16,6 +16,12 @@ path = "src/main.rs" name = "igneum-ota-sign" path = "src/bin/ota-sign.rs" +# the Windows proof verifier wrapper (release 0.3.6): the node runs it as IGNEUM_PROOF_VERIFIER and it runs +# igneum-prove-host inside WSL2; shipped next to the engine by packaging/windows/make-payload.sh +[[bin]] +name = "igneum-prove-verify" +path = "src/bin/prove-verify.rs" + [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/app/igneum-app/src/bin/prove-verify.rs b/app/igneum-app/src/bin/prove-verify.rs new file mode 100644 index 000000000..0509b2404 --- /dev/null +++ b/app/igneum-app/src/bin/prove-verify.rs @@ -0,0 +1,148 @@ +//! igneum-prove-verify: the Windows wrapper the node's proof pool verifier calls (spec 7.7 item 4, release 0.3.6). +//! +//! The node on a PC is a Windows exe; the SP1 host (`igneum-prove-host`) is Linux-only and lives inside WSL2. +//! The engine sets `IGNEUM_PROOF_VERIFIER=` for its node, and the node runs +//! `igneum-prove-verify.exe --mode verify --proof --statement 0x...`. This wrapper converts the proof +//! path with `wslpath -a` inside Ubuntu-24.04, finds the host in the same order the prover uses +//! (src/wslhost.rs: the payload's wsl2/bin, the setup-wsl.sh build, the old layout, /opt/igneum), runs it +//! there with the same arguments and exits with its exit code. +//! +//! igneum-prove-verify --probe prints `HOST ` and exits 0 when a host is found; exits 2 otherwise +//! igneum-prove-verify runs the host; exit 2 when there is no host or WSL did not answer +//! +//! Exit 2 is reserved for "no host": the engine probes before it sets the variable, so a node never gets a +//! verifier that cannot run. The wrapper never trusts a proof it did not verify. + +#[path = "../wslhost.rs"] +mod wslhost; + +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +const NO_HOST: i32 = 2; + +fn wsl_exe() -> PathBuf { + #[cfg(windows)] + { + let root = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".into()); + PathBuf::from(format!("{root}\\System32\\wsl.exe")) + } + #[cfg(not(windows))] + { + PathBuf::from("wsl") + } +} + +fn quiet(cmd: &mut Command) -> &mut Command { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW + } + cmd +} + +fn shell_quote(s: &str) -> String { + format!("'{}'", s.replace('\'', "'\\''")) +} + +/// `wslpath -a ` inside the distribution; the drive-letter mapping when wslpath did not answer. +fn to_wsl(p: &Path) -> String { + let out = quiet(&mut Command::new(wsl_exe())).args(["-d", wslhost::DISTRO, "--", "wslpath", "-a"]).arg(p).stdin(Stdio::null()).output(); + if let Ok(o) = out { + if o.status.success() { + let s = String::from_utf8_lossy(&o.stdout).trim().to_string(); + if s.starts_with('/') { + return s; + } + } + } + wslhost::wsl_path(p) +} + +/// The host's arguments with `--proof ` rewritten for WSL. Pure, so it has a test. +pub fn rewrite_args String>(args: &[String], to_wsl: F) -> Vec { + let mut out = Vec::with_capacity(args.len()); + let mut i = 0; + while i < args.len() { + let a = &args[i]; + if a == "--proof" && i + 1 < args.len() { + out.push(a.clone()); + out.push(to_wsl(Path::new(&args[i + 1]))); + i += 2; + continue; + } + if let Some(v) = a.strip_prefix("--proof=") { + out.push(format!("--proof={}", to_wsl(Path::new(v)))); + i += 1; + continue; + } + out.push(a.clone()); + i += 1; + } + out +} + +/// The script that finds the host and replaces the shell with it: the host's exit code is the script's. With no +/// host, a line on stderr naming the places looked at, and exit 2. +pub fn run_script(bin_dir: &Path, host_args: &[String]) -> String { + let lookup = wslhost::lookup_script(bin_dir); + let args: Vec = host_args.iter().map(|a| shell_quote(a)).collect(); + format!( + "h=$({lookup}); if [ -n \"$h\" ]; then exec \"$h\" {}; fi; echo 'igneum-prove-verify: no igneum-prove-host in WSL2 (looked at: {})' >&2; exit {NO_HOST}", + args.join(" "), + wslhost::candidates_text(bin_dir).replace('\'', "'\\''") + ) +} + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + let bin_dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())).unwrap_or_default(); + if args.iter().any(|a| a == "--version" || a == "-V") { + println!("igneum-prove-verify {}", env!("CARGO_PKG_VERSION")); + return; + } + if args.iter().any(|a| a == "--probe") { + let out = quiet(&mut Command::new(wsl_exe())).args(["-d", wslhost::DISTRO, "--", "bash", "-lc", &wslhost::lookup_script(&bin_dir)]).stdin(Stdio::null()).output(); + let host = out.ok().filter(|o| o.status.success()).map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()).unwrap_or_default(); + if host.is_empty() { + eprintln!("igneum-prove-verify: no igneum-prove-host in WSL2 ({}) (looked at: {})", wslhost::DISTRO, wslhost::candidates_text(&bin_dir)); + std::process::exit(NO_HOST); + } + println!("HOST {host}"); + return; + } + let host_args = rewrite_args(&args, to_wsl); + let script = run_script(&bin_dir, &host_args); + let status = quiet(&mut Command::new(wsl_exe())).args(["-d", wslhost::DISTRO, "--", "bash", "-lc", &script]).stdin(Stdio::null()).status(); + match status { + Ok(st) => std::process::exit(st.code().unwrap_or(1)), + Err(e) => { + eprintln!("igneum-prove-verify: WSL2 did not start ({}): {e}", wsl_exe().display()); + std::process::exit(NO_HOST); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn only_the_proof_path_is_rewritten() { + let args: Vec = ["--mode", "verify", "--proof", "C:\\Users\\x\\p.bin", "--statement", "0xab"].iter().map(|s| s.to_string()).collect(); + let out = rewrite_args(&args, |p| wslhost::wsl_path(p)); + assert_eq!(out, vec!["--mode", "verify", "--proof", "/mnt/c/Users/x/p.bin", "--statement", "0xab"]); + let args: Vec = vec!["--proof=D:\\q.bin".into()]; + assert_eq!(rewrite_args(&args, |p| wslhost::wsl_path(p)), vec!["--proof=/mnt/d/q.bin"]); + } + + #[test] + fn the_script_execs_the_first_host_and_exits_2_without_one() { + let s = run_script(Path::new("C:\\Igneum"), &["--mode".into(), "verify".into(), "--statement".into(), "0xab".into()]); + assert!(s.starts_with("h=$(for f in '/mnt/c/Igneum/wsl2/bin/igneum-prove-host' ~/igneum-prove/proving/igneum-prove/target/release/igneum-prove-host ~/igneum-prove/target/release/igneum-prove-host '/opt/igneum/igneum-prove-host'; do"), "{s}"); + assert!(s.contains("exec \"$h\" '--mode' 'verify' '--statement' '0xab'; fi;")); + assert!(s.ends_with("exit 2")); + assert!(s.contains("looked at: /mnt/c/Igneum/wsl2/bin/igneum-prove-host, ~/igneum-prove/proving/")); + } +} diff --git a/app/igneum-app/src/config.rs b/app/igneum-app/src/config.rs index 1b36edb57..bcb8ca9bb 100644 --- a/app/igneum-app/src/config.rs +++ b/app/igneum-app/src/config.rs @@ -83,6 +83,10 @@ pub struct Settings { /// Lifetime dev-fee blocks this machine found (the miner's `dev-fee block` lines), carried across runs. #[serde(default)] pub fee_total: u64, + /// Devnet only: when no verifier is found next to the engine, start the node with `IGNEUM_PROOF_VERIFY=trust` + /// so it includes proof records it never verified (src/verifier.rs). Default off; a found verifier always wins. + #[serde(default)] + pub proof_verify_trust: bool, } fn one() -> u32 { @@ -94,7 +98,7 @@ fn yes() -> bool { impl Default for Settings { fn default() -> Settings { - Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, installed_at: 0, dev_fee: true, fee_total: 0 } + 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 } } } diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index cd6cfa4cb..cdad711d9 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -70,6 +70,9 @@ pub enum Cmd { SweepHelperDone(Result<(), String>), /// a direct `nvidia-smi -pl` for the sweep finished: what it printed SweepCapSet(String), + /// restart the node with the verifier decided again (src/verifier.rs): the trust setting changed, or the + /// prover found a host that was not there when the node started + RestartNode(String), Quit, } @@ -110,7 +113,7 @@ impl Shared { st.mining.accepted_total = settings.accepted_total; st.mining.fee_total = settings.fee_total; st.address = address_state(&settings, &wallet_path); - st.settings = crate::state::SettingsState { identities: settings.identities, vote: settings.vote, start_at_login: crate::platform::start_at_login_is_on(), auto_update: settings.auto_update, remote_jobs: settings.remote_jobs, prove: settings.prove, sweep: settings.sweep, dev_fee: settings.dev_fee }; + st.settings = crate::state::SettingsState { identities: settings.identities, vote: settings.vote, start_at_login: crate::platform::start_at_login_is_on(), auto_update: settings.auto_update, remote_jobs: settings.remote_jobs, prove: settings.prove, sweep: settings.sweep, dev_fee: settings.dev_fee, proof_verify_trust: settings.proof_verify_trust }; st.dev_fee = crate::state::DevFeeState { on: settings.dev_fee, percent: if settings.dev_fee { 1 } else { 0 }, address: String::new(), line: String::new() }; st.live_page = packaged.live_page.clone(); st.finality.message = "waiting for the miner".into(); @@ -265,8 +268,9 @@ impl Shared { Ok(json!({ "ok": true })) } - pub fn apply_settings(&self, identities: Option, vote: Option, login: Option, address: Option<&str>, display_name: Option<&str>, dev_fee: Option) -> Result { + pub fn apply_settings(&self, identities: Option, vote: Option, login: Option, address: Option<&str>, display_name: Option<&str>, dev_fee: Option, proof_verify_trust: Option) -> Result { let mut restart = Vec::new(); + let mut restart_node: Option = None; { let mut s = self.settings.lock().unwrap(); if let Some(n) = display_name { @@ -292,6 +296,12 @@ impl Shared { restart.push(if v { "dev fee on (1 block in 100)".into() } else { "dev fee off".into() }); } } + if let Some(v) = proof_verify_trust { + if v != s.proof_verify_trust { + s.proof_verify_trust = v; + restart_node = Some(if v { "proof trust mode on (devnet only)".into() } else { "proof trust mode off".into() }); + } + } if let Some(a) = address { let a = a.trim().to_ascii_lowercase(); if !a.is_empty() && a != s.address { @@ -310,6 +320,7 @@ impl Shared { st.settings.identities = s.identities; st.settings.vote = s.vote; st.settings.dev_fee = s.dev_fee; + st.settings.proof_verify_trust = s.proof_verify_trust; st.dev_fee.on = s.dev_fee; st.dev_fee.percent = if s.dev_fee { 1 } else { 0 }; st.address = address_state(&s, &self.wallet_path); @@ -322,7 +333,10 @@ impl Shared { if !restart.is_empty() { self.send(Cmd::RestartMiners(restart.join(", "))); } - Ok(json!({ "ok": true, "restart": !restart.is_empty() })) + if let Some(why) = restart_node.clone() { + self.send(Cmd::RestartNode(why)); + } + Ok(json!({ "ok": true, "restart": !restart.is_empty(), "restart_node": restart_node.is_some() })) } } @@ -397,6 +411,8 @@ pub struct Engine { node_restarts: u32, node_log: Option, node_last_reading: Option, + /// the proof verifier decided for the node (src/verifier.rs); None = decide at the next node start + verifier: Option, sync_prev: Option, sync_stable_since: Option, last_sync_check: Instant, @@ -491,6 +507,7 @@ impl Engine { node_restarts: 0, node_log: None, node_last_reading: None, + verifier: None, sync_prev: None, sync_stable_since: None, last_sync_check: now, @@ -710,6 +727,15 @@ impl Engine { m.restart_at = Some(Instant::now()); } } + Cmd::RestartNode(why) => { + self.verifier = None; + if self.node_external { + self.shared.event("info", &format!("{why}; the node is external, so the app cannot restart it")); + } else if self.node.is_some() || self.node_restart_at.is_some() { + self.shared.event("info", &format!("{why}; the node restarts")); + self.restart_node(&why, Duration::from_secs(2)); + } + } Cmd::CheckUpdate => self.ota.check_now(&self.shared), Cmd::InstallUpdate => self.ota.install_now(&self.shared), Cmd::AutoUpdate(on) => self.ota.set_auto(&self.shared, on), @@ -971,6 +997,8 @@ impl Engine { let mut st = self.st(); st.node.state = "syncing".into(); st.node.message = "external node".into(); + st.proving.verifier_reason = "external node: the app did not start it, so it set no verifier".into(); + st.proving.verifier_note = crate::verifier::note("unknown", "", "", true); } else { self.start_node(); } @@ -1037,9 +1065,11 @@ impl Engine { let seg = if self.node_starts > 1 { format!("-r{}", self.node_starts) } else { String::new() }; let log = self.shared.runtime.log_dir.join(format!("node-{}{seg}.log", self.stamp)); let args = self.node_args(); - match procs::spawn(Source::Node, &self.bins.node, &args, None, &log, &self.lines_tx, &[]) { + let verifier = self.node_verifier(); + match procs::spawn(Source::Node, &self.bins.node, &args, None, &log, &self.lines_tx, &verifier.env) { Ok(p) => { self.shared.log(&format!("igneumd started (pid {}): {}", p.pid(), p.cmdline)); + self.shared.log(&format!("node proof verifier: {} ({})", verifier.mode, verifier.detail)); let mut st = self.st(); st.node.pid = p.pid(); st.node.state = "starting".into(); @@ -1065,6 +1095,26 @@ impl Engine { } } + /// The proof verifier for this node start (spec 7.7 item 4; src/verifier.rs), decided once and kept across + /// restarts until a RestartNode command asks again. The Windows probe runs WSL, so the result is cached. + fn node_verifier(&mut self) -> crate::verifier::Verifier { + if self.verifier.is_none() { + let trust = self.shared.settings.lock().unwrap().proof_verify_trust; + let v = crate::verifier::resolve(&self.bins.dir, trust); + if v.mode == "trust" { + self.shared.event("info", "devnet only: the node trusts proof records without verifying them (Settings)"); + } + self.verifier = Some(v); + } + let v = self.verifier.clone().unwrap(); + let mut st = self.st(); + st.proving.verifier_set = v.set_text(); + st.proving.verifier_reason = if v.mode == "command" { String::new() } else { v.detail.clone() }; + let (mode, set, reason) = (st.proving.verifier_mode.clone(), st.proving.verifier_set.clone(), st.proving.verifier_reason.clone()); + st.proving.verifier_note = crate::verifier::note(&mode, &set, &reason, false); + v + } + fn stop_node(&mut self) { if let Some(mut n) = self.node.take() { self.shared.log("stopping the node"); diff --git a/app/igneum-app/src/main.rs b/app/igneum-app/src/main.rs index 41e7d9ce2..6addd3103 100644 --- a/app/igneum-app/src/main.rs +++ b/app/igneum-app/src/main.rs @@ -28,6 +28,8 @@ mod jobs; mod jobrun; mod jobbuild; mod prover; +mod verifier; +mod wslhost; mod sweep; mod watchdog; diff --git a/app/igneum-app/src/prover.rs b/app/igneum-app/src/prover.rs index 0f66f72a3..71186c404 100644 --- a/app/igneum-app/src/prover.rs +++ b/app/igneum-app/src/prover.rs @@ -11,11 +11,17 @@ //! (`igneum_submitProofRecord`). The tile shows assigned, proving, submitted, paid. //! //! Where the prover runs: macOS runs the host next to the engine on the CPU (slow, shown as slow). Windows runs -//! it inside WSL2 (SP1's CUDA prover is Linux-only): the engine looks for `~/igneum-prove/target/release/ -//! igneum-prove-host` in the Ubuntu-24.04 distribution; without it the tile says "proving needs the WSL2 setup, -//! 20 minutes, Set up" and Set up runs proving/windows-wsl2/setup-wsl.sh from the payload (`wsl2/` next to the -//! engine). Linux runs the host next to the engine. Everything the prover needs on a PC is in the payload or -//! installed by that script; there is no other channel. +//! it inside WSL2 (SP1's CUDA prover is Linux-only): the engine looks for `igneum-prove-host` in the Ubuntu-24.04 +//! distribution in the order of src/wslhost.rs (the payload's wsl2/bin, the setup-wsl.sh build under +//! `~/igneum-prove/proving/igneum-prove/target/release`, the old `~/igneum-prove/target/release`, `/opt/igneum`); +//! without it the tile says "proving needs the WSL2 setup, 20 minutes, Set up" and names the paths it looked at, +//! and Set up runs proving/windows-wsl2/setup-wsl.sh from the payload (`wsl2/` next to the engine). Linux runs +//! the host next to the engine. Everything the prover needs on a PC is in the payload or installed by that +//! script; there is no other channel. +//! +//! The same thread reads the node's verifier state every 30 s (`igneum_getProvingStatus().verifier`, spec 7.7 +//! item 4) whether proving is on or off, so the tile and `/api/state` say when this node relays proof records +//! but never includes them (src/verifier.rs decides what the node spawn sets). use crate::engine::Shared; use serde_json::{json, Value}; @@ -80,15 +86,7 @@ pub fn choose(work: &[Work], attempted: &HashSet<(String, u32)>) -> Option pick(true).or_else(|| pick(false)) } -/// A Windows path as WSL sees it: `C:\Users\x\f` -> `/mnt/c/Users/x/f`. -pub fn wsl_path(p: &Path) -> String { - let s = p.display().to_string().replace('\\', "/"); - if s.len() > 2 && s.as_bytes()[1] == b':' { - format!("/mnt/{}{}", s[..1].to_ascii_lowercase(), &s[2..]) - } else { - s - } -} +pub use crate::wslhost::wsl_path; /// The identity labels this machine mines with (the vote keys the node assigns shards to): one per enabled /// card, `-`, and `-1..N` per identity when a card runs more than one. @@ -154,12 +152,11 @@ fn find_tools(bin_dir: &Path) -> Result { let miner = bin_dir.join(if cfg!(windows) { "igneum-miner.exe" } else { "igneum-miner" }); if cfg!(windows) { // the SP1 host runs inside WSL2 (Ubuntu-24.04): the Linux binaries the payload ships under wsl2\bin\ (seen - // from Ubuntu as /mnt//.../wsl2/bin), else one built there by setup-wsl.sh - let shipped = wsl_path(&bin_dir.join("wsl2").join("bin").join("igneum-prove-host")); - let script = format!("for f in '{shipped}' ~/igneum-prove/target/release/igneum-prove-host /opt/igneum/igneum-prove-host; do [ -x \"$f\" ] && {{ echo \"$f\"; break; }}; done; command -v nvidia-smi >/dev/null && echo cuda"); - let mut probe_cmd = Command::new(crate::platform::tool("wsl")); - probe_cmd.args(["-d", "Ubuntu-24.04", "--", "bash", "-lc", &script]); - let probe = crate::platform::quiet(&mut probe_cmd).output(); // hidden: this probe opened a console window on PC 2 every minute (5 October 2026) + // from Ubuntu as /mnt//.../wsl2/bin), else one built there by setup-wsl.sh, else a hand install + // (the order and the list are src/wslhost.rs, shared with igneum-prove-verify.exe) + let script = format!("{}; command -v nvidia-smi >/dev/null && echo cuda", crate::wslhost::lookup_script(bin_dir)); + let probe = crate::platform::quiet(&mut Command::new(crate::platform::tool("wsl"))).args(["-d", crate::wslhost::DISTRO, "--", "bash", "-lc", &script]).output(); + let answered = probe.is_ok(); let text = probe.map(|o| String::from_utf8_lossy(&o.stdout).to_string()).unwrap_or_default(); let host = text.lines().find(|l| l.contains("igneum-prove-host")).map(|l| PathBuf::from(l.trim())); let setup = bin_dir.join("wsl2").join("setup-wsl.sh"); @@ -168,7 +165,7 @@ fn find_tools(bin_dir: &Path) -> Result { let export = host.parent().map(|d| d.join("igneum-prove-export")).unwrap_or_default(); Ok(Tools { host, export, miner, wsl: true, setup_script: setup.exists().then_some(setup), cuda: text.contains("cuda") }) } - None => Err(format!("proving needs the WSL2 setup, 20 minutes, Set up{}", if setup.exists() { "" } else { " (setup script missing from the payload)" })), + None => Err(probe_message(bin_dir, answered, setup.exists())), } } else { let host = bin_dir.join("igneum-prove-host"); @@ -180,6 +177,43 @@ fn find_tools(bin_dir: &Path) -> Result { } } +/// The tile's message when no host is found inside WSL2: what to do, and the paths that were looked at. +pub fn probe_message(bin_dir: &Path, wsl_answered: bool, setup_present: bool) -> String { + let looked = crate::wslhost::candidates_text(bin_dir); + if !wsl_answered { + return format!("proving needs the WSL2 setup, 20 minutes, Set up (WSL2 with {} did not answer; no igneum-prove-host at {looked})", crate::wslhost::DISTRO); + } + format!("proving needs the WSL2 setup, 20 minutes, Set up (no igneum-prove-host at {looked}{})", if setup_present { "" } else { "; setup script missing from the payload" }) +} + +/// Reads the node's verifier state (`igneum_getProvingStatus`): the verifier word, the pool counts, the tile's +/// note. Nothing changes when the node does not answer (the mode stays as it was, "unknown" at first). +fn read_verifier(shared: &Shared) { + let external = shared.state.lock().unwrap().node.message == "external node"; + match evm_rpc(shared, "igneum_getProvingStatus", json!([]), Duration::from_secs(5)) { + Ok(v) => { + let report = v["verifier"].as_str().unwrap_or("").to_string(); + let mode = crate::verifier::mode_of_report(&report); + let count = |k: &str| v["pool"][k].as_u64().unwrap_or(0); + let (entries, verified, failed) = (count("entries"), count("verified"), count("failed")); + let mut st = shared.state.lock().unwrap(); + let changed = st.proving.verifier_mode != mode; + st.proving.verifier = report; + st.proving.verifier_mode = mode.into(); + st.proving.pool_entries = entries; + st.proving.pool_verified = verified; + st.proving.pool_failed = failed; + let (set, reason) = (st.proving.verifier_set.clone(), st.proving.verifier_reason.clone()); + st.proving.verifier_note = crate::verifier::note(mode, &set, &reason, external); + drop(st); + if changed { + shared.log(&format!("node proof verifier reported: {mode}{}", if mode == "off" { " (this node relays proof records and never includes them)" } else { "" })); + } + } + Err(_) => {} + } +} + /// Runs the host or the exporter: directly, or through WSL on Windows. Output goes to `log`; the child is polled /// every second and killed when the app quits, the setting goes off or `limit` passes (a proof must never outlive /// the app). Returns (exit ok, output). @@ -188,7 +222,7 @@ fn run_tool(shared: &Shared, t: &Tools, exe: &Path, args: &[String], env: &[(&st let mut c = Command::new(crate::platform::tool("wsl")); let envs: String = env.iter().map(|(k, v)| format!("{k}={v} ")).collect(); let line = format!("{envs}{} {}", exe.display(), args.iter().map(|a| format!("'{a}'")).collect::>().join(" ")); - c.args(["-d", "Ubuntu-24.04", "--", "bash", "-lc", &line]); + c.args(["-d", crate::wslhost::DISTRO, "--", "bash", "-lc", &line]); c } else { let mut c = Command::new(exe); @@ -259,16 +293,23 @@ fn loop_forever(shared: Arc, bin_dir: PathBuf) { let mut tools: Option = None; let mut last_probe = Instant::now() - Duration::from_secs(600); let mut submitted: Vec<(u64, String, u32, u128)> = Vec::new(); + let mut last_verifier_read = Instant::now() - Duration::from_secs(600); + let mut asked_restart = false; loop { std::thread::sleep(Duration::from_secs(10)); let enabled = shared.settings.lock().unwrap().prove; - let (synced, quitting) = { + let (synced, quitting, node_up) = { let st = shared.state.lock().unwrap(); - (st.node.synced, st.quitting) + (st.node.synced, st.quitting, matches!(st.node.state.as_str(), "syncing" | "synced")) }; if quitting { return; } + // the node's verifier state, proving on or off: a relaying-only node must say so on the tile + if node_up && last_verifier_read.elapsed() >= Duration::from_secs(30) { + last_verifier_read = Instant::now(); + read_verifier(&shared); + } if !enabled { set(&shared, |p| { p.enabled = false; @@ -286,6 +327,13 @@ fn loop_forever(shared: Arc, bin_dir: PathBuf) { p.setup_hint = String::new(); p.backend = if t.cuda { "cuda".into() } else { "cpu".into() }; }); + // Windows: the node was started before the WSL2 host existed (Set up ran since), so it verifies + // nothing; one restart lets src/verifier.rs find the host through igneum-prove-verify.exe + let node_has_none = shared.state.lock().unwrap().proving.verifier_set.is_empty(); + if t.wsl && node_has_none && !asked_restart { + asked_restart = true; + shared.send(crate::engine::Cmd::RestartNode("the WSL2 prover is installed now; the node restarts to verify proof records".into())); + } tools = Some(t); } Err(e) => { @@ -466,7 +514,7 @@ pub fn setup(shared: &Shared) -> Result { } let line = format!("bash {}", wsl_path(&script)); let mut c = Command::new(crate::platform::tool("cmd")); - c.args(["/c", "start", "", &crate::platform::tool("wsl").display().to_string(), "-d", "Ubuntu-24.04", "--", "bash", "-lc", &line]); + c.args(["/c", "start", "", &crate::platform::tool("wsl").display().to_string(), "-d", crate::wslhost::DISTRO, "--", "bash", "-lc", &line]); c.spawn().map_err(|e| e.to_string())?; shared.event("proving", "WSL2 prover setup started in its own window"); Ok(json!({ "ok": true })) @@ -499,8 +547,10 @@ mod tests { } #[test] - fn wsl_paths() { - assert_eq!(wsl_path(Path::new("C:\\Users\\[user]\\AppData\\Local\\igneum\\app\\proving\\seq.json")), "/mnt/c/Users/[user]/AppData/Local/igneum/app/proving/seq.json"); - assert_eq!(wsl_path(Path::new("/tmp/x")), "/tmp/x"); + fn the_probe_message_names_every_path_it_looked_at() { + let m = probe_message(Path::new("C:\\Igneum"), true, true); + assert!(m.starts_with("proving needs the WSL2 setup, 20 minutes, Set up (no igneum-prove-host at /mnt/c/Igneum/wsl2/bin/igneum-prove-host, ~/igneum-prove/proving/igneum-prove/target/release/igneum-prove-host, ~/igneum-prove/target/release/igneum-prove-host, /opt/igneum/igneum-prove-host)"), "{m}"); + assert!(probe_message(Path::new("C:\\Igneum"), true, false).contains("setup script missing from the payload")); + assert!(probe_message(Path::new("C:\\Igneum"), false, true).contains("Ubuntu-24.04 did not answer")); } } diff --git a/app/igneum-app/src/server.rs b/app/igneum-app/src/server.rs index 4cf06e7cf..0849d6834 100644 --- a/app/igneum-app/src/server.rs +++ b/app/igneum-app/src/server.rs @@ -258,7 +258,8 @@ fn api_post(shared: &Arc, path: &str, body: Value) -> Result shared.set_prove(body.get("on").and_then(|v| v.as_bool()).unwrap_or(false)), "/api/prove/setup" => crate::prover::setup(shared), diff --git a/app/igneum-app/src/state.rs b/app/igneum-app/src/state.rs index 6ce9846bb..faaa8199c 100644 --- a/app/igneum-app/src/state.rs +++ b/app/igneum-app/src/state.rs @@ -147,6 +147,21 @@ pub struct ProvingState { pub last_prove_s: f64, pub last_paid: String, pub message: String, + // the node's proof verifier (src/verifier.rs; spec 7.7 item 4): a node without one relays and never includes + /// the node's own report, `igneum_getProvingStatus().verifier` (`Off`, `Trust`, `Command("...")`); empty until read + pub verifier: String, + /// off | trust | command | unknown, from the report + pub verifier_mode: String, + /// what the app passed its node: `command:`, `trust`, or empty when it set nothing + pub verifier_set: String, + /// why the app set nothing (the paths it looked at), or the trust warning + pub verifier_reason: String, + /// the sentence on the tile for the state above + pub verifier_note: String, + /// the node's proof pool: records held, verified, rejected + pub pool_entries: u64, + pub pool_verified: u64, + pub pool_failed: u64, } #[derive(Clone, Serialize, Default)] @@ -194,6 +209,8 @@ pub struct SettingsState { pub sweep: bool, /// the miner software's dev fee switch (settings; `--dev-fee 0` when off) pub dev_fee: bool, + /// devnet only: the node trusts proof records without a verifier (`IGNEUM_PROOF_VERIFY=trust`) + pub proof_verify_trust: bool, } /// One remote job this machine ran (the ledger entry), for the Settings history and the last-job strip. diff --git a/app/igneum-app/src/verifier.rs b/app/igneum-app/src/verifier.rs new file mode 100644 index 000000000..79725555e --- /dev/null +++ b/app/igneum-app/src/verifier.rs @@ -0,0 +1,174 @@ +//! The proof verifier the engine gives its node (spec 7.7 item 4, docs/plans/release-0.3.6.md item 1). +//! +//! The node keeps a proof pool and offers only verified records to its block templates; without a verifier it +//! relays and stores records and never includes one, so proofs are never paid. The node reads two variables +//! (`VerifyMode::from_env` in the node's exec/src/proving.rs): `IGNEUM_PROOF_VERIFIER=` runs +//! ` --mode verify --proof --statement 0x..` per proof; `IGNEUM_PROOF_VERIFY=trust` treats every +//! record as verified. This module decides which, once per node start: +//! +//! macOS, Linux `igneum-prove-host` next to the engine's binaries (the DMG ships it in Contents/Resources/bin) +//! Windows `igneum-prove-verify.exe` next to the engine (src/bin/prove-verify.rs), which runs the host +//! inside WSL2; it is set only when its `--probe` finds a host, so a node never gets a verifier +//! that cannot run +//! trust never by default; only the setting `proof_verify_trust` (shown as "devnet only") and only +//! when no verifier was found, so a real verifier always wins over trust +//! +//! What was decided is on the proving tile and in `/api/state` (`proving.verifier_set`, `proving.verifier_reason`); +//! the node's own report (`igneum_getProvingStatus().verifier`) is polled beside it (src/prover.rs). + +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::Duration; + +/// What the engine passes to the node. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Verifier { + /// "command" | "trust" | "off" + pub mode: &'static str, + /// the environment for the node spawn (empty when off) + pub env: Vec<(String, String)>, + /// for the tile: the verifier path, or why there is none + pub detail: String, +} + +impl Verifier { + /// `/api/state` `proving.verifier_set`: `command:`, `trust`, or empty. + pub fn set_text(&self) -> String { + match self.mode { + "command" => format!("command:{}", self.detail), + "trust" => "trust".into(), + _ => String::new(), + } + } +} + +/// How long the Windows probe may take: WSL's first start of the day can take several seconds. +const PROBE_LIMIT: Duration = Duration::from_secs(45); + +/// Decides the verifier for one node start. `trust` is the `proof_verify_trust` setting. +pub fn resolve(bin_dir: &Path, trust: bool) -> Verifier { + let found = find(bin_dir); + match found { + Ok(path) => Verifier { mode: "command", env: vec![("IGNEUM_PROOF_VERIFIER".into(), path.display().to_string())], detail: path.display().to_string() }, + Err(reason) if trust => Verifier { mode: "trust", env: vec![("IGNEUM_PROOF_VERIFY".into(), "trust".into())], detail: format!("devnet only: records are trusted without verification ({reason})") }, + Err(reason) => Verifier { mode: "off", env: vec![], detail: reason }, + } +} + +/// The verifier executable, or why there is none. +fn find(bin_dir: &Path) -> Result { + if cfg!(windows) { + let wrapper = bin_dir.join("igneum-prove-verify.exe"); + if !wrapper.exists() { + return Err(format!("igneum-prove-verify.exe is not next to the engine ({})", bin_dir.display())); + } + let out = crate::detect::run_timeout(crate::platform::quiet(&mut Command::new(&wrapper)).arg("--probe"), None, PROBE_LIMIT); + match out { + Some(text) => match text.lines().find(|l| l.starts_with("HOST ")) { + Some(_) => Ok(wrapper), + None => Err(format!("the WSL2 prover is not installed (looked at {}); Set up on the Proving tile installs it", crate::wslhost::candidates_text(bin_dir))), + }, + None => Err(format!("igneum-prove-verify.exe --probe did not answer within {} s (is WSL2 with {} installed?)", PROBE_LIMIT.as_secs(), crate::wslhost::DISTRO)), + } + } else { + let host = bin_dir.join("igneum-prove-host"); + if host.exists() { + Ok(host) + } else { + Err(format!("igneum-prove-host is not next to the engine ({})", bin_dir.display())) + } + } +} + +/// The node's `igneum_getProvingStatus().verifier` text (`Off`, `Trust`, `Command("...")`) as a word. +pub fn mode_of_report(report: &str) -> &'static str { + let r = report.trim(); + if r.eq_ignore_ascii_case("off") { + "off" + } else if r.eq_ignore_ascii_case("trust") { + "trust" + } else if r.starts_with("Command") { + "command" + } else { + "unknown" + } +} + +/// The sentence on the proving tile for the node's reported mode and what the app set. `external` = the app did +/// not start this node. +pub fn note(report_mode: &str, set: &str, reason: &str, external: bool) -> String { + match report_mode { + "command" => "This node verifies proof records with igneum-prove-host and includes the verified ones in its blocks.".into(), + "trust" => "Devnet only: this node trusts proof records without verifying them and includes them in its blocks.".into(), + "off" => { + let why = if external { + "the app did not start this node, so it set no verifier".to_string() + } else if !set.is_empty() { + format!("the app set a verifier ({set}) but the node reports none; an older node build, or it has not restarted since") + } else if reason.is_empty() { + "no verifier was set".to_string() + } else { + reason.to_string() + }; + format!("This node relays proofs but does not verify them, so it never includes a proof record in its blocks: {why}.") + } + _ => { + if set.is_empty() && !reason.is_empty() && !external { + format!("Verifier state not read yet. The app set no verifier: {reason}.") + } else { + "Verifier state not read yet (the node has not answered).".into() + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolve_never_trusts_by_default_and_names_the_missing_host() { + let dir = std::env::temp_dir().join(format!("igneum-verifier-test-{}", std::process::id())); + let _ = std::fs::create_dir_all(&dir); + let v = resolve(&dir, false); + assert_eq!(v.mode, "off"); + assert!(v.env.is_empty()); + assert!(v.detail.contains("is not next to the engine"), "{}", v.detail); + assert_eq!(v.set_text(), ""); + let v = resolve(&dir, true); + assert_eq!(v.mode, "trust"); + assert_eq!(v.env, vec![("IGNEUM_PROOF_VERIFY".to_string(), "trust".to_string())]); + assert!(v.detail.starts_with("devnet only")); + assert_eq!(v.set_text(), "trust"); + let _ = std::fs::remove_dir_all(&dir); + } + + #[cfg(not(windows))] + #[test] + fn a_host_next_to_the_engine_wins_over_trust() { + let dir = std::env::temp_dir().join(format!("igneum-verifier-host-{}", std::process::id())); + let _ = std::fs::create_dir_all(&dir); + std::fs::write(dir.join("igneum-prove-host"), "#!/bin/sh\nexit 0\n").unwrap(); + let v = resolve(&dir, true); + assert_eq!(v.mode, "command"); + assert_eq!(v.env.len(), 1); + assert_eq!(v.env[0].0, "IGNEUM_PROOF_VERIFIER"); + assert!(v.env[0].1.ends_with("igneum-prove-host")); + assert!(v.set_text().starts_with("command:")); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn report_modes_and_notes() { + assert_eq!(mode_of_report("Off"), "off"); + assert_eq!(mode_of_report("Trust"), "trust"); + assert_eq!(mode_of_report("Command(\"/x/igneum-prove-host\")"), "command"); + assert_eq!(mode_of_report(""), "unknown"); + assert!(note("off", "", "igneum-prove-host is not next to the engine (/x)", false).ends_with("blocks: igneum-prove-host is not next to the engine (/x).")); + assert!(note("off", "", "", true).contains("the app did not start this node")); + assert!(note("off", "command:/x", "", false).contains("older node build")); + assert!(note("command", "command:/x", "", false).starts_with("This node verifies")); + assert!(note("trust", "trust", "", false).starts_with("Devnet only")); + assert!(note("unknown", "", "", false).starts_with("Verifier state not read yet")); + } +} diff --git a/app/igneum-app/src/wslhost.rs b/app/igneum-app/src/wslhost.rs new file mode 100644 index 000000000..5bc195068 --- /dev/null +++ b/app/igneum-app/src/wslhost.rs @@ -0,0 +1,85 @@ +//! Where the Linux `igneum-prove-host` lives as seen from inside WSL2 (Ubuntu-24.04) on a PC. One list, used by +//! three callers: the prover's probe (src/prover.rs), the verifier lookup the node spawn uses (src/verifier.rs) +//! and the Windows wrapper `igneum-prove-verify.exe` (src/bin/prove-verify.rs, which includes this file by path +//! because the package has no library target). +//! +//! Lookup order, first executable wins: +//! 1. the payload's `wsl2\bin\igneum-prove-host` next to the engine (`/mnt//.../wsl2/bin/...` from Ubuntu) +//! 2. `~/igneum-prove/proving/igneum-prove/target/release/igneum-prove-host`, what setup-wsl.sh builds +//! (it copies the package to `$HOME/igneum-prove` and builds in `proving/igneum-prove`) +//! 3. `~/igneum-prove/target/release/igneum-prove-host`, the layout before 5 October 2026 +//! 4. `/opt/igneum/igneum-prove-host`, a hand install (the devnet jobs put the CUDA host there) + +use std::path::Path; + +/// The WSL distribution the host runs in. +pub const DISTRO: &str = "Ubuntu-24.04"; + +/// A Windows path as WSL sees it: `C:\Users\x\f` -> `/mnt/c/Users/x/f`. A path without a drive letter is +/// returned with forward slashes only. +pub fn wsl_path(p: &Path) -> String { + let s = p.display().to_string().replace('\\', "/"); + let s = s.strip_prefix("//?/").map(|x| x.to_string()).unwrap_or(s); + if s.len() > 2 && s.as_bytes()[1] == b':' { + format!("/mnt/{}{}", s[..1].to_ascii_lowercase(), &s[2..]) + } else { + s + } +} + +/// The candidates in lookup order. `bin_dir` is the engine's folder on Windows (the payload root); `~` is left +/// for the shell inside WSL to expand, so the list reads the same in a message. +pub fn candidates(bin_dir: &Path) -> Vec { + vec![ + wsl_path(&bin_dir.join("wsl2").join("bin").join("igneum-prove-host")), + "~/igneum-prove/proving/igneum-prove/target/release/igneum-prove-host".to_string(), + "~/igneum-prove/target/release/igneum-prove-host".to_string(), + "/opt/igneum/igneum-prove-host".to_string(), + ] +} + +/// The candidates as one line for a message. +pub fn candidates_text(bin_dir: &Path) -> String { + candidates(bin_dir).join(", ") +} + +/// A `bash -lc` script that prints the first executable candidate (its path, one line) and nothing when there is +/// none. `~` expands in the shell; the shipped path is quoted. +pub fn lookup_script(bin_dir: &Path) -> String { + let list: Vec = candidates(bin_dir).into_iter().map(|c| if c.starts_with('~') { c } else { format!("'{}'", c.replace('\'', "'\\''")) }).collect(); + format!("for f in {}; do [ -x \"$f\" ] && {{ echo \"$f\"; break; }}; done", list.join(" ")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn candidate_order_is_shipped_then_setup_build_then_old_layout_then_opt() { + let c = candidates(Path::new("C:\\Program Files\\Igneum Miner")); + assert_eq!( + c, + vec![ + "/mnt/c/Program Files/Igneum Miner/wsl2/bin/igneum-prove-host", + "~/igneum-prove/proving/igneum-prove/target/release/igneum-prove-host", + "~/igneum-prove/target/release/igneum-prove-host", + "/opt/igneum/igneum-prove-host", + ] + ); + } + + #[test] + fn lookup_script_quotes_the_shipped_path_and_leaves_tilde_to_the_shell() { + let s = lookup_script(Path::new("C:\\Program Files\\Igneum Miner")); + assert!(s.starts_with("for f in '/mnt/c/Program Files/Igneum Miner/wsl2/bin/igneum-prove-host' ~/igneum-prove/proving/"), "{s}"); + assert!(s.contains("'/opt/igneum/igneum-prove-host'")); + assert!(s.ends_with("[ -x \"$f\" ] && { echo \"$f\"; break; }; done")); + } + + #[test] + fn wsl_paths() { + assert_eq!(wsl_path(Path::new("C:\\Users\\[user]\\AppData\\Local\\igneum\\app\\proving\\seq.json")), "/mnt/c/Users/[user]/AppData/Local/igneum/app/proving/seq.json"); + assert_eq!(wsl_path(Path::new("\\\\?\\D:\\x")), "/mnt/d/x"); + assert_eq!(wsl_path(Path::new("/tmp/x")), "/tmp/x"); + } +} diff --git a/app/igneum-app/ui/app.css b/app/igneum-app/ui/app.css index 73ed4fd19..69ea67842 100644 --- a/app/igneum-app/ui/app.css +++ b/app/igneum-app/ui/app.css @@ -287,6 +287,7 @@ td .sub{display:block;font-family:var(--mono);font-size:var(--t-xs);color:var(-- .kv>div:last-child{border-bottom:0} .kv .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.1em;text-transform:uppercase;color:var(--ash);white-space:nowrap} .kv .v{font-size:var(--t-md);font-variant-numeric:tabular-nums;color:var(--bone);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:right} +.kv .v.warn{color:var(--ember)} .card .note{margin-top:10px} .feed{display:flex;flex-direction:column;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);max-height:300px;overflow:auto} .feed>div{display:flex;justify-content:space-between;gap:10px;border-bottom:1px solid var(--line);padding:8px 0;align-items:baseline} diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index cd372c3b5..355404c67 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -286,6 +286,7 @@ if (typeof document !== 'undefined') (function () { $('s-auto-update').addEventListener('change', function () { api('api/update/auto', { on: this.checked }); }); $('s-live').addEventListener('click', function () { if (state && state.live_page) api('api/open', { url: state.live_page }); }); $('s-prove').addEventListener('change', function () { api('api/prove', { on: this.checked }).then(function (r) { if (r.ok) toast(r.ok && $('s-prove').checked ? 'Proving on; the first shard arrives within a minute' : 'Proving off'); }); }); + $('s-trust').addEventListener('change', function () { var on = this.checked; api('api/settings', { proof_verify_trust: on }).then(function (r) { if (r.ok) toast(on ? 'Trust mode on (devnet only); the node restarts' : 'Trust mode off; the node restarts'); else { toast(r.error || 'could not change'); $('s-trust').checked = !on; } }); }); $('pv-setup').addEventListener('click', function () { api('api/prove/setup', {}).then(function (r) { toast(r.ok ? 'Setup started in its own window' : (r.error || 'could not start')); }); }); $('s-jobs-allow').addEventListener('change', function () { api('api/jobs/allow', { on: this.checked }); setTimeout(fillSettings, 800); }); $('s-sweep').addEventListener('change', function () { api('api/sweep/enable', { on: this.checked }).then(function (r) { if (r.ok) toast($('s-sweep').checked ? 'Sweep on: once after install, then weekly' : 'Sweep off'); }); }); @@ -751,7 +752,14 @@ if (typeof document !== 'undefined') (function () { $('pv-submitted').textContent = String(pv.submitted || 0); $('pv-paid').textContent = String(pv.paid || 0) + (pv.paid_wei ? ' (' + (Number(pv.paid_wei) / 1e18).toFixed(4) + ' IGN)' : ''); $('pv-note').textContent = pv.enabled ? (pv.message || '') : 'Off. Settings switches it on: this machine proves the shards the chain assigns to its keys.'; - $('pv-setup-row').hidden = !(pv.enabled && !pv.available && pv.setup_hint); + // the node's proof verifier (spec 7.7 item 4): a node without one relays records and never includes them + var vm = pv.verifier_mode || 'unknown'; + var vWord = { command: 'verifying', trust: 'trust (devnet only)', off: 'off: relay only', unknown: 'not read yet' }[vm] || vm; + $('pv-verifier').textContent = vWord + (vm === 'command' && pv.pool_entries ? ' ยท pool ' + pv.pool_verified + '/' + pv.pool_entries + (pv.pool_failed ? ', ' + pv.pool_failed + ' rejected' : '') : ''); + $('pv-verifier').classList.toggle('warn', vm === 'off' || vm === 'trust'); + $('pv-verifier-note').textContent = pv.verifier_note || ''; + $('pv-verifier-note').hidden = !pv.verifier_note; + $('pv-setup-row').hidden = !((pv.enabled && !pv.available && pv.setup_hint) || (vm === 'off' && /WSL2 prover is not installed/.test(pv.verifier_reason || ''))); $('f-votes').textContent = withCommas(f.votes); // events var key = s.events.length ? s.events[0].t + ':' + s.events.length : ''; @@ -880,6 +888,7 @@ if (typeof document !== 'undefined') (function () { function fillJobsSettings(j) { $('s-jobs-allow').checked = !!j.allowed; $('s-prove').checked = !!(state.settings && state.settings.prove); + $('s-trust').checked = !!(state.settings && state.settings.proof_verify_trust); $('s-sweep').checked = !!(state.settings && state.settings.sweep); $('s-jobs-key').textContent = j.key_fingerprint ? 'signing key sha256:' + j.key_fingerprint : ''; var parts = []; diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index 8c166611c..e99c75909 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -203,8 +203,10 @@
assigned0
submitted0
paid0
+
verifiernot read yet

Off. Settings switches it on: this machine proves the shards the chain assigns to its keys.

+

@@ -273,6 +275,8 @@
+ +

Only when no verifier is found next to the engine: the node then includes proof records it never checked. Never on a testnet. A found verifier always wins. Changing this restarts the node.

diff --git a/docs/plans/release-0.3.6.md b/docs/plans/release-0.3.6.md index c236c1a4e..ab713eeb2 100644 --- a/docs/plans/release-0.3.6.md +++ b/docs/plans/release-0.3.6.md @@ -220,6 +220,16 @@ Written 5 October 2026, 08:45 BST, while proving v0 went live on the devnet at D | Rotation phase 2 | Branch `rotation-2` (5317305): `--dl-both`, `tools/logs.mjs --rotation`, fresh-repo script. Plan: `docs/plans/rotation-phase-2.md`. | | | Testnet parameters behind `fees_v1_activation_daa` | Branch `testnet-prep` and the fork's `testnet-params` (agent in progress). | | +### Done (5 October 2026, branch `proving-app`, app side only; the node is unchanged) + +| Item | Done | Commit | +|---|---|---| +| The app sets `IGNEUM_PROOF_VERIFIER` for its node | `app/igneum-app/src/verifier.rs` decides once per node start and `engine.rs` passes it to the igneumd spawn. macOS and Linux: `igneum-prove-host` next to the engine's binaries (the DMG's Contents/Resources/bin). Windows: the new `igneum-prove-verify.exe` (`src/bin/prove-verify.rs`, a bin target of the app crate, shipped by `make-payload.sh` next to the engine) is set only when its `--probe` finds a host inside WSL2; it rewrites `--proof` with `wslpath -a`, runs the host in the order of `src/wslhost.rs` and returns its exit code, 2 when there is no host. Trust mode is never the default: the setting `proof_verify_trust` (Settings, "devnet only") sets `IGNEUM_PROOF_VERIFY=trust` only when no verifier was found, and changing it restarts the node. After the WSL2 setup runs on a PC, the prover thread asks for one node restart so the verifier is picked up. | 063a9e7 | +| The prover's WSL2 probe checks both layouts | Order: the payload's `wsl2/bin`, `~/igneum-prove/proving/igneum-prove/target/release/igneum-prove-host` (what setup-wsl.sh builds), `~/igneum-prove/target/release/igneum-prove-host`, `/opt/igneum/igneum-prove-host`; one list in `src/wslhost.rs`, shared with the wrapper. The tile's message names every path it looked at, and says when WSL2 did not answer. | 063a9e7 | +| The proving tile shows the verifier state | The prover thread reads `igneum_getProvingStatus().verifier` every 30 s whether proving is on or off; `/api/state` carries `proving.verifier` (the node's words), `verifier_mode` (off, trust, command, unknown), `verifier_set` (what the app passed), `verifier_reason`, `verifier_note` and the pool counts. The tile has a `verifier` row and a note: "This node relays proofs but does not verify them, so it never includes a proof record in its blocks: ", "Devnet only: this node trusts proof records without verifying them", or "This node verifies proof records with igneum-prove-host". `site/api/live.mjs` already carried `verifier`; untouched. | 063a9e7 | + +The three items are one commit because they share `src/prover.rs` and `src/state.rs`. Not done here: the Windows payload's `wsl2/bin` host binaries (item 2 of the table above, needs the Linux cross-build), rotation phase 2, testnet parameters. The version in `app/igneum-app/Cargo.toml` is still 0.3.5; the ship script bumps it. + ### Operational lessons from the activation (5 October 2026) - Consensus override changes must land on every node at once: a hand node restarted early with a different `proving_v0_activation_daa` was refused by every peer (digest handshake) and sat isolated at a lower height for 20 minutes. Order that works: publish the manifest override, `update-now` to every app, wait for every app node to log the new parameters, then restart the hand nodes and the seed with the same file. diff --git a/packaging/windows/make-payload.sh b/packaging/windows/make-payload.sh index 53739b1f2..3b1133342 100755 --- a/packaging/windows/make-payload.sh +++ b/packaging/windows/make-payload.sh @@ -6,6 +6,7 @@ # # What goes in: # igneum-app.exe the engine, cross-compiled here (app/igneum-app, x86_64-pc-windows-gnu) +# igneum-prove-verify.exe the node's proof verifier wrapper (runs igneum-prove-host inside WSL2), same build # igneumd.exe, igneum-miner.exe the devnet-v4 cross-build (vendor/igneum-node/target-integration/x86_64-pc-windows-gnu/release) # lib*.dll the three mingw runtime DLLs, as igneum-windows-v4.zip ships them # igneum-worker-cuda.exe, igneum-worker-opencl.exe, nvrtc*.dll the prebuilt GPU workers from the proto-cuda/proto-opencl @@ -39,6 +40,10 @@ TOKEN_FILE="$HOME/.config/igneum/dl-token" rm -rf "$STAGE" mkdir -p "$STAGE/proto-cuda/packs" "$STAGE/proto-opencl" "$STAGE/app/windows" cp "$ENGINE" "$STAGE/igneum-app.exe" +# the node's proof verifier on a PC (release 0.3.6, app/igneum-app/src/bin/prove-verify.rs): the engine sets +# IGNEUM_PROOF_VERIFIER to this wrapper, which runs the WSL2 host; built beside the engine by the same cargo build +if [ -f "$(dirname "$ENGINE")/igneum-prove-verify.exe" ]; then cp "$(dirname "$ENGINE")/igneum-prove-verify.exe" "$STAGE/"; echo "verifier wrapper: igneum-prove-verify.exe" +else echo "warning: no igneum-prove-verify.exe next to $ENGINE; the node on this build relays proof records and never includes them"; fi cp "$REL/igneumd.exe" "$STAGE/igneumd.exe" cp "$REL/igneum-miner.exe" "$STAGE/igneum-miner.exe" for dll in lib/libstdc++-6.dll lib/libgcc_s_seh-1.dll bin/libwinpthread-1.dll; do