Merge proving-app (c510f29) into release-0.3.6: verifier env for the node, igneum-prove-verify.exe wrapper, WSL probe through wslhost (hidden, as master cce1a12), tile shows the verifier
This commit is contained in:
commit
e09a13da31
15 changed files with 601 additions and 35 deletions
|
|
@ -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"
|
||||
|
|
|
|||
148
app/igneum-app/src/bin/prove-verify.rs
Normal file
148
app/igneum-app/src/bin/prove-verify.rs
Normal 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/"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ mod jobs;
|
|||
mod jobrun;
|
||||
mod jobbuild;
|
||||
mod prover;
|
||||
mod verifier;
|
||||
mod wslhost;
|
||||
mod sweep;
|
||||
mod watchdog;
|
||||
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
174
app/igneum-app/src/verifier.rs
Normal file
174
app/igneum-app/src/verifier.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
85
app/igneum-app/src/wslhost.rs
Normal file
85
app/igneum-app/src/wslhost.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue