Merge proving-app (05051c1) into release-0.3.6: verifier env for the node, igneum-prove-verify.exe wrapper, WSL probe through wslhost (hidden, as master 9835f49), tile shows the verifier

This commit is contained in:
igneum-labs 2026-10-05 08:33:13 +00:00
commit c797fe49b3
15 changed files with 601 additions and 35 deletions

View file

@ -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"

View file

@ -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=<this exe>` for its node, and the node runs
//! `igneum-prove-verify.exe --mode verify --proof <file> --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 <wsl path>` and exits 0 when a host is found; exits 2 otherwise
//! igneum-prove-verify <host args> 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 <windows path>` 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 <path>` rewritten for WSL. Pure, so it has a test.
pub fn rewrite_args<F: Fn(&Path) -> String>(args: &[String], to_wsl: F) -> Vec<String> {
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<String> = 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<String> = 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<String> = ["--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<String> = 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/"));
}
}

View file

@ -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 }
}
}

View file

@ -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<u32>, vote: Option<bool>, login: Option<bool>, address: Option<&str>, display_name: Option<&str>, dev_fee: Option<bool>) -> Result<Value, String> {
pub fn apply_settings(&self, identities: Option<u32>, vote: Option<bool>, login: Option<bool>, address: Option<&str>, display_name: Option<&str>, dev_fee: Option<bool>, proof_verify_trust: Option<bool>) -> Result<Value, String> {
let mut restart = Vec::new();
let mut restart_node: Option<String> = 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<PathBuf>,
node_last_reading: Option<Instant>,
/// the proof verifier decided for the node (src/verifier.rs); None = decide at the next node start
verifier: Option<crate::verifier::Verifier>,
sync_prev: Option<u64>,
sync_stable_since: Option<Instant>,
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");

View file

@ -28,6 +28,8 @@ mod jobs;
mod jobrun;
mod jobbuild;
mod prover;
mod verifier;
mod wslhost;
mod sweep;
mod watchdog;

View file

@ -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<Work>
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, `<label_base>-<card n>`, and `-1..N` per identity when a card runs more than one.
@ -154,12 +152,11 @@ fn find_tools(bin_dir: &Path) -> Result<Tools, String> {
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/<drive>/.../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/<drive>/.../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<Tools, String> {
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<Tools, String> {
}
}
/// 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::<Vec<_>>().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<Shared>, bin_dir: PathBuf) {
let mut tools: Option<Tools> = None;
let mut last_probe = Instant::now() - Duration::from_secs(600);
let mut submitted: Vec<(u64, String, u32, u128)> = Vec::new();
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<Shared>, 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<Value, String> {
}
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"));
}
}

View file

@ -258,7 +258,8 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
let address = s("address");
let display_name = s("display_name");
let dev_fee = body.get("dev_fee").and_then(|v| v.as_bool());
shared.apply_settings(identities, vote, login, address.as_deref(), display_name.as_deref(), dev_fee)
let proof_verify_trust = body.get("proof_verify_trust").and_then(|v| v.as_bool());
shared.apply_settings(identities, vote, login, address.as_deref(), display_name.as_deref(), dev_fee, proof_verify_trust)
}
"/api/prove" => shared.set_prove(body.get("on").and_then(|v| v.as_bool()).unwrap_or(false)),
"/api/prove/setup" => crate::prover::setup(shared),

View file

@ -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:<path>`, `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.

View file

@ -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=<exe>` runs
//! `<exe> --mode verify --proof <file> --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:<path>`, `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<PathBuf, String> {
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"));
}
}

View file

@ -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/<drive>/.../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<String> {
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<String> = 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");
}
}

View file

@ -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}

View file

@ -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 = [];

View file

@ -203,8 +203,10 @@
<div><span class="k">assigned</span><span class="v mono" id="pv-assigned">0</span></div>
<div><span class="k">submitted</span><span class="v mono" id="pv-submitted">0</span></div>
<div><span class="k">paid</span><span class="v mono" id="pv-paid">0</span></div>
<div><span class="k">verifier</span><span class="v mono" id="pv-verifier">not read yet</span></div>
</div>
<p class="note" id="pv-note">Off. Settings switches it on: this machine proves the shards the chain assigns to its keys.</p>
<p class="note" id="pv-verifier-note"></p>
<div class="row" id="pv-setup-row" hidden><button class="btn small" id="pv-setup">Set up</button></div>
</div>
<div class="card">
@ -273,6 +275,8 @@
<div class="field">
<label class="switch"><input type="checkbox" id="s-vote"><span class="track"></span><span>Vote on finality checkpoints</span></label>
<label class="switch"><input type="checkbox" id="s-prove"><span class="track"></span><span>Prove assigned shards (proving v0; on a Mac the CPU prover is slow)</span></label>
<label class="switch"><input type="checkbox" id="s-trust"><span class="track"></span><span>Trust proof records without verifying them (devnet only)</span></label>
<p class="note" id="s-trust-note">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.</p>
<label class="switch"><input type="checkbox" id="s-login"><span class="track"></span><span>Start at login</span></label>
</div>
<div class="field">

View file

@ -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: <why>", "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.

View file

@ -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