prover: a stale igneum-prove-export is named when the node's export carries the account dump and the exporter never reads it

PC 1 on 0.3.14 (6 October 2026, 18:16Z, block 140,662): "segment 140661: port state root 0xe45c... differs from the
node's 0xddd7...". The export was [140661, 140662] with the dump; the 0.3.14 exporter seeds from segment 140,661 and
never replays it, so the only exporter that replays 140,661 (from the restart state, hence the mismatch) is the one
from before 0.3.14: the installed binary was not replaced, the same class as the stale verifier host. The prover's
failure line now names the stale exporter by path when the export carries preState and the output has no "account
dump" line (exporter_failure, unit-tested with the PC 1 text); the real line stays when the exporter did read the dump.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-06 18:24:40 +00:00
parent 98bdeeb88b
commit 0477f8857d

View file

@ -135,6 +135,22 @@ struct Tools {
/// The node's re-derivation boundary (0.3.14, 6 October 2026): the chain block its EVM restarted at after the /// The node's re-derivation boundary (0.3.14, 6 October 2026): the chain block its EVM restarted at after the
/// bodies below the pruning point were gone (`igneum_getExecStatus.restartNumber`; on a 0.3.13 node the /// bodies below the pruning point were gone (`igneum_getExecStatus.restartNumber`; on a 0.3.13 node the
/// `startedFrom` text "restart at chain block N"), else 0. The prover never claims work below it (no bodies, no /// `startedFrom` text "restart at chain block N"), else 0. The prover never claims work below it (no bodies, no
/// The exporter's failure line. When the node's export carries the account dump (0.3.14) and the exporter's output
/// never mentions it, the exporter at `path` predates 0.3.14: it replays the dump's own segment from the restart
/// state and fails there with "port state root differs" (PC 1, 6 October 2026 18:16Z, block 140,662: the installer
/// had not replaced igneum-prove-export); the line names the stale binary instead of the misleading root error.
fn exporter_failure(export_has_dump: bool, out: &str, path: &Path, block: Option<u64>) -> String {
let last = out.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("failed");
let what = match block {
Some(b) => format!("exporter, block {b}"),
None => "exporter".to_string(),
};
if export_has_dump && !out.contains("account dump") {
return format!("{what}: stale igneum-prove-export at {}: the node's export carries the account dump and this exporter does not read it (it predates 0.3.14); install this release's exporter there ({last})", path.display());
}
format!("{what}: {last}")
}
/// state: unprovable on every node). /// state: unprovable on every node).
fn exec_boundary(shared: &Shared) -> u64 { fn exec_boundary(shared: &Shared) -> u64 {
let st = match evm_rpc(shared, "igneum_getExecStatus", json!([]), Duration::from_secs(5)) { let st = match evm_rpc(shared, "igneum_getExecStatus", json!([]), Duration::from_secs(5)) {
@ -665,7 +681,7 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
let (seq_p, fix_p, res_p) = if t.wsl { (wsl_path(&seq), wsl_path(&fixture), wsl_path(&results)) } else { (seq.display().to_string(), fixture.display().to_string(), results.display().to_string()) }; let (seq_p, fix_p, res_p) = if t.wsl { (wsl_path(&seq), wsl_path(&fixture), wsl_path(&results)) } else { (seq.display().to_string(), fixture.display().to_string(), results.display().to_string()) };
let (ok, out) = run_tool(&shared, t, &t.export, &[seq_p, w.number.to_string(), fix_p.clone()], &[], Duration::from_secs(600), &dir.join(format!("export-{}.log", w.number))); let (ok, out) = run_tool(&shared, t, &t.export, &[seq_p, w.number.to_string(), fix_p.clone()], &[], Duration::from_secs(600), &dir.join(format!("export-{}.log", w.number)));
if !ok || !fixture.exists() { if !ok || !fixture.exists() {
return Err(format!("exporter: {}", out.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("failed"))); return Err(exporter_failure(export["preState"].is_array(), &out, &t.export, None));
} }
set(&shared, |p| p.message = if t.cuda { "proving on the GPU".into() } else { "CPU prover: about five minutes a shard, 30 GB of RAM, paid only when no card proves first".into() }); set(&shared, |p| p.message = if t.cuda { "proving on the GPU".into() } else { "CPU prover: about five minutes a shard, 30 GB of RAM, paid only when no card proves first".into() });
let prover_env = if t.cuda { "cuda" } else { "cpu" }; let prover_env = if t.cuda { "cuda" } else { "cpu" };
@ -854,7 +870,7 @@ fn prove_segment(shared: &Shared, t: &Tools, seg: &crate::segments::SegmentWork,
let fixture = dir.join(format!("block-{b}.json")); let fixture = dir.join(format!("block-{b}.json"));
let (ok, out) = run_tool(shared, t, &t.export, &[as_host_path(&seq), b.to_string(), as_host_path(&fixture)], &[], Duration::from_secs(600), &dir.join(format!("export-{b}.log"))); let (ok, out) = run_tool(shared, t, &t.export, &[as_host_path(&seq), b.to_string(), as_host_path(&fixture)], &[], Duration::from_secs(600), &dir.join(format!("export-{b}.log")));
if !ok || !fixture.exists() { if !ok || !fixture.exists() {
return Err(format!("exporter, block {b}: {}", out.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("failed"))); return Err(exporter_failure(export["preState"].is_array(), &out, &t.export, Some(b)));
} }
fixtures.push(as_host_path(&fixture)); fixtures.push(as_host_path(&fixture));
} }
@ -1092,6 +1108,21 @@ pub fn setup(shared: &Shared) -> Result<Value, String> {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn a_stale_exporter_is_named_when_the_export_carries_the_dump() {
let p = Path::new("C:/x/igneum-prove-export.exe");
let old_out = "Error: segment 140661: port state root 0xe45c differs from the node's 0xddd7; the port is not the node's executor, stop\n";
let line = exporter_failure(true, old_out, p, Some(140662));
assert!(line.contains("stale igneum-prove-export at C:/x/igneum-prove-export.exe"), "{line}");
assert!(line.starts_with("exporter, block 140662:"));
// the 0.3.14 exporter read the dump and failed later: the real line, not the stale one
let new_out = "account dump: 79 accounts after chain block 140661, state root 0x1 (equals the node's)\nError: segment 140662: port state root 0x2 differs from the node's 0x3\n";
let line = exporter_failure(true, new_out, p, None);
assert_eq!(line, "exporter: Error: segment 140662: port state root 0x2 differs from the node's 0x3");
// an export without a dump (an older node): never the stale line
assert_eq!(exporter_failure(false, old_out, p, None), format!("exporter: {}", old_out.trim()));
}
#[test] #[test]
fn pinned_ids_come_out_of_the_hosts_id_line() { fn pinned_ids_come_out_of_the_hosts_id_line() {
let out = "RESULT id: pinned guests: shard program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a (2832504 bytes, sha256 0x150f4c05a2951fc5) aggregator id 0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896 (319744 bytes), pinned 2026-10-05T16:20:38Z on Darwin, SP1 5.0 circuit v5\n"; let out = "RESULT id: pinned guests: shard program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a (2832504 bytes, sha256 0x150f4c05a2951fc5) aggregator id 0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896 (319744 bytes), pinned 2026-10-05T16:20:38Z on Darwin, SP1 5.0 circuit v5\n";