Proving: aggregator guest, host modes shard and block with per-stage timestamps, exporter with shard plans
Two SP1 programs: the shard guest and the aggregator guest (deferred proof verification of the shard vk). Host modes native (cut, witnesses, chain and sums, three tamper checks, stub), execute (cycles per shard and for the aggregator), shard (execute, core, compressed, each verified), block (compressed proof per shard, aggregation, verified against the shard program id and the claim). Every stage prints a STAGE line and a RESULT line with a UTC timestamp so a silent gap is visible, and the host holds a Tokio runtime for the whole run and drops the proof system inside it, for the sp1-cuda Drop panic (ledger P20). The exporter writes v1 fixtures with the plan and every shard's expected roots, links and witness size; --budget makes a test cut. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
9bea7c718b
commit
f71af23dcb
10 changed files with 604 additions and 190 deletions
12
proving/igneum-prove/aggregator/Cargo.toml
Normal file
12
proving/igneum-prove/aggregator/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "igneum-prove-aggregator"
|
||||||
|
description = "SP1 guest: verifies the shard proofs of one Igneum chain block in order and commits the block statement (design 5.3)"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sp1-zkvm = { workspace = true, features = ["verify"] }
|
||||||
|
igneum-prove-core.workspace = true
|
||||||
|
bincode.workspace = true
|
||||||
|
sha2 = "0.10"
|
||||||
20
proving/igneum-prove/aggregator/src/main.rs
Normal file
20
proving/igneum-prove/aggregator/src/main.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
//! The aggregator guest. Input: bincode of `AggInput` (the shard program's verifying key, the shard proofs'
|
||||||
|
//! public values in order, the parent hash, optionally the previous block proof's key and public values); the
|
||||||
|
//! shard proofs themselves come in as SP1 deferred proofs. Output: `BlockOutput` in its fixed byte layout.
|
||||||
|
//! Every check is `igneum_prove_core::agg::aggregate`, the same code the host runs natively first.
|
||||||
|
|
||||||
|
#![no_main]
|
||||||
|
sp1_zkvm::entrypoint!(main);
|
||||||
|
|
||||||
|
use igneum_prove_core::agg::{aggregate, AggInput};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
let input = sp1_zkvm::io::read_vec();
|
||||||
|
let input: AggInput = bincode::deserialize(&input).expect("AggInput decodes");
|
||||||
|
let out = aggregate(&input, &mut |vk, pv| {
|
||||||
|
let digest: [u8; 32] = Sha256::digest(pv).into();
|
||||||
|
sp1_zkvm::lib::verify::verify_sp1_proof(vk, &digest);
|
||||||
|
});
|
||||||
|
sp1_zkvm::io::commit_slice(&out.to_bytes());
|
||||||
|
}
|
||||||
|
|
@ -11,3 +11,4 @@ alloy-primitives.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
bincode.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
//! Usage: igneum-prove-export <seq.json> <block number> <out.json> [--source "text"]
|
//! Usage: igneum-prove-export <seq.json> <block number> <out.json> [--source "text"] [--budget <pgas>]
|
||||||
//!
|
//!
|
||||||
//! `seq.json` is the output of `igneum_exportSegments` (tools/evm-smoke writes it). The exporter rebuilds the
|
//! `seq.json` is the output of `igneum_exportSegments` (tools/evm-smoke and tools/prove-fixtures write it). The
|
||||||
//! genesis state (the DeveloperRegistry at its fixed address, nothing else), replays every segment through
|
//! exporter rebuilds the genesis state (the DeveloperRegistry at its fixed address, nothing else), replays every
|
||||||
//! `igneum_prove_core::execute_block` and compares the state root after each with the node's `stateRoot`.
|
//! segment through `igneum_prove_core::execute_block` and compares the state root after each with the node's
|
||||||
//! Any mismatch is fatal: the port would not be the node's executor. At the requested block it snapshots the
|
//! `stateRoot`. Any mismatch is fatal: the port would not be the node's executor. At the requested block it
|
||||||
//! pre-state, runs the block, and writes the fixture with the expected roots.
|
//! snapshots the pre-state, runs the block, cuts the shard plan at `S_p` (or `--budget`, a test cut), builds
|
||||||
|
//! and checks every shard's witness, and writes the fixture with the expected values per shard and per block.
|
||||||
|
|
||||||
use alloy_primitives::{keccak256, Address, Bytes, B256, U256};
|
use alloy_primitives::{keccak256, Address, Bytes, B256, U256};
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use igneum_prove_core::config::DEVELOPER_REGISTRY_ADDRESS;
|
use igneum_prove_core::config::{DEVELOPER_REGISTRY_ADDRESS, SHARD_PROVING_GAS_BUDGET};
|
||||||
use igneum_prove_core::{execute_block, AccountFixture, BlockFixture, Expected, Fixture, FixtureEnv, IgneumDb, IncludingBlock};
|
use igneum_prove_core::shard::build_shards;
|
||||||
|
use igneum_prove_core::{execute_block, AccountFixture, BlockFixture, Expected, Fixture, FixtureEnv, IgneumDb, IncludingBlock, Plan, ShardExpected};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
fn addr(v: &Value) -> Result<Address> {
|
fn addr(v: &Value) -> Result<Address> {
|
||||||
|
|
@ -86,12 +88,13 @@ fn fixture_of(export: &Value, segments: &[Value], n: usize, hashes: &[(u64, B256
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
if args.len() < 4 {
|
if args.len() < 4 {
|
||||||
bail!("usage: igneum-prove-export <seq.json> <block number> <out.json> [--source text]");
|
bail!("usage: igneum-prove-export <seq.json> <block number> <out.json> [--source text] [--budget pgas]");
|
||||||
}
|
}
|
||||||
let export: Value = serde_json::from_str(&std::fs::read_to_string(&args[1])?)?;
|
let export: Value = serde_json::from_str(&std::fs::read_to_string(&args[1])?)?;
|
||||||
let want: u64 = args[2].parse()?;
|
let want: u64 = args[2].parse()?;
|
||||||
let out_path = &args[3];
|
let out_path = &args[3];
|
||||||
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 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);
|
||||||
|
|
||||||
let segments = export["segments"].as_array().context("segments")?.clone();
|
let segments = export["segments"].as_array().context("segments")?.clone();
|
||||||
let registry_code = bytes(&export["registryCode"])?;
|
let registry_code = bytes(&export["registryCode"])?;
|
||||||
|
|
@ -120,6 +123,7 @@ fn main() -> Result<()> {
|
||||||
pre_state_root: pre_root,
|
pre_state_root: pre_root,
|
||||||
post_state_root: out.state_root,
|
post_state_root: out.state_root,
|
||||||
receipts_root: out.receipts_root,
|
receipts_root: out.receipts_root,
|
||||||
|
tx_commitment: out.tx_commitment,
|
||||||
gas_used: out.gas_used,
|
gas_used: out.gas_used,
|
||||||
pgas_used: out.pgas_used,
|
pgas_used: out.pgas_used,
|
||||||
executed: out.executed.len() as u32,
|
executed: out.executed.len() as u32,
|
||||||
|
|
@ -140,10 +144,40 @@ fn main() -> Result<()> {
|
||||||
println!(" skipped {h}: {why:?}");
|
println!(" skipped {h}: {why:?}");
|
||||||
}
|
}
|
||||||
for r in &out.executed {
|
for r in &out.executed {
|
||||||
println!(" executed {}: status {} gas {} pgas {} logs {}", r.tx_hash, r.status, r.gas_used, r.pgas_used, r.logs.len());
|
println!(" executed {}: status {} gas {} pgas {} logs {}{}", r.tx_hash, r.status, r.gas_used, r.pgas_used, r.logs.len(), if r.pgas_aborted { " pgas-aborted" } else { "" });
|
||||||
}
|
}
|
||||||
println!(" pre-state root {pre_root}\n post-state root {} (node: {node_root})\n receipts root {}", out.state_root, out.receipts_root);
|
println!(" pre-state root {pre_root}\n post-state root {} (node: {node_root})\n receipts root {}\n tx commitment {}", out.state_root, out.receipts_root, out.tx_commitment);
|
||||||
fixture = Some(Fixture { format: igneum_prove_core::fixture::FORMAT.into(), source: source.clone(), block: f, expected });
|
|
||||||
|
// The shard plan and every shard's witness, checked natively (design 5.1).
|
||||||
|
let (_, _, shards) = build_shards(&f, budget, Address::ZERO);
|
||||||
|
println!(" plan: {} shard(s) at S_p = {budget} pgas{}", shards.len(), if budget == SHARD_PROVING_GAS_BUDGET { "" } else { " (TEST CUT below the consensus budget)" });
|
||||||
|
let mut plan_shards = Vec::with_capacity(shards.len());
|
||||||
|
for s in &shards {
|
||||||
|
let (accounts, slots, leaves, hashes) = s.input.witness.stats();
|
||||||
|
let bytes = bincode::serialize(&s.input)?.len() as u64;
|
||||||
|
println!(
|
||||||
|
" shard {}: txs {}..{}, gas {}, pgas {}{}, executed {}, skipped {}, witness {accounts} accounts {slots} slots {leaves} leaves {hashes} hashes, input {bytes} bytes\n pre {} post {}",
|
||||||
|
s.spec.index, s.spec.tx_start, s.spec.tx_end, s.spec.gas, s.spec.pgas, if s.spec.over_budget { " (ONE TRANSACTION ABOVE THE BUDGET)" } else { "" }, s.output.executed, s.output.skipped, s.spec.pre_root, s.spec.post_root
|
||||||
|
);
|
||||||
|
plan_shards.push(ShardExpected {
|
||||||
|
index: s.spec.index,
|
||||||
|
tx_start: s.spec.tx_start,
|
||||||
|
tx_end: s.spec.tx_end,
|
||||||
|
over_budget: s.spec.over_budget,
|
||||||
|
pre_root: s.spec.pre_root,
|
||||||
|
post_root: s.spec.post_root,
|
||||||
|
receipts_root: s.output.receipts_root,
|
||||||
|
link_in: s.spec.carry_in.link(),
|
||||||
|
link_out: s.spec.carry_out.link(),
|
||||||
|
gas_used: s.spec.gas,
|
||||||
|
pgas_used: s.spec.pgas,
|
||||||
|
executed: s.output.executed,
|
||||||
|
skipped: s.output.skipped,
|
||||||
|
witness: (accounts as u32, slots as u32, leaves as u32, hashes as u32, bytes),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let plan = Plan { shard_budget: budget, consensus: budget == SHARD_PROVING_GAS_BUDGET, shards: plan_shards };
|
||||||
|
fixture = Some(Fixture { format: igneum_prove_core::fixture::FORMAT.into(), source: source.clone(), block: f, plan, expected });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let final_root = b256(&export["stateRoot"])?;
|
let final_root = b256(&export["stateRoot"])?;
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
[package]
|
[package]
|
||||||
name = "igneum-prove-host"
|
name = "igneum-prove-host"
|
||||||
description = "SP1 host: loads a block fixture, runs the guest in execute, core and compressed modes, prints cycles, times and sizes, verifies the proofs; holds the versioned ProofSystem trait"
|
description = "SP1 host: loads a block fixture, cuts and witnesses its shards, proves one shard (execute, core, compressed) or the whole block (shard proofs plus aggregation), prints cycles, times and sizes with timestamps, verifies every proof; holds the versioned ProofSystem trait"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
igneum-prove-core.workspace = true
|
igneum-prove-core.workspace = true
|
||||||
|
igneum-evm-types.workspace = true
|
||||||
sp1-sdk = { workspace = true, features = ["blocking"] }
|
sp1-sdk = { workspace = true, features = ["blocking"] }
|
||||||
alloy-primitives.workspace = true
|
alloy-primitives.workspace = true
|
||||||
|
alloy-trie.workspace = true
|
||||||
|
alloy-rlp.workspace = true
|
||||||
bincode.workspace = true
|
bincode.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread", "time"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
sp1-build.workspace = true
|
sp1-build.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
sp1_build::build_program("../program");
|
sp1_build::build_program("../program");
|
||||||
|
sp1_build::build_program("../aggregator");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,139 +1,408 @@
|
||||||
//! igneum-prove-host: proves one Igneum chain block fixture with SP1.
|
//! igneum-prove-host: proves one Igneum chain block fixture with SP1, shard by shard (design 5.1 and 5.3).
|
||||||
//!
|
//!
|
||||||
//! Usage: igneum-prove-host <fixture.json> [--mode native|execute|core|compressed|all] [--out <results.json>]
|
//! Usage: igneum-prove-host <fixture.json> [--mode native|execute|shard|block|all] [--shard N]
|
||||||
|
//! [--prover 0x<payout address>] [--out <results.json>]
|
||||||
//!
|
//!
|
||||||
//! Modes build on each other: `native` runs the statement on the host and checks the fixture's expected roots;
|
//! Modes build on each other. `native` cuts the block into shards at `S_p`, builds every shard's witness, runs
|
||||||
//! `execute` runs the guest in SP1's executor for the cycle count (no proof); `core` makes and verifies a core
|
//! every shard statement natively, checks that the shards chain (roots, links) and sum (gas, pgas) to the whole
|
||||||
//! (STARK, many shards) proof; `compressed` makes and verifies the recursion-compressed proof; `all` runs them
|
//! block, aggregates them natively, and shows that a tampered witness fails. `execute` runs every shard guest
|
||||||
//! all. The prover comes from `SP1_PROVER` (cpu, the default; cuda, Linux x86_64 with the `cuda` feature; mock).
|
//! and the aggregator guest in SP1's executor for the cycle counts (no proof). `shard` proves one shard (the
|
||||||
//! Every run prints one `RESULT` line per mode (cycles, seconds, bytes) that PROVE-BLOCK and the bench log read.
|
//! `--shard` index, default 0) in all three SP1 stages: execute, core, compressed, each verified. `block` makes
|
||||||
|
//! the compressed proof of every shard and the aggregated block proof, verified. `all` is shard then block.
|
||||||
|
//! The prover comes from `SP1_PROVER` (cpu, the default; cuda, Linux x86_64 with the `cuda` feature; mock).
|
||||||
|
//! Every stage prints a `STAGE ... start` line and one `RESULT` line with a UTC timestamp, so a silent gap
|
||||||
|
//! between stages is visible (ledger P20). The host holds a Tokio runtime for the whole run and drops the
|
||||||
|
//! proof system inside it, so the CUDA client's destructor finds a runtime (ledger P20).
|
||||||
|
|
||||||
mod proof_system;
|
mod proof_system;
|
||||||
|
|
||||||
|
use alloy_primitives::{Address, B256};
|
||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use igneum_prove_core::executor::prove_statement;
|
use igneum_prove_core::agg::{self, AggInput, BlockOutput};
|
||||||
use igneum_prove_core::{Fixture, ProveOutput};
|
use igneum_prove_core::config::SHARD_PROVING_GAS_BUDGET;
|
||||||
use proof_system::{ProofSystem, SegmentClaim, ShardWitness, Sp1ProofSystem, StubProofSystem};
|
use igneum_prove_core::shard::{build_shards, shard_statement, BuiltShard, ShardInput, ShardOutput};
|
||||||
|
use igneum_prove_core::Fixture;
|
||||||
|
use proof_system::{ProofSystem, SegmentClaim, ShardWitness, Sp1ProofSystem, Sp1ShardProof, StubProofSystem};
|
||||||
use sp1_sdk::{include_elf, Elf};
|
use sp1_sdk::{include_elf, Elf};
|
||||||
use std::time::Instant;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
const ELF: Elf = include_elf!("igneum-prove-program");
|
const SHARD_ELF: Elf = include_elf!("igneum-prove-program");
|
||||||
|
const AGG_ELF: Elf = include_elf!("igneum-prove-aggregator");
|
||||||
|
|
||||||
|
fn now() -> String {
|
||||||
|
let secs = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
|
||||||
|
let (h, m, s) = ((secs / 3600) % 24, (secs / 60) % 60, secs % 60);
|
||||||
|
let days = secs / 86400;
|
||||||
|
// Civil date from days since the epoch (Howard Hinnant's algorithm).
|
||||||
|
let z = days as i64 + 719468;
|
||||||
|
let era = z.div_euclid(146097);
|
||||||
|
let doe = z - era * 146097;
|
||||||
|
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||||
|
let y = yoe + era * 400;
|
||||||
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||||
|
let mp = (5 * doy + 2) / 153;
|
||||||
|
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||||
|
let mo = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||||
|
let y = if mo <= 2 { y + 1 } else { y };
|
||||||
|
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{m:02}:{s:02}Z")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stage(name: &str) {
|
||||||
|
println!("STAGE {name} start {}", now());
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
// Ledger P20: the SP1 CUDA client spawns its shutdown on the current Tokio runtime from `Drop`; hold one for
|
||||||
|
// the whole run so the drop at the end finds it, and give the spawned tasks time to finish.
|
||||||
|
let runtime = tokio::runtime::Runtime::new()?;
|
||||||
|
let guard = runtime.enter();
|
||||||
|
let result = run();
|
||||||
|
drop(guard);
|
||||||
|
runtime.shutdown_timeout(Duration::from_secs(10));
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<()> {
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
let path = args.get(1).filter(|a| !a.starts_with("--")).context("usage: igneum-prove-host <fixture.json> [--mode native|execute|core|compressed|all] [--out results.json]")?;
|
let path = args.get(1).filter(|a| !a.starts_with("--")).context("usage: igneum-prove-host <fixture.json> [--mode native|execute|shard|block|all] [--shard N] [--prover 0x..] [--out results.json]")?;
|
||||||
let mode = args.iter().position(|a| a == "--mode").and_then(|i| args.get(i + 1)).map(String::as_str).unwrap_or("all");
|
let arg = |name: &str| args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).cloned();
|
||||||
let out_path = args.iter().position(|a| a == "--out").and_then(|i| args.get(i + 1)).cloned();
|
let mode = arg("--mode").unwrap_or_else(|| "all".into());
|
||||||
|
let shard_index: usize = arg("--shard").map(|s| s.parse()).transpose()?.unwrap_or(0);
|
||||||
|
let prover: Address = arg("--prover").map(|s| s.parse()).transpose()?.unwrap_or_else(|| Address::from_slice(&[0x19; 20]));
|
||||||
|
let out_path = arg("--out");
|
||||||
let fixture: Fixture = serde_json::from_str(&std::fs::read_to_string(path).with_context(|| format!("read {path}"))?)?;
|
let fixture: Fixture = serde_json::from_str(&std::fs::read_to_string(path).with_context(|| format!("read {path}"))?)?;
|
||||||
if fixture.format != igneum_prove_core::fixture::FORMAT {
|
if fixture.format != igneum_prove_core::fixture::FORMAT {
|
||||||
bail!("fixture format {} is not {}", fixture.format, igneum_prove_core::fixture::FORMAT);
|
bail!("fixture format {} is not {} (regenerate with igneum-prove-export)", fixture.format, igneum_prove_core::fixture::FORMAT);
|
||||||
}
|
}
|
||||||
let block = &fixture.block;
|
let block = &fixture.block;
|
||||||
let txs: usize = block.blocks.iter().map(|b| b.txs.len()).sum();
|
let txs: usize = block.blocks.iter().map(|b| b.txs.len()).sum();
|
||||||
let prover = std::env::var("SP1_PROVER").unwrap_or_else(|_| "cpu".into());
|
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; SP1_PROVER={prover}", block.chain_id, block.env.number, block.env.hash, block.blocks.len(), block.pre_state.len());
|
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}; {}",
|
||||||
|
block.chain_id,
|
||||||
|
block.env.number,
|
||||||
|
block.env.hash,
|
||||||
|
block.blocks.len(),
|
||||||
|
block.pre_state.len(),
|
||||||
|
fixture.plan.shards.len(),
|
||||||
|
fixture.plan.shard_budget,
|
||||||
|
if fixture.plan.consensus { "" } else { " (TEST CUT below the consensus budget)" },
|
||||||
|
now()
|
||||||
|
);
|
||||||
let mut results = serde_json::Map::new();
|
let mut results = serde_json::Map::new();
|
||||||
results.insert("fixture".into(), path.clone().into());
|
results.insert("fixture".into(), path.clone().into());
|
||||||
results.insert("block".into(), block.env.number.into());
|
results.insert("block".into(), block.env.number.into());
|
||||||
results.insert("prover".into(), prover.clone().into());
|
results.insert("prover".into(), prover_kind.clone().into());
|
||||||
|
results.insert("shard_budget".into(), fixture.plan.shard_budget.into());
|
||||||
|
results.insert("consensus_budget".into(), fixture.plan.consensus.into());
|
||||||
results.insert("sp1_crate_version".into(), "6.8.1".into());
|
results.insert("sp1_crate_version".into(), "6.8.1".into());
|
||||||
results.insert("sp1_circuit_version".into(), sp1_sdk::SP1_CIRCUIT_VERSION.into());
|
results.insert("sp1_circuit_version".into(), sp1_sdk::SP1_CIRCUIT_VERSION.into());
|
||||||
|
|
||||||
// 1. Native: the statement on the host, against the fixture's expected values.
|
// 1. Native: the cut, the witnesses, every shard statement, the chain and the sums, against the fixture.
|
||||||
|
stage("native");
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
let native = prove_statement(block);
|
let (outcome, pre_root, shards) = build_shards(block, fixture.plan.shard_budget, prover);
|
||||||
let native_s = t.elapsed().as_secs_f64();
|
let native_s = t.elapsed().as_secs_f64();
|
||||||
let e = &fixture.expected;
|
let e = &fixture.expected;
|
||||||
let same = native.pre_state_root == e.pre_state_root && native.post_state_root == e.post_state_root && native.receipts_root == e.receipts_root && native.gas_used == e.gas_used && native.pgas_used == e.pgas_used;
|
let same = pre_root == e.pre_state_root && outcome.state_root == e.post_state_root && outcome.receipts_root == e.receipts_root && outcome.tx_commitment == e.tx_commitment && outcome.gas_used == e.gas_used && outcome.pgas_used == e.pgas_used;
|
||||||
println!("RESULT native: {:.4} s, pre {} post {} receipts {} gas {} pgas {} executed {} skipped {}: {}", native_s, native.pre_state_root, native.post_state_root, native.receipts_root, native.gas_used, native.pgas_used, native.executed, native.skipped, if same { "MATCHES the fixture's expected values" } else { "DIFFERS from the fixture's expected values" });
|
println!(
|
||||||
|
"RESULT native: {:.4} s, pre {} post {} receipts {} gas {} pgas {} executed {} skipped {}: {} at {}",
|
||||||
|
native_s,
|
||||||
|
pre_root,
|
||||||
|
outcome.state_root,
|
||||||
|
outcome.receipts_root,
|
||||||
|
outcome.gas_used,
|
||||||
|
outcome.pgas_used,
|
||||||
|
outcome.executed.len(),
|
||||||
|
outcome.skipped.len(),
|
||||||
|
if same { "MATCHES the fixture's expected values" } else { "DIFFERS from the fixture's expected values" },
|
||||||
|
now()
|
||||||
|
);
|
||||||
if !same {
|
if !same {
|
||||||
bail!("native execution differs from the fixture; regenerate the fixture with igneum-prove-export");
|
bail!("native execution differs from the fixture; regenerate the fixture with igneum-prove-export");
|
||||||
}
|
}
|
||||||
if e.node_state_root != e.post_state_root {
|
if e.node_state_root != e.post_state_root {
|
||||||
bail!("the fixture's node state root differs from its expected post-state root; the exporter must not have produced this file");
|
bail!("the fixture's node state root differs from its expected post-state root; the exporter must not have produced this file");
|
||||||
}
|
}
|
||||||
|
if shards.len() != fixture.plan.shards.len() {
|
||||||
|
bail!("the plan has {} shards here and {} in the fixture", shards.len(), fixture.plan.shards.len());
|
||||||
|
}
|
||||||
|
let (mut sum_gas, mut sum_pgas) = (0u64, 0u64);
|
||||||
|
let mut prev_root = pre_root;
|
||||||
|
let mut prev_link = igneum_prove_core::Carry::default().link();
|
||||||
|
for (s, x) in shards.iter().zip(&fixture.plan.shards) {
|
||||||
|
let o = &s.output;
|
||||||
|
let (accounts, slots, leaves, hashes) = s.input.witness.stats();
|
||||||
|
let bytes = bincode::serialize(&s.input)?.len();
|
||||||
|
if o.pre_root != x.pre_root || o.post_root != x.post_root || o.receipts_root != x.receipts_root || o.link_in != x.link_in || o.link_out != x.link_out || o.gas_used != x.gas_used || o.pgas_used != x.pgas_used {
|
||||||
|
bail!("shard {}: the native statement differs from the fixture's plan", o.shard_index);
|
||||||
|
}
|
||||||
|
if o.pre_root != prev_root || o.link_in != prev_link {
|
||||||
|
bail!("shard {}: does not continue the previous shard", o.shard_index);
|
||||||
|
}
|
||||||
|
prev_root = o.post_root;
|
||||||
|
prev_link = o.link_out;
|
||||||
|
sum_gas += o.gas_used;
|
||||||
|
sum_pgas += o.pgas_used;
|
||||||
|
println!(
|
||||||
|
"RESULT shard {} native: txs {}..{} ({} executed, {} skipped), gas {}, pgas {}{}, witness {accounts} accounts {slots} slots {leaves} leaves {hashes} hashes, input {bytes} bytes, pre {} post {}",
|
||||||
|
o.shard_index,
|
||||||
|
s.spec.tx_start,
|
||||||
|
s.spec.tx_end,
|
||||||
|
o.executed,
|
||||||
|
o.skipped,
|
||||||
|
o.gas_used,
|
||||||
|
o.pgas_used,
|
||||||
|
if s.spec.over_budget { " (ONE TRANSACTION ABOVE S_p)" } else { "" },
|
||||||
|
o.pre_root,
|
||||||
|
o.post_root
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if prev_root != outcome.state_root || sum_gas != outcome.gas_used || sum_pgas != outcome.pgas_used {
|
||||||
|
bail!("the shards do not chain to the block's post-root or do not sum to its gas and pgas");
|
||||||
|
}
|
||||||
|
let native_block = agg::aggregate(&AggInput { shard_vk: [0; 8], shards: shards.iter().map(|s| s.output.to_bytes()).collect(), parent_hash: block.env.parent_hash, prev: None }, &mut |_, _| {});
|
||||||
|
if native_block.post_root != outcome.state_root || native_block.tx_commitment != outcome.tx_commitment || native_block.gas_used != outcome.gas_used {
|
||||||
|
bail!("the native aggregation does not reproduce the block");
|
||||||
|
}
|
||||||
|
println!("RESULT plan: {} shard(s) chain from {} to {} and sum to gas {} pgas {}: the cut equals the block; native aggregation receipts {} provers {}", shards.len(), pre_root, outcome.state_root, sum_gas, sum_pgas, native_block.receipts, native_block.provers);
|
||||||
results.insert("native_seconds".into(), native_s.into());
|
results.insert("native_seconds".into(), native_s.into());
|
||||||
let claim = SegmentClaim::from_output(&native);
|
results.insert("shards".into(), (shards.len() as u64).into());
|
||||||
|
results.insert("shard_pgas".into(), shards.iter().map(|s| serde_json::Value::from(s.output.pgas_used)).collect::<Vec<_>>().into());
|
||||||
|
results.insert("witness_bytes".into(), shards.iter().map(|s| serde_json::Value::from(bincode::serialize(&s.input).map(|b| b.len() as u64).unwrap_or(0))).collect::<Vec<_>>().into());
|
||||||
|
|
||||||
// 2. The devnet stub behind the same trait (design 5.7), so the pipeline shape is exercised too.
|
// 2. Tampered witnesses must fail: a changed balance moves the pre-root away from the node's; a changed
|
||||||
|
// storage value breaks the storage root check; a dropped account makes the execution unprovable.
|
||||||
|
tamper_checks(&shards[0])?;
|
||||||
|
|
||||||
|
// 3. The devnet stub behind the same trait (design 5.7), so the pipeline shape is exercised too.
|
||||||
let stub = StubProofSystem::new([7u8; 32]);
|
let stub = StubProofSystem::new([7u8; 32]);
|
||||||
let witness = ShardWitness { block: block.clone(), shard_index: 0, prover: alloy_primitives::B256::ZERO };
|
let stub_shards: Vec<_> = shards.iter().map(|s| stub.prove_shard(&ShardWitness { input: s.input.clone() })).collect::<Result<_>>()?;
|
||||||
let stub_proof = stub.prove_shard(&witness)?;
|
let stub_seg = stub.aggregate(None, &stub_shards)?;
|
||||||
let stub_seg = stub.aggregate(None, &[stub_proof])?;
|
let claim = SegmentClaim { segment: block.env.hash, pre_root, post_root: outcome.state_root, receipts: native_block.receipts };
|
||||||
println!("RESULT stub: ProofSystem v{} program {} verify_segment {}", StubProofSystem::VERSION, stub.program_id(), stub.verify_segment(&stub_seg, &claim));
|
println!("RESULT stub: ProofSystem v{} program {} verify_segment {}", StubProofSystem::VERSION, stub.program_id(), stub.verify_segment(&stub_seg, &claim));
|
||||||
if mode == "native" {
|
if mode == "native" {
|
||||||
return finish(results, out_path);
|
return finish(results, out_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. SP1, version 1 behind the trait.
|
// 4. SP1, version 1 behind the trait: two programs.
|
||||||
|
stage("setup");
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
let sp1 = Sp1ProofSystem::from_env(ELF)?;
|
let sp1 = Sp1ProofSystem::from_env(SHARD_ELF, AGG_ELF)?;
|
||||||
println!("RESULT setup: {:.2} s, ProofSystem v{} program id (vk hash) {}", t.elapsed().as_secs_f64(), Sp1ProofSystem::VERSION, sp1.program_id());
|
let setup_s = t.elapsed().as_secs_f64();
|
||||||
results.insert("setup_seconds".into(), t.elapsed().as_secs_f64().into());
|
println!("RESULT setup: {:.2} s (shard {:.2} s, aggregator {:.2} s), ProofSystem v{} shard program id {} aggregator id {} at {}", setup_s, sp1.last_timing("setup-shard").map(|d| d.as_secs_f64()).unwrap_or(0.0), sp1.last_timing("setup-aggregator").map(|d| d.as_secs_f64()).unwrap_or(0.0), Sp1ProofSystem::VERSION, sp1.program_id(), sp1.aggregator_id(), now());
|
||||||
results.insert("program_id".into(), sp1.program_id().to_string().into());
|
results.insert("setup_seconds".into(), setup_s.into());
|
||||||
|
results.insert("shard_program_id".into(), sp1.program_id().to_string().into());
|
||||||
|
results.insert("aggregator_id".into(), sp1.aggregator_id().to_string().into());
|
||||||
|
|
||||||
if matches!(mode, "execute" | "all" | "core" | "compressed") {
|
let r = match mode.as_str() {
|
||||||
let (out, report, dt) = sp1.execute(block)?;
|
"execute" => run_execute(&sp1, &shards, block.env.parent_hash, &mut results),
|
||||||
check_output(&out, &native)?;
|
"shard" => run_shard(&sp1, &shards, shard_index, out_path.as_deref(), &mut results),
|
||||||
let cycles = report.total_instruction_count();
|
"block" => run_block(&sp1, &shards, &claim, out_path.as_deref(), &mut results),
|
||||||
let gas = report.gas().unwrap_or(0);
|
"all" => run_shard(&sp1, &shards, shard_index, out_path.as_deref(), &mut results).and_then(|_| run_block(&sp1, &shards, &claim, out_path.as_deref(), &mut results)),
|
||||||
println!("RESULT execute: {} cycles, prover gas {}, {:.2} s, {:.0} cycles per EVM gas, syscalls {}", cycles, gas, dt.as_secs_f64(), cycles as f64 / native.gas_used.max(1) as f64, report.syscall_counts.values().sum::<u64>());
|
other => Err(anyhow!("unknown mode {other}")),
|
||||||
results.insert("cycles".into(), cycles.into());
|
};
|
||||||
results.insert("prover_gas".into(), gas.into());
|
// The proof system (and with it the CUDA client) is dropped here, inside the runtime guard of `main`.
|
||||||
results.insert("execute_seconds".into(), dt.as_secs_f64().into());
|
drop(sp1);
|
||||||
}
|
r?;
|
||||||
if mode == "execute" {
|
|
||||||
return finish(results, out_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(mode, "core" | "all") {
|
|
||||||
let shard = sp1.prove_shard(&witness)?;
|
|
||||||
let core_proof = shard.proof.clone().expect("prove_shard returns a core proof");
|
|
||||||
let (prove_s, verify_s, bytes) = report_proof(&sp1, "core", &core_proof, |p| sp1.verify_shard(p, &claim), &shard)?;
|
|
||||||
results.insert("core_prove_seconds".into(), prove_s.into());
|
|
||||||
results.insert("core_verify_seconds".into(), verify_s.into());
|
|
||||||
results.insert("core_proof_bytes".into(), bytes.into());
|
|
||||||
if let Some(dir) = out_path.as_deref().and_then(|p| std::path::Path::new(p).parent()) {
|
|
||||||
let _ = core_proof.save(dir.join(format!("block-{}-core.bin", block.env.number)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if matches!(mode, "compressed" | "all") {
|
|
||||||
// v0: the segment proof is the compressed proof of the single shard; the witness is re-proven in
|
|
||||||
// compressed mode (SP1 compresses by recursion over the core shards it generates itself).
|
|
||||||
let shard = proof_system::Sp1ShardProof { proof: None, witness_input: igneum_prove_core::ProveInput { block: block.clone() } };
|
|
||||||
let seg = sp1.aggregate(None, &[shard])?;
|
|
||||||
let (prove_s, verify_s, bytes) = report_proof(&sp1, "compressed", &seg.proof, |_| sp1.verify_segment(&seg, &claim), &seg)?;
|
|
||||||
results.insert("compressed_prove_seconds".into(), prove_s.into());
|
|
||||||
results.insert("compressed_verify_seconds".into(), verify_s.into());
|
|
||||||
results.insert("compressed_proof_bytes".into(), bytes.into());
|
|
||||||
if let Some(dir) = out_path.as_deref().and_then(|p| std::path::Path::new(p).parent()) {
|
|
||||||
let _ = seg.proof.save(dir.join(format!("block-{}-compressed.bin", block.env.number)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finish(results, out_path)
|
finish(results, out_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_output(out: &ProveOutput, native: &ProveOutput) -> Result<()> {
|
fn tamper_checks(shard: &BuiltShard) -> Result<()> {
|
||||||
if out != native {
|
let quiet = std::panic::take_hook();
|
||||||
bail!("the guest's public values differ from the native run:\n guest {out:?}\n native {native:?}");
|
std::panic::set_hook(Box::new(|_| {}));
|
||||||
|
let outcome = |name: &str, input: ShardInput, expect_root: B256| -> Result<()> {
|
||||||
|
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| shard_statement(&input)));
|
||||||
|
let verdict = match &r {
|
||||||
|
Err(_) => "REJECTED (the statement cannot be proven)".to_string(),
|
||||||
|
Ok(o) if o.pre_root != expect_root => format!("REJECTED (pre-root {} is not the node's {})", o.pre_root, expect_root),
|
||||||
|
Ok(_) => "ACCEPTED".to_string(),
|
||||||
|
};
|
||||||
|
println!("RESULT tamper {name}: {verdict}");
|
||||||
|
if verdict.starts_with("ACCEPTED") {
|
||||||
|
bail!("a tampered witness ({name}) was accepted");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
let expect = shard.output.pre_root;
|
||||||
|
// (a) A balance in an account leaf, re-encoded so the leaf still decodes: the witness is consistent with
|
||||||
|
// itself and the pre-root it yields is simply not the node's.
|
||||||
|
let mut a = shard.input.clone();
|
||||||
|
let addresses: Vec<B256> = a.witness.accounts.iter().map(|acc| alloy_primitives::keccak256(acc.address)).collect();
|
||||||
|
if let Some((_, v)) = a.witness.trie.leaves.iter_mut().find(|(k, _)| addresses.contains(k)) {
|
||||||
|
let mut acc = <alloy_trie::TrieAccount as alloy_rlp::Decodable>::decode(&mut v.as_ref()).expect("leaf decodes");
|
||||||
|
acc.balance += alloy_primitives::U256::from(1);
|
||||||
|
*v = alloy_rlp::encode(acc).into();
|
||||||
|
}
|
||||||
|
outcome("account balance", a, expect)?;
|
||||||
|
// (b) A storage value, where a shard touches storage; else a code byte.
|
||||||
|
let mut b = shard.input.clone();
|
||||||
|
let mut touched = false;
|
||||||
|
for acc in b.witness.accounts.iter_mut() {
|
||||||
|
if let Some((_, v)) = acc.storage.leaves.first_mut() {
|
||||||
|
let mut bytes = v.to_vec();
|
||||||
|
let last = bytes.len() - 1;
|
||||||
|
bytes[last] ^= 0x01;
|
||||||
|
*v = bytes.into();
|
||||||
|
touched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !touched {
|
||||||
|
for acc in b.witness.accounts.iter_mut() {
|
||||||
|
if !acc.code.is_empty() {
|
||||||
|
let mut bytes = acc.code.to_vec();
|
||||||
|
bytes[0] ^= 0x01;
|
||||||
|
acc.code = bytes.into();
|
||||||
|
touched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if touched {
|
||||||
|
outcome("storage or code", b, expect)?;
|
||||||
|
}
|
||||||
|
// (c) An account dropped from the witness: its leaf stays in the trie (the pre-root still matches) but the
|
||||||
|
// execution reads it and the strict database refuses.
|
||||||
|
let mut c = shard.input.clone();
|
||||||
|
let victim = c.txs.first().and_then(|t| igneum_evm_types::decode_and_check(&t.raw, c.chain_id).ok()).map(|tx| tx.sender);
|
||||||
|
if let Some(sender) = victim {
|
||||||
|
c.witness.accounts.retain(|acc| acc.address != sender);
|
||||||
|
outcome("dropped account", c, expect)?;
|
||||||
|
}
|
||||||
|
std::panic::set_hook(quiet);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_execute(sp1: &Sp1ProofSystem, shards: &[BuiltShard], parent_hash: B256, results: &mut serde_json::Map<String, serde_json::Value>) -> Result<()> {
|
||||||
|
let mut cycles_all = Vec::new();
|
||||||
|
let mut outputs = Vec::new();
|
||||||
|
for s in shards {
|
||||||
|
stage(&format!("execute shard {}", s.output.shard_index));
|
||||||
|
let (out, report, dt) = sp1.execute_shard(&s.input)?;
|
||||||
|
check_shard_output(&out, &s.output)?;
|
||||||
|
let cycles = report.total_instruction_count();
|
||||||
|
println!("RESULT execute shard {}: {} cycles, prover gas {}, {:.2} s, {:.0} cycles per EVM gas, {:.0} cycles per pgas, syscalls {} at {}", out.shard_index, cycles, report.gas().unwrap_or(0), dt.as_secs_f64(), cycles as f64 / out.gas_used.max(1) as f64, cycles as f64 / out.pgas_used.max(1) as f64, report.syscall_counts.values().sum::<u64>(), now());
|
||||||
|
cycles_all.push(serde_json::Value::from(cycles));
|
||||||
|
outputs.push(out);
|
||||||
|
}
|
||||||
|
stage("execute aggregator");
|
||||||
|
let (out, report, dt) = sp1.execute_aggregator(&outputs, parent_hash)?;
|
||||||
|
let cycles = report.total_instruction_count();
|
||||||
|
println!("RESULT execute aggregator: {} cycles over {} shards (deferred proof verification off), {:.2} s, post-root {} at {}", cycles, out.shard_count, dt.as_secs_f64(), out.post_root, now());
|
||||||
|
results.insert("shard_cycles".into(), cycles_all.into());
|
||||||
|
results.insert("aggregator_cycles".into(), cycles.into());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_shard(sp1: &Sp1ProofSystem, shards: &[BuiltShard], index: usize, out_dir: Option<&str>, results: &mut serde_json::Map<String, serde_json::Value>) -> Result<()> {
|
||||||
|
let s = shards.get(index).ok_or_else(|| anyhow!("shard {index} is not in the plan ({} shards)", shards.len()))?;
|
||||||
|
let i = s.output.shard_index;
|
||||||
|
results.insert("shard_index".into(), i.into());
|
||||||
|
stage(&format!("execute shard {i}"));
|
||||||
|
let (out, report, dt) = sp1.execute_shard(&s.input)?;
|
||||||
|
check_shard_output(&out, &s.output)?;
|
||||||
|
let cycles = report.total_instruction_count();
|
||||||
|
println!("RESULT execute shard {i}: {} cycles, prover gas {}, {:.2} s, {:.0} cycles per EVM gas, {:.0} cycles per pgas, syscalls {} at {}", cycles, report.gas().unwrap_or(0), dt.as_secs_f64(), cycles as f64 / out.gas_used.max(1) as f64, cycles as f64 / out.pgas_used.max(1) as f64, report.syscall_counts.values().sum::<u64>(), now());
|
||||||
|
results.insert("cycles".into(), cycles.into());
|
||||||
|
results.insert("prover_gas".into(), report.gas().unwrap_or(0).into());
|
||||||
|
results.insert("execute_seconds".into(), dt.as_secs_f64().into());
|
||||||
|
|
||||||
|
stage(&format!("core shard {i}"));
|
||||||
|
let (core, dt) = sp1.prove_shard_core(&s.input)?;
|
||||||
|
let (ok, vdt) = sp1.verify_shard(&core, &s.output);
|
||||||
|
let bytes = bincode::serialize(&core)?.len();
|
||||||
|
println!("RESULT core shard {i}: prove {:.1} s, proof {} bytes, verify {:.3} s, {}; post-root {} at {}", dt.as_secs_f64(), bytes, vdt.as_secs_f64(), if ok { "VERIFIED" } else { "VERIFY FAILED" }, s.output.post_root, now());
|
||||||
|
if !ok {
|
||||||
|
bail!("core proof of shard {i} did not verify");
|
||||||
|
}
|
||||||
|
results.insert("core_prove_seconds".into(), dt.as_secs_f64().into());
|
||||||
|
results.insert("core_verify_seconds".into(), vdt.as_secs_f64().into());
|
||||||
|
results.insert("core_proof_bytes".into(), bytes.into());
|
||||||
|
if let Some(dir) = out_dir.and_then(|p| std::path::Path::new(p).parent()) {
|
||||||
|
let _ = core.save(dir.join(format!("block-{}-shard-{i}-core.bin", s.input.env.number)));
|
||||||
|
}
|
||||||
|
|
||||||
|
stage(&format!("compressed shard {i}"));
|
||||||
|
let proof = sp1.prove_shard(&ShardWitness { input: s.input.clone() })?;
|
||||||
|
let dt = sp1.last_timing("compressed").unwrap_or_default();
|
||||||
|
let (ok, vdt) = sp1.verify_shard(&proof.proof, &s.output);
|
||||||
|
let bytes = bincode::serialize(&proof.proof)?.len();
|
||||||
|
println!("RESULT compressed shard {i}: prove {:.1} s, proof {} bytes, verify {:.3} s, {}; post-root {} prover {} at {}", dt.as_secs_f64(), bytes, vdt.as_secs_f64(), if ok { "VERIFIED" } else { "VERIFY FAILED" }, proof.output.post_root, proof.output.prover, now());
|
||||||
|
if !ok {
|
||||||
|
bail!("compressed proof of shard {i} did not verify");
|
||||||
|
}
|
||||||
|
results.insert("compressed_prove_seconds".into(), dt.as_secs_f64().into());
|
||||||
|
results.insert("compressed_verify_seconds".into(), vdt.as_secs_f64().into());
|
||||||
|
results.insert("compressed_proof_bytes".into(), bytes.into());
|
||||||
|
if let Some(dir) = out_dir.and_then(|p| std::path::Path::new(p).parent()) {
|
||||||
|
let _ = proof.proof.save(dir.join(format!("block-{}-shard-{i}-compressed.bin", s.input.env.number)));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prints one RESULT line for a proof: prove time (from the system's timing log), proof size (bincode, as SP1
|
fn run_block(sp1: &Sp1ProofSystem, shards: &[BuiltShard], claim: &SegmentClaim, out_dir: Option<&str>, results: &mut serde_json::Map<String, serde_json::Value>) -> Result<()> {
|
||||||
/// saves it) and verify time. Returns (prove seconds, verify seconds, bytes).
|
let block_t = Instant::now();
|
||||||
fn report_proof<T>(sp1: &Sp1ProofSystem, what: &str, proof: &sp1_sdk::SP1ProofWithPublicValues, verify: impl Fn(&T) -> bool, p: &T) -> Result<(f64, f64, usize)> {
|
let mut proofs: Vec<Sp1ShardProof> = Vec::with_capacity(shards.len());
|
||||||
let prove_s = sp1.timings.lock().unwrap().iter().rev().find(|(k, _)| k == what).map(|(_, d)| d.as_secs_f64()).ok_or_else(|| anyhow!("no timing for {what}"))?;
|
let mut shard_seconds = Vec::new();
|
||||||
let bytes = bincode::serialize(proof)?.len();
|
for s in shards {
|
||||||
let t = Instant::now();
|
let i = s.output.shard_index;
|
||||||
let ok = verify(p);
|
stage(&format!("compressed shard {i} (block mode)"));
|
||||||
let verify_s = t.elapsed().as_secs_f64();
|
let p = sp1.prove_shard(&ShardWitness { input: s.input.clone() })?;
|
||||||
let out = ProveOutput::from_bytes(proof.public_values.as_slice()).ok_or_else(|| anyhow!("public values of the {what} proof have the wrong length"))?;
|
let dt = sp1.last_timing("compressed").unwrap_or_default();
|
||||||
println!("RESULT {what}: prove {:.1} s, proof {} bytes, verify {:.3} s, {}; post-state root {} receipts root {}", prove_s, bytes, verify_s, if ok { "VERIFIED" } else { "VERIFY FAILED" }, out.post_state_root, out.receipts_root);
|
let (ok, vdt) = sp1.verify_shard(&p.proof, &s.output);
|
||||||
if !ok {
|
println!("RESULT block shard {i}: compressed prove {:.1} s, proof {} bytes, verify {:.3} s, {}, pgas {} at {}", dt.as_secs_f64(), bincode::serialize(&p.proof)?.len(), vdt.as_secs_f64(), if ok { "VERIFIED" } else { "VERIFY FAILED" }, p.output.pgas_used, now());
|
||||||
bail!("{what} proof did not verify");
|
if !ok {
|
||||||
|
bail!("compressed proof of shard {i} did not verify");
|
||||||
|
}
|
||||||
|
shard_seconds.push(serde_json::Value::from(dt.as_secs_f64()));
|
||||||
|
proofs.push(p);
|
||||||
}
|
}
|
||||||
Ok((prove_s, verify_s, bytes))
|
stage(&format!("aggregate {} shards", proofs.len()));
|
||||||
|
let seg = sp1.aggregate(None, &proofs)?;
|
||||||
|
let dt = sp1.last_timing("aggregate").unwrap_or_default();
|
||||||
|
let ok = sp1.verify_segment(&seg, claim);
|
||||||
|
let vdt = sp1.last_timing("verify-block").unwrap_or_default();
|
||||||
|
let bytes = bincode::serialize(&seg.proof)?.len();
|
||||||
|
let total = block_t.elapsed().as_secs_f64();
|
||||||
|
println!(
|
||||||
|
"RESULT block: {} shards, aggregate prove {:.1} s, proof {} bytes, verify {:.3} s, {}; block {} pre {} post {} receipts {} tx commitment {} gas {} pgas {} provers {}; shard proofs plus aggregation {:.1} s at {}",
|
||||||
|
seg.output.shard_count,
|
||||||
|
dt.as_secs_f64(),
|
||||||
|
bytes,
|
||||||
|
vdt.as_secs_f64(),
|
||||||
|
if ok { "VERIFIED (shard program id and claim checked)" } else { "VERIFY FAILED" },
|
||||||
|
seg.output.number,
|
||||||
|
seg.output.pre_root,
|
||||||
|
seg.output.post_root,
|
||||||
|
seg.output.receipts,
|
||||||
|
seg.output.tx_commitment,
|
||||||
|
seg.output.gas_used,
|
||||||
|
seg.output.pgas_used,
|
||||||
|
seg.output.provers,
|
||||||
|
total,
|
||||||
|
now()
|
||||||
|
);
|
||||||
|
if !ok {
|
||||||
|
bail!("the block proof did not verify");
|
||||||
|
}
|
||||||
|
results.insert("block_shard_prove_seconds".into(), shard_seconds.into());
|
||||||
|
results.insert("aggregate_prove_seconds".into(), dt.as_secs_f64().into());
|
||||||
|
results.insert("aggregate_verify_seconds".into(), vdt.as_secs_f64().into());
|
||||||
|
results.insert("block_proof_bytes".into(), bytes.into());
|
||||||
|
results.insert("block_total_seconds".into(), total.into());
|
||||||
|
if let Some(dir) = out_dir.and_then(|p| std::path::Path::new(p).parent()) {
|
||||||
|
let _ = seg.proof.save(dir.join(format!("block-{}-aggregated.bin", seg.output.number)));
|
||||||
|
}
|
||||||
|
let _ = BlockOutput::LEN;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_shard_output(out: &ShardOutput, native: &ShardOutput) -> Result<()> {
|
||||||
|
if out != native {
|
||||||
|
bail!("the guest's public values differ from the native run:\n guest {out:?}\n native {native:?}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(results: serde_json::Map<String, serde_json::Value>, out_path: Option<String>) -> Result<()> {
|
fn finish(results: serde_json::Map<String, serde_json::Value>, out_path: Option<String>) -> Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
//! The versioned prover trait of `docs/design/execution-layer.md` section 5.6, with the two implementations the
|
//! The versioned prover trait of `docs/design/execution-layer.md` section 5.6, with the two implementations the
|
||||||
//! design names: the devnet stub (5.7, a signed claim) and version 1, SP1 (Hypercube class, hash-based).
|
//! design names: the devnet stub (5.7, a signed claim) and version 1, SP1 (Hypercube class, hash-based).
|
||||||
//!
|
//!
|
||||||
//! v0 scope. A shard is one whole chain block (the shard planner of 5.1 is not here yet); `aggregate` over one
|
//! Devnet v4 scope. A shard is a planned range of a segment with its witness (design 5.1); `prove_shard` makes
|
||||||
//! shard is SP1's compress stage on that shard's witness (recursion over several shard proofs is the next step);
|
//! the compressed shard proof the aggregator can verify (the core proof is made separately for the
|
||||||
//! `wrap` (Groth16 or Plonk over bn254 for the bridge verifier and light clients, ledger P3) is not run in v0
|
//! measurement); `aggregate` proves the aggregator guest over the shard proofs by recursion (design 5.3), with
|
||||||
//! and returns an error. Ledger P12 (the prover's payout key inside the shard statement) is also not in v0:
|
//! the previous segment's proof when given (the chain rule); `wrap` (Groth16 or Plonk over bn254, ledger P3)
|
||||||
//! `ShardWitness.prover` is carried but not committed by the guest yet.
|
//! is not run and returns an error. The prover's payout address is in every shard statement (ledger P12).
|
||||||
|
|
||||||
#![allow(dead_code)] // wrap, verify_wrapped, pgas_table, shard_index and prover are the trait surface of design 5.6, unused in v0
|
#![allow(dead_code)] // wrap, verify_wrapped and pgas_table are the trait surface of design 5.6, unused here
|
||||||
|
|
||||||
use alloy_primitives::{keccak256, B256};
|
use alloy_primitives::{keccak256, B256};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use igneum_prove_core::{BlockFixture, ProveInput, ProveOutput};
|
use igneum_prove_core::agg::{self, AggInput, BlockOutput, PrevLink};
|
||||||
|
use igneum_prove_core::shard::{shard_statement, ShardInput, ShardOutput};
|
||||||
use sp1_sdk::blocking::{Prover, ProveRequest, ProverClient, SP1Stdin};
|
use sp1_sdk::blocking::{Prover, ProveRequest, ProverClient, SP1Stdin};
|
||||||
use sp1_sdk::{Elf, HashableKey, ProvingKey, SP1ProofWithPublicValues, SP1VerifyingKey};
|
use sp1_sdk::{Elf, HashableKey, ProvingKey, SP1Proof, SP1ProofWithPublicValues, SP1VerifyingKey};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
/// The prototype pgas table identity (design 4.2, the table itself lives in `igneum_prove_core::pgas`).
|
/// The prototype pgas table identity (design 4.2, the table itself lives in `igneum_prove_core::pgas`).
|
||||||
|
|
@ -23,17 +24,13 @@ pub struct PgasTable {
|
||||||
pub version: u16,
|
pub version: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a shard proof is about: the segment's block environment and ordered transactions plus the pre-state
|
/// What a shard proof is about (design 5.1): the shard's input, witness and prover included.
|
||||||
/// the shard touches (v0: the whole in-memory state). `prover` is the payout key of design 5.4 (not yet in the
|
|
||||||
/// statement, ledger P12).
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ShardWitness {
|
pub struct ShardWitness {
|
||||||
pub block: BlockFixture,
|
pub input: ShardInput,
|
||||||
pub shard_index: u32,
|
|
||||||
pub prover: B256,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The claim a proof record makes (design 5.4): the segment, its pre- and post-roots and receipts root.
|
/// The claim a proof record makes (design 5.4): the segment, its pre- and post-roots and receipts commitment.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct SegmentClaim {
|
pub struct SegmentClaim {
|
||||||
pub segment: B256,
|
pub segment: B256,
|
||||||
|
|
@ -43,8 +40,8 @@ pub struct SegmentClaim {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SegmentClaim {
|
impl SegmentClaim {
|
||||||
pub fn from_output(o: &ProveOutput) -> Self {
|
pub fn from_block(o: &BlockOutput) -> Self {
|
||||||
Self { segment: o.block_hash, pre_root: o.pre_state_root, post_root: o.post_state_root, receipts: o.receipts_root }
|
Self { segment: o.block_hash, pre_root: o.pre_root, post_root: o.post_root, receipts: o.receipts }
|
||||||
}
|
}
|
||||||
pub fn digest(&self) -> B256 {
|
pub fn digest(&self) -> B256 {
|
||||||
let mut buf = Vec::with_capacity(128);
|
let mut buf = Vec::with_capacity(128);
|
||||||
|
|
@ -61,7 +58,7 @@ pub trait ProofSystem: Send + Sync {
|
||||||
type ShardProof;
|
type ShardProof;
|
||||||
type SegmentProof;
|
type SegmentProof;
|
||||||
type WrappedProof;
|
type WrappedProof;
|
||||||
/// Hash of the executor guest (SP1: the verifying key hash; stub: a fixed tag).
|
/// Hash of the executor guest (SP1: the shard program's verifying key hash; stub: a fixed tag).
|
||||||
fn program_id(&self) -> B256;
|
fn program_id(&self) -> B256;
|
||||||
fn prove_shard(&self, w: &ShardWitness) -> Result<Self::ShardProof>;
|
fn prove_shard(&self, w: &ShardWitness) -> Result<Self::ShardProof>;
|
||||||
fn aggregate(&self, prev: Option<&Self::SegmentProof>, shards: &[Self::ShardProof]) -> Result<Self::SegmentProof>;
|
fn aggregate(&self, prev: Option<&Self::SegmentProof>, shards: &[Self::ShardProof]) -> Result<Self::SegmentProof>;
|
||||||
|
|
@ -72,14 +69,21 @@ pub trait ProofSystem: Send + Sync {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------
|
||||||
// Version 0: the devnet stub of design 5.7. `prove_shard` executes natively and signs the claim with a devnet
|
// Version 0: the devnet stub of design 5.7. `prove_shard` executes natively and signs the shard output with a
|
||||||
// key (keccak MAC); `verify_segment` checks the MAC. Never a mainnet version.
|
// devnet key (keccak MAC); `aggregate` chains the shard outputs natively; `verify_segment` checks the MAC.
|
||||||
|
// Never a mainnet version.
|
||||||
|
|
||||||
pub struct StubProofSystem {
|
pub struct StubProofSystem {
|
||||||
pub key: [u8; 32],
|
pub key: [u8; 32],
|
||||||
table: PgasTable,
|
table: PgasTable,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct StubShardProof {
|
||||||
|
pub output: ShardOutput,
|
||||||
|
pub mac: B256,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct StubProof {
|
pub struct StubProof {
|
||||||
pub claim: SegmentClaim,
|
pub claim: SegmentClaim,
|
||||||
|
|
@ -88,38 +92,46 @@ pub struct StubProof {
|
||||||
|
|
||||||
impl StubProofSystem {
|
impl StubProofSystem {
|
||||||
pub fn new(key: [u8; 32]) -> Self {
|
pub fn new(key: [u8; 32]) -> Self {
|
||||||
Self { key, table: PgasTable { name: "prototype-fb33069", version: 0 } }
|
Self { key, table: PgasTable { name: "prototype-b7fca5a0", version: 0 } }
|
||||||
}
|
}
|
||||||
fn mac(&self, claim: &SegmentClaim) -> B256 {
|
fn mac(&self, digest: B256) -> B256 {
|
||||||
let mut buf = Vec::with_capacity(64);
|
let mut buf = Vec::with_capacity(64);
|
||||||
buf.extend_from_slice(&self.key);
|
buf.extend_from_slice(&self.key);
|
||||||
buf.extend_from_slice(claim.digest().as_slice());
|
buf.extend_from_slice(digest.as_slice());
|
||||||
keccak256(buf)
|
keccak256(buf)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProofSystem for StubProofSystem {
|
impl ProofSystem for StubProofSystem {
|
||||||
const VERSION: u16 = 0;
|
const VERSION: u16 = 0;
|
||||||
type ShardProof = StubProof;
|
type ShardProof = StubShardProof;
|
||||||
type SegmentProof = StubProof;
|
type SegmentProof = StubProof;
|
||||||
type WrappedProof = StubProof;
|
type WrappedProof = StubProof;
|
||||||
|
|
||||||
fn program_id(&self) -> B256 {
|
fn program_id(&self) -> B256 {
|
||||||
keccak256(b"igneum-proof-system-stub-v0")
|
keccak256(b"igneum-proof-system-stub-v0")
|
||||||
}
|
}
|
||||||
fn prove_shard(&self, w: &ShardWitness) -> Result<StubProof> {
|
fn prove_shard(&self, w: &ShardWitness) -> Result<StubShardProof> {
|
||||||
let out = igneum_prove_core::executor::prove_statement(&w.block);
|
let output = shard_statement(&w.input);
|
||||||
let claim = SegmentClaim::from_output(&out);
|
Ok(StubShardProof { mac: self.mac(keccak256(output.to_bytes())), output })
|
||||||
Ok(StubProof { mac: self.mac(&claim), claim })
|
|
||||||
}
|
}
|
||||||
fn aggregate(&self, _prev: Option<&StubProof>, shards: &[StubProof]) -> Result<StubProof> {
|
fn aggregate(&self, _prev: Option<&StubProof>, shards: &[StubShardProof]) -> Result<StubProof> {
|
||||||
shards.first().cloned().ok_or_else(|| anyhow!("no shards"))
|
for s in shards {
|
||||||
|
if s.mac != self.mac(keccak256(s.output.to_bytes())) {
|
||||||
|
return Err(anyhow!("stub shard proof {} has a bad MAC", s.output.shard_index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let parent_hash = B256::ZERO;
|
||||||
|
let input = AggInput { shard_vk: [0; 8], shards: shards.iter().map(|s| s.output.to_bytes()).collect(), parent_hash, prev: None };
|
||||||
|
let out = agg::aggregate(&input, &mut |_, _| {});
|
||||||
|
let claim = SegmentClaim::from_block(&out);
|
||||||
|
Ok(StubProof { mac: self.mac(claim.digest()), claim })
|
||||||
}
|
}
|
||||||
fn wrap(&self, p: &StubProof) -> Result<StubProof> {
|
fn wrap(&self, p: &StubProof) -> Result<StubProof> {
|
||||||
Ok(p.clone())
|
Ok(p.clone())
|
||||||
}
|
}
|
||||||
fn verify_segment(&self, p: &StubProof, claim: &SegmentClaim) -> bool {
|
fn verify_segment(&self, p: &StubProof, claim: &SegmentClaim) -> bool {
|
||||||
&p.claim == claim && p.mac == self.mac(claim)
|
&p.claim == claim && p.mac == self.mac(claim.digest())
|
||||||
}
|
}
|
||||||
fn verify_wrapped(&self, p: &StubProof, claim: &SegmentClaim) -> bool {
|
fn verify_wrapped(&self, p: &StubProof, claim: &SegmentClaim) -> bool {
|
||||||
self.verify_segment(p, claim)
|
self.verify_segment(p, claim)
|
||||||
|
|
@ -130,72 +142,118 @@ impl ProofSystem for StubProofSystem {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------------------------
|
||||||
// Version 1: SP1. Core proof per shard, compressed proof as the segment proof (v0: one shard, so the segment
|
// Version 1: SP1. Compressed proof per shard, the aggregator guest's compressed proof as the segment proof,
|
||||||
// proof is the compressed proof of that shard's witness), wrap deferred.
|
// wrap deferred.
|
||||||
|
|
||||||
pub struct Sp1ProofSystem {
|
pub struct Sp1ProofSystem {
|
||||||
client: sp1_sdk::blocking::EnvProver,
|
client: sp1_sdk::blocking::EnvProver,
|
||||||
pk: sp1_sdk::blocking::EnvProvingKey,
|
shard_pk: sp1_sdk::blocking::EnvProvingKey,
|
||||||
vk: SP1VerifyingKey,
|
shard_vk: SP1VerifyingKey,
|
||||||
|
agg_pk: sp1_sdk::blocking::EnvProvingKey,
|
||||||
|
agg_vk: SP1VerifyingKey,
|
||||||
table: PgasTable,
|
table: PgasTable,
|
||||||
pub timings: std::sync::Mutex<Vec<(String, Duration)>>,
|
pub timings: std::sync::Mutex<Vec<(String, Duration)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Sp1ShardProof {
|
pub struct Sp1ShardProof {
|
||||||
/// The core proof; `None` for a witness-only shard handed to `aggregate` when the core stage is skipped (v0).
|
/// The compressed (recursion) proof, what the aggregator verifies.
|
||||||
pub proof: Option<SP1ProofWithPublicValues>,
|
pub proof: SP1ProofWithPublicValues,
|
||||||
pub witness_input: ProveInput,
|
pub output: ShardOutput,
|
||||||
|
/// The segment's parent chain block (the chain rule binds the previous proof to it).
|
||||||
|
pub parent_hash: B256,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Sp1SegmentProof {
|
pub struct Sp1SegmentProof {
|
||||||
pub proof: SP1ProofWithPublicValues,
|
pub proof: SP1ProofWithPublicValues,
|
||||||
|
pub output: BlockOutput,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Sp1WrappedProof(pub SP1ProofWithPublicValues);
|
pub struct Sp1WrappedProof(pub SP1ProofWithPublicValues);
|
||||||
|
|
||||||
impl Sp1ProofSystem {
|
impl Sp1ProofSystem {
|
||||||
/// Builds the prover from the environment (`SP1_PROVER` = cpu, cuda or mock) and runs the key setup.
|
/// Builds the prover from the environment (`SP1_PROVER` = cpu, cuda or mock) and runs both key setups.
|
||||||
pub fn from_env(elf: Elf) -> Result<Self> {
|
pub fn from_env(shard_elf: Elf, agg_elf: Elf) -> Result<Self> {
|
||||||
let client = ProverClient::from_env();
|
let client = ProverClient::from_env();
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
let pk = client.setup(elf)?;
|
let shard_pk = client.setup(shard_elf)?;
|
||||||
let vk = pk.verifying_key().clone();
|
let shard_vk = shard_pk.verifying_key().clone();
|
||||||
let setup = t.elapsed();
|
let setup_shard = t.elapsed();
|
||||||
Ok(Self { client, pk, vk, table: PgasTable { name: "prototype-fb33069", version: 1 }, timings: std::sync::Mutex::new(vec![("setup".into(), setup)]) })
|
let t = Instant::now();
|
||||||
|
let agg_pk = client.setup(agg_elf)?;
|
||||||
|
let agg_vk = agg_pk.verifying_key().clone();
|
||||||
|
let setup_agg = t.elapsed();
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
shard_pk,
|
||||||
|
shard_vk,
|
||||||
|
agg_pk,
|
||||||
|
agg_vk,
|
||||||
|
table: PgasTable { name: "prototype-b7fca5a0", version: 1 },
|
||||||
|
timings: std::sync::Mutex::new(vec![("setup-shard".into(), setup_shard), ("setup-aggregator".into(), setup_agg)]),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn vk(&self) -> &SP1VerifyingKey {
|
pub fn shard_vk_hash(&self) -> [u32; 8] {
|
||||||
&self.vk
|
self.shard_vk.hash_u32()
|
||||||
|
}
|
||||||
|
pub fn agg_vk_hash(&self) -> [u32; 8] {
|
||||||
|
self.agg_vk.hash_u32()
|
||||||
|
}
|
||||||
|
pub fn aggregator_id(&self) -> B256 {
|
||||||
|
agg::vk_bytes(&self.agg_vk_hash())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stdin_for(block: &BlockFixture) -> Result<SP1Stdin> {
|
pub fn stdin_for(input: &ShardInput) -> Result<SP1Stdin> {
|
||||||
let mut stdin = SP1Stdin::new();
|
let mut stdin = SP1Stdin::new();
|
||||||
stdin.write_vec(bincode::serialize(&ProveInput { block: block.clone() })?);
|
stdin.write_vec(bincode::serialize(input)?);
|
||||||
Ok(stdin)
|
Ok(stdin)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute(&self, block: &BlockFixture) -> Result<(ProveOutput, sp1_sdk::ExecutionReport, Duration)> {
|
pub fn execute_shard(&self, input: &ShardInput) -> Result<(ShardOutput, sp1_sdk::ExecutionReport, Duration)> {
|
||||||
let stdin = Self::stdin_for(block)?;
|
let stdin = Self::stdin_for(input)?;
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
let (pv, report) = self.client.execute(self.pk.elf().clone(), stdin).calculate_gas(true).run().map_err(|e| anyhow!("{e}"))?;
|
let (pv, report) = self.client.execute(self.shard_pk.elf().clone(), stdin).calculate_gas(true).run().map_err(|e| anyhow!("{e}"))?;
|
||||||
let dt = t.elapsed();
|
let dt = t.elapsed();
|
||||||
let out = ProveOutput::from_bytes(pv.as_slice()).ok_or_else(|| anyhow!("public values are {} bytes, expected {}", pv.as_slice().len(), ProveOutput::LEN))?;
|
let out = ShardOutput::from_bytes(pv.as_slice()).ok_or_else(|| anyhow!("public values are {} bytes, expected {}", pv.as_slice().len(), ShardOutput::LEN))?;
|
||||||
Ok((out, report, dt))
|
Ok((out, report, dt))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Executes the aggregator over the shards' public values without proofs (deferred verification off):
|
||||||
|
/// the cycle count of the aggregation statement itself.
|
||||||
|
pub fn execute_aggregator(&self, shard_outputs: &[ShardOutput], parent_hash: B256) -> Result<(BlockOutput, sp1_sdk::ExecutionReport, Duration)> {
|
||||||
|
let input = AggInput { shard_vk: self.shard_vk_hash(), shards: shard_outputs.iter().map(|o| o.to_bytes()).collect(), parent_hash, prev: None };
|
||||||
|
let mut stdin = SP1Stdin::new();
|
||||||
|
stdin.write_vec(bincode::serialize(&input)?);
|
||||||
|
let t = Instant::now();
|
||||||
|
let (pv, report) = self.client.execute(self.agg_pk.elf().clone(), stdin).deferred_proof_verification(false).calculate_gas(true).run().map_err(|e| anyhow!("{e}"))?;
|
||||||
|
let dt = t.elapsed();
|
||||||
|
let out = BlockOutput::from_bytes(pv.as_slice()).ok_or_else(|| anyhow!("block public values are {} bytes, expected {}", pv.as_slice().len(), BlockOutput::LEN))?;
|
||||||
|
Ok((out, report, dt))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A core (STARK) proof of one shard, for the measurement; not what the aggregator consumes.
|
||||||
|
pub fn prove_shard_core(&self, input: &ShardInput) -> Result<(SP1ProofWithPublicValues, Duration)> {
|
||||||
|
let stdin = Self::stdin_for(input)?;
|
||||||
|
let t = Instant::now();
|
||||||
|
let proof = self.client.prove(&self.shard_pk, stdin).core().run()?;
|
||||||
|
let dt = t.elapsed();
|
||||||
|
self.record("core", dt);
|
||||||
|
Ok((proof, dt))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_shard(&self, proof: &SP1ProofWithPublicValues, expected: &ShardOutput) -> (bool, Duration) {
|
||||||
|
let t = Instant::now();
|
||||||
|
let ok = self.client.verify(proof, &self.shard_vk, None).is_ok();
|
||||||
|
let dt = t.elapsed();
|
||||||
|
(ok && ShardOutput::from_bytes(proof.public_values.as_slice()).as_ref() == Some(expected), dt)
|
||||||
|
}
|
||||||
|
|
||||||
fn record(&self, what: &str, dt: Duration) {
|
fn record(&self, what: &str, dt: Duration) {
|
||||||
self.timings.lock().unwrap().push((what.to_string(), dt));
|
self.timings.lock().unwrap().push((what.to_string(), dt));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn output_of(proof: &SP1ProofWithPublicValues) -> Option<ProveOutput> {
|
pub fn last_timing(&self, what: &str) -> Option<Duration> {
|
||||||
ProveOutput::from_bytes(proof.public_values.as_slice())
|
self.timings.lock().unwrap().iter().rev().find(|(k, _)| k == what).map(|(_, d)| *d)
|
||||||
}
|
|
||||||
|
|
||||||
fn check(&self, proof: &SP1ProofWithPublicValues, claim: &SegmentClaim) -> bool {
|
|
||||||
let t = Instant::now();
|
|
||||||
let ok = self.client.verify(proof, &self.vk, None).is_ok();
|
|
||||||
self.record("verify", t.elapsed());
|
|
||||||
ok && Self::output_of(proof).map(|o| &SegmentClaim::from_output(&o) == claim).unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,48 +264,63 @@ impl ProofSystem for Sp1ProofSystem {
|
||||||
type WrappedProof = Sp1WrappedProof;
|
type WrappedProof = Sp1WrappedProof;
|
||||||
|
|
||||||
fn program_id(&self) -> B256 {
|
fn program_id(&self) -> B256 {
|
||||||
B256::from_slice(&self.vk.bytes32_raw())
|
agg::vk_bytes(&self.shard_vk_hash())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prove_shard(&self, w: &ShardWitness) -> Result<Sp1ShardProof> {
|
fn prove_shard(&self, w: &ShardWitness) -> Result<Sp1ShardProof> {
|
||||||
let stdin = Self::stdin_for(&w.block)?;
|
let stdin = Self::stdin_for(&w.input)?;
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
let proof = self.client.prove(&self.pk, stdin).core().run()?;
|
let proof = self.client.prove(&self.shard_pk, stdin).compressed().run()?;
|
||||||
self.record("core", t.elapsed());
|
self.record("compressed", t.elapsed());
|
||||||
Ok(Sp1ShardProof { proof: Some(proof), witness_input: ProveInput { block: w.block.clone() } })
|
let output = ShardOutput::from_bytes(proof.public_values.as_slice()).ok_or_else(|| anyhow!("shard public values have the wrong length"))?;
|
||||||
|
Ok(Sp1ShardProof { proof, output, parent_hash: w.input.env.parent_hash })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The aggregator guest over the shard proofs (and the previous segment's proof when given), by recursion.
|
||||||
fn aggregate(&self, prev: Option<&Sp1SegmentProof>, shards: &[Sp1ShardProof]) -> Result<Sp1SegmentProof> {
|
fn aggregate(&self, prev: Option<&Sp1SegmentProof>, shards: &[Sp1ShardProof]) -> Result<Sp1SegmentProof> {
|
||||||
if prev.is_some() {
|
let first = shards.first().ok_or_else(|| anyhow!("no shards"))?;
|
||||||
return Err(anyhow!("v0 has no chain recursion (segment N-1 inside segment N); next step"));
|
let mut stdin = SP1Stdin::new();
|
||||||
|
let input = AggInput {
|
||||||
|
shard_vk: self.shard_vk_hash(),
|
||||||
|
shards: shards.iter().map(|s| s.output.to_bytes()).collect(),
|
||||||
|
parent_hash: first.parent_hash,
|
||||||
|
prev: prev.map(|p| PrevLink { agg_vk: self.agg_vk_hash(), public_values: p.proof.public_values.to_vec() }),
|
||||||
|
};
|
||||||
|
stdin.write_vec(bincode::serialize(&input)?);
|
||||||
|
for s in shards {
|
||||||
|
let SP1Proof::Compressed(proof) = s.proof.proof.clone() else { return Err(anyhow!("shard {} is not a compressed proof", s.output.shard_index)) };
|
||||||
|
stdin.write_proof(*proof, self.shard_vk.vk.clone());
|
||||||
|
}
|
||||||
|
if let Some(p) = prev {
|
||||||
|
let SP1Proof::Compressed(proof) = p.proof.proof.clone() else { return Err(anyhow!("the previous block proof is not a compressed proof")) };
|
||||||
|
stdin.write_proof(*proof, self.agg_vk.vk.clone());
|
||||||
}
|
}
|
||||||
let [shard] = shards else { return Err(anyhow!("v0 aggregates exactly one shard (the whole block); got {}", shards.len())) };
|
|
||||||
let stdin = Self::stdin_for(&shard.witness_input.block)?;
|
|
||||||
let t = Instant::now();
|
let t = Instant::now();
|
||||||
let proof = self.client.prove(&self.pk, stdin).compressed().run()?;
|
let proof = self.client.prove(&self.agg_pk, stdin).compressed().run()?;
|
||||||
self.record("compressed", t.elapsed());
|
self.record("aggregate", t.elapsed());
|
||||||
Ok(Sp1SegmentProof { proof })
|
let output = BlockOutput::from_bytes(proof.public_values.as_slice()).ok_or_else(|| anyhow!("block public values have the wrong length"))?;
|
||||||
|
Ok(Sp1SegmentProof { proof, output })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wrap(&self, _p: &Sp1SegmentProof) -> Result<Sp1WrappedProof> {
|
fn wrap(&self, _p: &Sp1SegmentProof) -> Result<Sp1WrappedProof> {
|
||||||
Err(anyhow!("wrap (Groth16 or Plonk over bn254) is not run in v0: it needs SP1's circuit artifacts and is the ledger P3 measurement"))
|
Err(anyhow!("wrap (Groth16 or Plonk over bn254) is not run: it needs SP1's circuit artifacts and is the ledger P3 measurement"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_segment(&self, p: &Sp1SegmentProof, claim: &SegmentClaim) -> bool {
|
fn verify_segment(&self, p: &Sp1SegmentProof, claim: &SegmentClaim) -> bool {
|
||||||
self.check(&p.proof, claim)
|
let t = Instant::now();
|
||||||
|
let ok = self.client.verify(&p.proof, &self.agg_vk, None).is_ok();
|
||||||
|
self.record("verify-block", t.elapsed());
|
||||||
|
let Some(out) = BlockOutput::from_bytes(p.proof.public_values.as_slice()) else { return false };
|
||||||
|
// The aggregated statement is about the real shard program, and about this aggregator when it chains.
|
||||||
|
ok && out.shard_vk == self.program_id() && (out.chain_len == 1 || out.agg_vk == self.aggregator_id()) && &SegmentClaim::from_block(&out) == claim
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_wrapped(&self, p: &Sp1WrappedProof, claim: &SegmentClaim) -> bool {
|
fn verify_wrapped(&self, p: &Sp1WrappedProof, claim: &SegmentClaim) -> bool {
|
||||||
self.check(&p.0, claim)
|
let Some(out) = BlockOutput::from_bytes(p.0.public_values.as_slice()) else { return false };
|
||||||
|
self.client.verify(&p.0, &self.agg_vk, None).is_ok() && &SegmentClaim::from_block(&out) == claim
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pgas_table(&self) -> &PgasTable {
|
fn pgas_table(&self) -> &PgasTable {
|
||||||
&self.table
|
&self.table
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sp1ProofSystem {
|
|
||||||
pub fn verify_shard(&self, p: &Sp1ShardProof, claim: &SegmentClaim) -> bool {
|
|
||||||
p.proof.as_ref().map(|pr| self.check(pr, claim)).unwrap_or(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "igneum-prove-program"
|
name = "igneum-prove-program"
|
||||||
description = "SP1 guest: re-executes one Igneum chain block and commits the post-state root and the receipts root"
|
description = "SP1 guest: proves one shard of an Igneum chain block (design 5.1): witness checked against the pre-root, the shard executed, the post-root and the prover committed"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
//! The guest. Input: bincode of `ProveInput` (the block fixture). Output (public values): `ProveOutput` in its
|
//! The shard guest. Input: bincode of `ShardInput` (environment, the shard's transactions, the carried-in
|
||||||
//! fixed byte layout. Everything between is `igneum_prove_core::executor::prove_statement`, the same code the
|
//! position, the prover's payout address, the witness). Output (public values): `ShardOutput` in its fixed byte
|
||||||
//! host runs natively first.
|
//! layout. Everything between is `igneum_prove_core::shard::shard_statement`, the same code the host runs
|
||||||
|
//! natively first.
|
||||||
|
|
||||||
#![no_main]
|
#![no_main]
|
||||||
sp1_zkvm::entrypoint!(main);
|
sp1_zkvm::entrypoint!(main);
|
||||||
|
|
||||||
use igneum_prove_core::executor::prove_statement;
|
use igneum_prove_core::shard::{shard_statement, ShardInput};
|
||||||
use igneum_prove_core::ProveInput;
|
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
let input = sp1_zkvm::io::read_vec();
|
let input = sp1_zkvm::io::read_vec();
|
||||||
let input: ProveInput = bincode::deserialize(&input).expect("ProveInput decodes");
|
let input: ShardInput = bincode::deserialize(&input).expect("ShardInput decodes");
|
||||||
let out = prove_statement(&input.block);
|
let out = shard_statement(&input);
|
||||||
sp1_zkvm::io::commit_slice(&out.to_bytes());
|
sp1_zkvm::io::commit_slice(&out.to_bytes());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue