Merge prover-match: the empty-shard fixture and end-to-end export tests, source stamps in the prover tools
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
6cc65f278a
10 changed files with 1371 additions and 1 deletions
|
|
@ -1373,5 +1373,6 @@ Proving v0 activated at DAA 84,100 (manifest `consensus.override`, every node re
|
|||
| Paid shards by 10:53 UTC (Mac node `igneum_getProvingStatus`) | 3 shards, 3.370437410 IGN in total, pool balance 68,601.72 IGN |
|
||||
| Pool at that moment | 4 entries: 3 pending, 1 failed verification, 0 verified-and-waiting |
|
||||
| Non-empty shards | not yet: the exporter's post-root assertion fires on blocks with content (58,584 to 58,984 on 5 October); investigation open |
|
||||
| The assertion, explained (12:30 UTC, branch prover-match) | Not block content. Every block it fired on is empty (PC 2's export logs: 58,752 to 58,843 hit the assertion; 58,584 to 58,740 hit the backslash path of 6d51e53), one reward plus the pool credit, no transactions, no payouts. PC 2's exporter was a stale build: the panic names shard.rs:175, the line before commit 1251f0a moved the assert to 179, and that core's planner gave an empty segment the pre-root as its post-root while the statement applied the rewards (left = the node's root after the rewards, right = the root before them, as the log shows for 58,752). The core at master reproduces 58,927 and 59,192 with the node's roots, and the same shape (59,507: one reward to the same miner) was proven and paid after the 10:49 and 10:52 UTC rebuilds on PC 2. Branch prover-match: fixture `block-58927-empty-reward.json`, `export/tests/fixtures.rs` (every fixture reproduces; an empty segment ends at the root after the rewards), and a source stamp on the first line of the exporter and the host so a stale binary names itself. The guest is untouched: built in one directory, master and the branch give byte-identical loadable segments for the shard program and the aggregator (shard program id 0x1ec8b941 at master in that directory). Noted on the way: the same sources built in three directories on this Mac gave two different guest ELFs (text segment c173b3de in the main checkout and in a fresh worktree, 830f7433 in the branch's worktree, shard program id 0x366e2aca there), so the program id is not yet a pure function of the sources on a native build; SP1's docker build is the reproducible path and is not in use. Open item. |
|
||||
|
||||
Commands: `curl -X POST http://127.0.0.1:26800 -d '{"jsonrpc":"2.0","id":1,"method":"igneum_getProvingStatus","params":[]}'` on the Mac; `node tools/logs.mjs` for PC 2's prover lines (`prover: block N shard 0 assigned to win-1ccfe586-1-1: export, cut, prove (CUDA), sign, submit`).
|
||||
|
|
|
|||
1170
proving/fixtures/block-58927-empty-reward.json
Normal file
1170
proving/fixtures/block-58927-empty-reward.json
Normal file
File diff suppressed because it is too large
Load diff
1
proving/igneum-prove/Cargo.lock
generated
1
proving/igneum-prove/Cargo.lock
generated
|
|
@ -2796,6 +2796,7 @@ dependencies = [
|
|||
"hex",
|
||||
"igneum-prove-core",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -12,3 +12,8 @@ serde_json.workspace = true
|
|||
hex.workspace = true
|
||||
anyhow.workspace = true
|
||||
bincode.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
# the source stamp (build.rs); sha2 0.10 is already in the lock through the host
|
||||
sha2 = "0.10"
|
||||
hex.workspace = true
|
||||
|
|
|
|||
30
proving/igneum-prove/export/build.rs
Normal file
30
proving/igneum-prove/export/build.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
//! Stamps the binary with a hash of the sources it was built from (`IGNEUM_PROVE_SOURCES`), printed on the
|
||||
//! exporter's first line. The stale-build class (5 October 2026: PC 2's exporter carried a core from before
|
||||
//! commit 1251f0a because cargo judged the copied sources older than its cache) is then visible in the first
|
||||
//! line of every export log instead of in a line number. Reproduce from a tree with
|
||||
//! `cat $(ls core/src/*.rs export/src/*.rs | sort) | shasum -a 256 | cut -c1-16` in proving/igneum-prove.
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let mut files: Vec<String> = Vec::new();
|
||||
for dir in ["core/src", "export/src"] {
|
||||
for entry in std::fs::read_dir(root.join(dir)).expect("source dir") {
|
||||
let name = entry.expect("entry").file_name().to_string_lossy().into_owned();
|
||||
if name.ends_with(".rs") {
|
||||
files.push(format!("{dir}/{name}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
let mut h = Sha256::new();
|
||||
for f in &files {
|
||||
let path = root.join(f);
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
h.update(std::fs::read(&path).expect("read source"));
|
||||
}
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rustc-env=IGNEUM_PROVE_SOURCES={}", &hex::encode(h.finalize())[..16]);
|
||||
}
|
||||
|
|
@ -99,6 +99,9 @@ fn main() -> Result<()> {
|
|||
let source = args.iter().position(|a| a == "--source").and_then(|i| args.get(i + 1)).cloned().unwrap_or_else(|| format!("{} (igneum_exportSegments), block {}", args[1], want));
|
||||
let budget: u64 = args.iter().position(|a| a == "--budget").and_then(|i| args.get(i + 1)).map(|b| b.parse()).transpose()?.unwrap_or(SHARD_PROVING_GAS_BUDGET);
|
||||
|
||||
// The sources this binary was built from (export/build.rs), so a stale build names itself in every export log.
|
||||
println!("igneum-prove-export sources {} (core/src and export/src)", env!("IGNEUM_PROVE_SOURCES"));
|
||||
|
||||
let segments = export["segments"].as_array().context("segments")?.clone();
|
||||
let registry_code = bytes(&export["registryCode"])?;
|
||||
let mut db = IgneumDb::new();
|
||||
|
|
|
|||
128
proving/igneum-prove/export/tests/fixtures.rs
Normal file
128
proving/igneum-prove/export/tests/fixtures.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
//! Every fixture in `proving/fixtures` reproduces natively: the whole-block statement gives the node's state
|
||||
//! root, the plan cuts the same shards, and every shard statement from its witness ends at the plan's post-root.
|
||||
//! A core whose rules drift from the node's, or whose planner drifts from the statement, fails here before any
|
||||
//! binary is packaged.
|
||||
//!
|
||||
//! `block-58927-empty-reward.json` is the regression for 5 October 2026: PC 2's exporter, a stale build whose
|
||||
//! core predated commit 1251f0a, failed every empty devnet block with "shard 0 post-root from the witness",
|
||||
//! left = the node's root after the rewards, right = the root before them. The planner of that core gave an
|
||||
//! empty segment the pre-root as its post-root (`root_at(end)` with no transactions); the statement, correctly,
|
||||
//! applied the rewards and the pool credit. The rule (spec 7.7 item 8, design 1.1): an empty segment is one
|
||||
//! shard whose statement applies the rewards and the payouts, so its post-root is the segment's root after them,
|
||||
//! never the pre-root.
|
||||
|
||||
use alloy_primitives::{Address, B256};
|
||||
use igneum_prove_core::config::PROVING_POOL_ADDRESS;
|
||||
use igneum_prove_core::executor::load_pre_state;
|
||||
use igneum_prove_core::shard::{build_shards, shard_statement};
|
||||
use igneum_prove_core::Fixture;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn fixtures_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../fixtures")
|
||||
}
|
||||
// This test lives in the export crate, not in igneum-prove-core, so the core's manifest (part of the guest build)
|
||||
// stays untouched: the export crate already has serde_json and is never part of the guest build. Checked on
|
||||
// 5 October 2026: built in one directory, this branch and master give byte-identical guest ELF segments.
|
||||
|
||||
fn load(name: &str) -> Fixture {
|
||||
let path = fixtures_dir().join(name);
|
||||
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
|
||||
let f: Fixture = serde_json::from_str(&text).unwrap_or_else(|e| panic!("parse {}: {e}", path.display()));
|
||||
assert_eq!(f.format, igneum_prove_core::fixture::FORMAT, "{name}: fixture format");
|
||||
f
|
||||
}
|
||||
|
||||
/// Runs the exporter's own check on one fixture: block statement against the expected values, the cut against
|
||||
/// the plan, every shard's witness statement against the plan's roots and links.
|
||||
fn check(name: &str) {
|
||||
let f = load(name);
|
||||
let budget = f.plan.shard_budget;
|
||||
// The exporter builds the plan with the zero address as the prover; the roots do not depend on it.
|
||||
let (outcome, pre_root, shards) = build_shards(&f.block, budget, Address::ZERO);
|
||||
let e = &f.expected;
|
||||
assert_eq!(pre_root, e.pre_state_root, "{name}: pre-state root");
|
||||
assert_eq!(outcome.state_root, e.post_state_root, "{name}: post-state root");
|
||||
assert_eq!(e.post_state_root, e.node_state_root, "{name}: the exporter's root is the node's");
|
||||
assert_eq!(outcome.receipts_root, e.receipts_root, "{name}: receipts root");
|
||||
assert_eq!(outcome.tx_commitment, e.tx_commitment, "{name}: tx commitment");
|
||||
assert_eq!((outcome.gas_used, outcome.pgas_used), (e.gas_used, e.pgas_used), "{name}: gas and pgas");
|
||||
assert_eq!((outcome.executed.len(), outcome.skipped.len()), (e.executed as usize, e.skipped as usize), "{name}: executed and skipped");
|
||||
assert_eq!(shards.len(), f.plan.shards.len(), "{name}: shard count");
|
||||
for (s, x) in shards.iter().zip(&f.plan.shards) {
|
||||
let o = &s.output;
|
||||
let tag = format!("{name} shard {}", x.index);
|
||||
assert_eq!(s.spec.index, x.index, "{tag}: index");
|
||||
assert_eq!((s.spec.tx_start, s.spec.tx_end), (x.tx_start, x.tx_end), "{tag}: range");
|
||||
assert_eq!(s.spec.over_budget, x.over_budget, "{tag}: over budget");
|
||||
assert_eq!(o.pre_root, x.pre_root, "{tag}: pre-root");
|
||||
assert_eq!(o.post_root, x.post_root, "{tag}: post-root");
|
||||
assert_eq!(o.receipts_root, x.receipts_root, "{tag}: receipts root");
|
||||
assert_eq!((o.link_in, o.link_out), (x.link_in, x.link_out), "{tag}: links");
|
||||
assert_eq!((o.gas_used, o.pgas_used), (x.gas_used, x.pgas_used), "{tag}: gas and pgas");
|
||||
assert_eq!((o.executed, o.skipped), (x.executed, x.skipped), "{tag}: executed and skipped");
|
||||
// The statement recomputed from the shard input alone (what the guest does) is the same statement.
|
||||
assert_eq!(shard_statement(&s.input), *o, "{tag}: statement from the input");
|
||||
}
|
||||
// The shards chain and end at the block's root.
|
||||
assert_eq!(shards.first().map(|s| s.output.pre_root), Some(e.pre_state_root), "{name}: first shard starts at the pre-root");
|
||||
assert_eq!(shards.last().map(|s| s.output.post_root), Some(e.post_state_root), "{name}: last shard ends at the post-root");
|
||||
for w in shards.windows(2) {
|
||||
assert_eq!(w[0].output.post_root, w[1].output.pre_root, "{name}: shards chain on roots");
|
||||
assert_eq!(w[0].output.link_out, w[1].output.link_in, "{name}: shards chain on links");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_fixture_reproduces() {
|
||||
let mut names: Vec<String> = std::fs::read_dir(fixtures_dir())
|
||||
.expect("proving/fixtures")
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.filter(|n| n.ends_with(".json"))
|
||||
.collect();
|
||||
names.sort();
|
||||
assert!(names.len() >= 7, "fixtures present: {names:?}");
|
||||
for n in &names {
|
||||
check(n);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_segment_shard_ends_at_the_root_after_the_rewards() {
|
||||
let name = "block-58927-empty-reward.json";
|
||||
let f = load(name);
|
||||
let b = &f.block;
|
||||
assert!(b.blocks.iter().all(|x| x.txs.is_empty()), "the block carries no transactions");
|
||||
assert_eq!(b.rewards.len(), 1, "one blue block, one reward");
|
||||
assert!(b.payouts.is_empty(), "no payouts in this segment");
|
||||
assert!(!b.proving_pool_credit.is_zero(), "the pool credit is part of the statement");
|
||||
|
||||
let (outcome, pre_root, shards) = build_shards(b, f.plan.shard_budget, Address::ZERO);
|
||||
assert_eq!(shards.len(), 1, "an empty segment is one shard");
|
||||
let s = &shards[0];
|
||||
assert_eq!((s.spec.tx_start, s.spec.tx_end), (0, 0));
|
||||
assert_eq!(s.output.pre_root, pre_root);
|
||||
assert_ne!(s.output.post_root, pre_root, "the rewards move the root: the post-root is never the pre-root");
|
||||
assert_eq!(s.output.post_root, f.expected.node_state_root, "the witness statement ends at the node's root");
|
||||
assert_eq!(s.spec.post_root, f.expected.node_state_root, "the plan ends the empty shard at the node's root, not the pre-root");
|
||||
assert_eq!(outcome.state_root, f.expected.node_state_root);
|
||||
|
||||
// The rule itself, by hand: rewards to each blue miner, the 20% credit to the pool escrow, then the payouts
|
||||
// (none here), before any transaction; the root after that is the shard's post-root.
|
||||
let mut db = load_pre_state(b);
|
||||
assert_eq!(db.state_root(), pre_root);
|
||||
for (miner, wei) in &b.rewards {
|
||||
db.add_balance(*miner, *wei);
|
||||
}
|
||||
db.add_balance(PROVING_POOL_ADDRESS, b.proving_pool_credit);
|
||||
for (to, wei) in &b.payouts {
|
||||
db.sub_balance(PROVING_POOL_ADDRESS, *wei);
|
||||
db.add_balance(*to, *wei);
|
||||
}
|
||||
assert_eq!(db.state_root(), s.output.post_root, "rewards, pool credit and payouts, nothing else");
|
||||
|
||||
// The pre-root PC 2 printed on the right of its assertion is what a planner without the fix would return.
|
||||
let wrong: B256 = pre_root;
|
||||
assert_ne!(s.spec.post_root, wrong);
|
||||
}
|
||||
|
|
@ -22,6 +22,9 @@ tokio = { version = "1", features = ["rt-multi-thread", "time"] }
|
|||
|
||||
[build-dependencies]
|
||||
sp1-build.workspace = true
|
||||
# the source stamp (build.rs)
|
||||
sha2 = "0.10"
|
||||
hex.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
|
|||
|
|
@ -1,4 +1,32 @@
|
|||
//! Builds the two SP1 guests and stamps the host with a hash of the native sources it was built from
|
||||
//! (`IGNEUM_PROVE_SOURCES`, printed in the host's first line; the guests have their own identity, the shard
|
||||
//! program's verifying key). Same purpose and recipe as export/build.rs (the stale-build class of 5 October
|
||||
//! 2026): `cat $(ls core/src/*.rs host/src/*.rs | sort) | shasum -a 256 | cut -c1-16` in proving/igneum-prove.
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
sp1_build::build_program("../program");
|
||||
sp1_build::build_program("../aggregator");
|
||||
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
|
||||
let mut files: Vec<String> = Vec::new();
|
||||
for dir in ["core/src", "host/src"] {
|
||||
for entry in std::fs::read_dir(root.join(dir)).expect("source dir") {
|
||||
let name = entry.expect("entry").file_name().to_string_lossy().into_owned();
|
||||
if name.ends_with(".rs") {
|
||||
files.push(format!("{dir}/{name}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
let mut h = Sha256::new();
|
||||
for f in &files {
|
||||
let path = root.join(f);
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
h.update(std::fs::read(&path).expect("read source"));
|
||||
}
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rustc-env=IGNEUM_PROVE_SOURCES={}", &hex::encode(h.finalize())[..16]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@ fn run() -> Result<()> {
|
|||
let txs: usize = block.blocks.iter().map(|b| b.txs.len()).sum();
|
||||
let prover_kind = std::env::var("SP1_PROVER").unwrap_or_else(|_| "cpu".into());
|
||||
println!(
|
||||
"fixture {path}: chain {} block {} ({}), {txs} transactions in {} including blocks, {} accounts in the pre-state, plan {} shard(s) at S_p = {} pgas{}; SP1_PROVER={prover_kind}; prover payout {prover}; {}",
|
||||
"igneum-prove-host sources {}: fixture {path}: chain {} block {} ({}), {txs} transactions in {} including blocks, {} accounts in the pre-state, plan {} shard(s) at S_p = {} pgas{}; SP1_PROVER={prover_kind}; prover payout {prover}; {}",
|
||||
env!("IGNEUM_PROVE_SOURCES"),
|
||||
block.chain_id,
|
||||
block.env.number,
|
||||
block.env.hash,
|
||||
|
|
|
|||
Loading…
Reference in a new issue