diff --git a/Cargo.lock b/Cargo.lock index f387371efac..d7df7cfcc7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4816,7 +4816,7 @@ dependencies = [ [[package]] name = "mithril-stm" -version = "0.11.3" +version = "0.11.4" dependencies = [ "anyhow", "blake2 0.10.6", diff --git a/mithril-common/Cargo.toml b/mithril-common/Cargo.toml index 06d6771ae58..e22693786df 100644 --- a/mithril-common/Cargo.toml +++ b/mithril-common/Cargo.toml @@ -46,7 +46,7 @@ fixed = "1.31.0" hex = { workspace = true } kes-summed-ed25519 = { version = "0.2.1", features = ["serde_enabled", "sk_clone_enabled"] } mithril-merkle-tree = { path = "../internal/mithril-merkle-tree", version = "0.1.4" } -mithril-stm = { path = "../mithril-stm", version = "0.11.3", default-features = false } +mithril-stm = { path = "../mithril-stm", version = "0.11.4", default-features = false } nom = "8.0.0" rand_chacha = { workspace = true } rand_core = { workspace = true } diff --git a/mithril-stm/CHANGELOG.md b/mithril-stm/CHANGELOG.md index 70704e401b2..837c4173eef 100644 --- a/mithril-stm/CHANGELOG.md +++ b/mithril-stm/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.11.4 (07-27-2026) + +### Added + +- Added benchmarks for the recursive IVC (SNARK) circuit covering proving (Poseidon and Blake2b transcripts), verification, and off-circuit accumulator folding across the genesis, same-epoch, and next-epoch transition paths, plus cold/warm setup measurements for the SRS and circuit keys, all driven through a `benchmark-internals` façade over the production proof system. +- Added a `mithril-stm/benches/README.md` documenting the recursive IVC and non-recursive certificate benchmarks and the IVC harness's fail-closed CLI. + ## 0.11.3 (07-22-2026) ### Changed diff --git a/mithril-stm/Cargo.toml b/mithril-stm/Cargo.toml index 99805f7fccc..e0b14255715 100644 --- a/mithril-stm/Cargo.toml +++ b/mithril-stm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-stm" -version = "0.11.3" +version = "0.11.4" edition = { workspace = true } authors = { workspace = true } homepage = { workspace = true } @@ -114,6 +114,11 @@ name = "halo2_prover_modes" harness = false required-features = ["future_snark", "benchmark-internals"] +[[bench]] +name = "ivc_halo2_snark" +harness = false +required-features = ["future_snark", "benchmark-internals"] + [[bench]] name = "stm" harness = false diff --git a/mithril-stm/benches/README.md b/mithril-stm/benches/README.md new file mode 100644 index 00000000000..bc4ba2ebe9a --- /dev/null +++ b/mithril-stm/benches/README.md @@ -0,0 +1,145 @@ +# `mithril-stm` benchmarks + +This folder holds the benchmark harnesses for `mithril-stm`. The two Halo2 **circuit** benchmark suites are +the focus of this document: + +- the **recursive IVC circuit** — [`ivc_halo2_snark`](ivc_halo2_snark.rs), which ships a small **CLI** to + list and select what runs; +- the **non-recursive certificate circuit** — [`halo2_snark`](halo2_snark.rs) and + [`halo2_prover_modes`](halo2_prover_modes.rs). + +Every harness is declared `harness = false` in `Cargo.toml` (a custom `main`, or a Criterion-generated +`main`), and each delegates to a **benchmark-only façade** in the library — +`circuits::halo2_ivc::bench::helpers` and `circuits::halo2::bench::helpers` — so the measured operations run +the same production code paths. (The one exception is the IVC `verify/kzg_opening` diagnostic, which +reproduces just the KZG-opening sub-step of `IvcProof::verify`.) The façades live in the library (not here) +because they call `pub(crate)`/private production code; these harness files are the thin public-API +front-ends. + +## Prerequisites + +- **Toolchain:** the crate's dev-dependencies require `rustc ≥ 1.88`. The commands below pin `+1.88.0`; use + any installed toolchain `≥ 1.88`, or set it as the default and drop the `+1.88.0`. +- **Features:** all circuit benches require `--features future_snark,benchmark-internals`. +- **Resources:** the recursive circuit runs at degree 19 (GB-scale RAM, minutes per proof); the non-recursive + `production` tier needs ≥ 70 GB RAM (server-class). Scope your run accordingly. + +General invocation: + +```bash +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench -- +``` + +Everything after `--` is passed to the benchmark binary. + +## Benchmark index + +| Bench | Circuit / area | What it measures | Selection | +| ------------------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------- | +| `ivc_halo2_snark` | recursive IVC circuit | prove / verify / fold per transition path + setup (cold/warm), single observation | **custom CLI** (`--list`, literal id/prefix filter) | +| `halo2_snark` | non-recursive certificate circuit | constraints, VK size, proof size, prove & verify time across parameter tiers | Criterion (filter `certificate/`) | +| `halo2_prover_modes` | non-recursive certificate circuit | mock-prover vs real-prover cost projected to an e2e run, across `k` tiers | no arguments | +| `multi_sig`, `schnorr_sig`, `stm`, `size_benches` | other crate areas (not Halo2 circuits) | see each file | — | + +--- + +## Recursive IVC circuit — `ivc_halo2_snark` + +Exercises the recursive IVC prover/verifier at its production degree (19), using the small committed +certificate as the inner proof. It measures, for each of the three transition **paths** — `genesis`, +`same_epoch`, `next_epoch`: + +- **prove** with each transcript: `prove/poseidon`, `prove/blake2b`; +- **verify**: `verify/full` (message binding + KZG opening + folded-accumulator pairing) and + `verify/kzg_opening` (the isolated opening); +- **fold**: the off-circuit accumulator fold (`genesis` has none — it is a passthrough); + +plus **setup** measured cold vs warm: `setup/srs` and `setup/keys`. Each measurement is a single +observation, printed as a small table when the run finishes. + +### The CLI + +Because a full run is expensive, this harness parses its own arguments (fail-closed, so a stray option can +never silently trigger a multi-minute key generation). Arguments go after `--`: + +```bash +# List every benchmark id — no benchmark setup or key generation (Cargo may still compile the target): +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench ivc_halo2_snark -- --list + +# Show usage: +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench ivc_halo2_snark -- --help + +# Run everything (tens of minutes; performs TWO recursive key generations — the shared per-path +# environment and the cold setup/keys measurement — then all proofs): +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench ivc_halo2_snark + +# Run a subset: pass ONE literal id or prefix (substring match against the ids from --list). +# One transition path (prove + verify + fold): +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench ivc_halo2_snark -- ivc/same_epoch +# One path's verification only: +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench ivc_halo2_snark -- ivc/genesis/verify +# SRS cold vs warm (no key generation): +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench ivc_halo2_snark -- ivc/setup/srs +# Keys cold vs warm (cold performs a full recursive key generation): +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench ivc_halo2_snark -- ivc/setup/keys +``` + +The filter is a **literal** substring, not a regex. The parser rejects (rather than silently ignores): +regex metacharacters in the filter, any value-taking option (`--sample-size`, `--color`, `--save-baseline`, +…), and `--exact` / `--test` / `--profile-time` (the last two would force a recursive keygen). Valueless +flags that `cargo bench` injects (`--bench`, `--nocapture`, `--quiet`, `--verbose`) are tolerated. + +Benchmark ids: + +``` +ivc/{genesis,same_epoch,next_epoch}/prove/{poseidon,blake2b} +ivc/{genesis,same_epoch,next_epoch}/verify/{full,kzg_opening} +ivc/{same_epoch,next_epoch}/fold # genesis has no fold +ivc/setup/{srs,keys} +``` + +Filtering to a subset still builds the shared environment it needs (one recursive keygen), so isolated +verify/fold runs pay that cost up front. + +--- + +## Non-recursive certificate circuit — `halo2_snark`, `halo2_prover_modes` + +### `halo2_snark` + +For each parameter **tier** it reports constraint rows vs `2^k`, advice columns, VK size, proof size, and +proving/verification time. The two cheap tiers (`small`, `medium`) are **Criterion-sampled** (10 samples); +the two expensive tiers (`large`, `production`) print a **single manually-timed observation** (10 Criterion +samples would be prohibitive). + +**Always pass a `certificate/` filter.** The harness runs every tier that a positional filter does not +exclude, so a bare invocation — or a run whose only arguments are Criterion control flags such as `--list`, +which are not positional filters — executes **all four tiers, including `production` (≥ 70 GB RAM)**. +Criterion's list/filter modes do **not** gate the manually-timed `large`/`production` tiers; only a +positional `certificate/` filter does. + +```bash +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench halo2_snark -- certificate/small +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench halo2_snark -- certificate/medium +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench halo2_snark -- certificate/large +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench halo2_snark -- certificate/production +``` + +| Tier | Quorum | `k` | Measurement | +| ------------ | ------ | --- | --------------------- | +| `small` | 3 | 13 | Criterion, 10 samples | +| `medium` | 32 | 16 | Criterion, 10 samples | +| `large` | 1024 | 21 | single observation | +| `production` | 1944 | 22 | single observation | + +`small` is the lightest and a good smoke test after touching the circuit or the façade. `production` requires +≥ 70 GB RAM (server only). + +### `halo2_prover_modes` + +Takes no arguments — it sweeps a range of `k` tiers and prints, for each, the mock-prover vs real-prover +timings projected onto a standard e2e run (~80 certificates): + +```bash +cargo +1.88.0 bench -p mithril-stm --features future_snark,benchmark-internals --bench halo2_prover_modes +``` diff --git a/mithril-stm/benches/halo2_prover_modes.rs b/mithril-stm/benches/halo2_prover_modes.rs index 766e00cecac..d0bacf0c721 100644 --- a/mithril-stm/benches/halo2_prover_modes.rs +++ b/mithril-stm/benches/halo2_prover_modes.rs @@ -1,6 +1,6 @@ use std::time::Instant; -use mithril_stm::circuits::halo2::bench_helpers::{BenchEnv, compute_circuit_degree}; +use mithril_stm::circuits::halo2::bench::helpers::{BenchEnv, compute_circuit_degree}; fn fmt_ms(ms: u128) -> String { if ms >= 1000 { diff --git a/mithril-stm/benches/halo2_snark.rs b/mithril-stm/benches/halo2_snark.rs index a772cf2610c..954667de57c 100644 --- a/mithril-stm/benches/halo2_snark.rs +++ b/mithril-stm/benches/halo2_snark.rs @@ -3,7 +3,7 @@ use std::time::Instant; use criterion::{Criterion, SamplingMode, criterion_group, criterion_main}; use std::time::Duration; -use mithril_stm::circuits::halo2::bench_helpers::BenchEnv; +use mithril_stm::circuits::halo2::bench::helpers::BenchEnv; struct Tier { name: &'static str, diff --git a/mithril-stm/benches/ivc_halo2_snark.rs b/mithril-stm/benches/ivc_halo2_snark.rs new file mode 100644 index 00000000000..a5d5080c56f --- /dev/null +++ b/mithril-stm/benches/ivc_halo2_snark.rs @@ -0,0 +1,363 @@ +//! Benchmarks for the recursive IVC circuit, driven end-to-end by the production-backed +//! [`IvcBenchEnv`] façade (`mithril-stm/src/circuits/halo2_ivc/bench/helpers.rs`). Every timed operation +//! delegates to the façade, so the measurements exercise the same code paths run in production. +//! +//! ## Running +//! +//! ```text +//! cargo bench -p mithril-stm --features future_snark,benchmark-internals --bench ivc_halo2_snark -- +//! ``` +//! +//! `` is a single **literal** substring of a benchmark id (e.g. `ivc/same_epoch/prove` or +//! `ivc/genesis/prove/poseidon`). Run `--list` to print the ids. +//! +//! ## CLI contract (restricted, fail-closed) +//! +//! Every benchmark is *manually timed* — a single observation each, gathered into consolidated tables (a +//! per-path prove/verify/fold table and a setup cold-vs-warm table) — because a recursive proof at +//! `RECURSIVE_CIRCUIT_DEGREE` costs tens of seconds and building an environment runs a full recursive +//! keygen (minutes, GB-scale RAM). So the harness validates its CLI itself, up front, before constructing +//! anything: +//! +//! - `--list` prints every benchmark id and returns — it never builds the environment (no keygen). +//! - `--test` and `--profile-time` are rejected: they would force a full recursive keygen for the shared +//! environment. Use the façade's `#[ignore]` smoke test (`facade_prepares_proves_and_verifies_all_paths`) +//! for a once-through sanity run instead. +//! - Only a few valueless flags (`--bench`, `--verbose`, `--quiet`, `--nocapture`) plus the control flags +//! above are accepted; every other option — including value-taking ones — is rejected, so nothing can +//! swallow the next token. A single literal positional filter is allowed (no regex, no `--exact`). + +use std::hint::black_box; +use std::time::{Duration, Instant}; + +use mithril_stm::circuits::halo2_ivc::{ + bench::cli::{self, BenchCli}, + bench::helpers::{IvcBenchEnv, PreparedStep, TransitionPath}, +}; +use tempfile::TempDir; + +/// The three transition paths and their id segments. +const PATHS: &[(TransitionPath, &str)] = &[ + (TransitionPath::Genesis, "genesis"), + (TransitionPath::SameEpoch, "same_epoch"), + (TransitionPath::NextEpoch, "next_epoch"), +]; + +fn die(message: &str) -> ! { + eprintln!("ivc_halo2_snark: {message}"); + std::process::exit(2); +} + +/// All benchmark ids for one transition path. Genesis has no accumulator fold (passthrough). +fn ids_for(path: TransitionPath, name: &str) -> Vec { + let mut ids = vec![ + format!("ivc/{name}/prove/poseidon"), + format!("ivc/{name}/prove/blake2b"), + format!("ivc/{name}/verify/full"), + format!("ivc/{name}/verify/kzg_opening"), + ]; + if !matches!(path, TransitionPath::Genesis) { + ids.push(format!("ivc/{name}/fold")); + } + ids +} + +/// Setup cold/warm benchmark ids. Each measures its cache **cold then warm**; they build their +/// own throwaway caches and do NOT use the shared per-path environment. +const SETUP_IDS: &[&str] = &["ivc/setup/srs", "ivc/setup/keys"]; + +/// All benchmark ids exposed by this harness: per-path prove/verify/fold, plus the setup ids. +fn all_ids() -> Vec { + PATHS + .iter() + .flat_map(|(path, name)| ids_for(*path, name)) + .chain(SETUP_IDS.iter().map(|id| id.to_string())) + .collect() +} + +/// A benchmark id runs iff no filter was given, or the literal filter is a substring of the full id. +fn selected(filter: Option<&str>, full_id: &str) -> bool { + filter.map(|f| full_id.contains(f)).unwrap_or(true) +} + +/// True if any per-path benchmark (prove/verify/fold) is selected — those share the recursive-keygen env. +fn any_path_bench_selected(filter: Option<&str>) -> bool { + PATHS.iter().any(|(path, name)| path_selected(filter, *path, name)) +} + +/// True if any setup (cold/warm) benchmark is selected. +fn any_setup_bench_selected(filter: Option<&str>) -> bool { + SETUP_IDS.iter().any(|id| selected(filter, id)) +} + +/// True if any benchmark for this path is selected (so its fixture is worth preparing). +fn path_selected(filter: Option<&str>, path: TransitionPath, name: &str) -> bool { + ids_for(path, name).iter().any(|id| selected(filter, id)) +} + +fn print_usage() { + println!( + "Recursive IVC circuit benchmarks (façade-driven).\n\n\ + Usage: cargo bench ... --bench ivc_halo2_snark -- []\n\n\ + Pass a single literal id or prefix to select benchmarks; omit it to run all. Use --list to see \ + the ids. --test and --profile-time are unsupported (they force a recursive keygen).\n\n\ + Cost: a no-filter run does the shared per-path recursive keygen AND the setup keys keygen, then \ + all proofs (~tens of minutes, GB-scale RAM). Filter to a path or id to scope the work." + ); + print_all_ids(); +} + +fn print_all_ids() { + println!("Benchmark ids (each manually timed — a single observation, not run under --list):"); + for id in all_ids() { + println!(" {id}"); + } +} + +/// Single-observation timings for one transition path. `None` = the id was filtered out, or (for +/// `fold` on genesis) not applicable — genesis is an accumulator passthrough. +struct PathTimings { + name: &'static str, + proof_size: usize, + prove_poseidon: Option, + prove_blake2b: Option, + verify_full: Option, + verify_kzg_opening: Option, + fold: Option, +} + +/// Times one operation once (a single observation) when `run` is true, else returns `None`. Prints a +/// progress line to stderr **before** starting the clock (so logging is never counted in the timing) and +/// `black_box`-es the result so the work is not optimised away. +fn timed(label: &str, run: bool, operation: impl FnOnce() -> T) -> Option { + if !run { + return None; + } + eprintln!(" {label} …"); + let start = Instant::now(); + black_box(operation()); + Some(start.elapsed()) +} + +/// Measures every selected operation for one path — prove (both transcripts), verify (full + isolated +/// KZG opening), and the off-circuit accumulator fold — each a single observation, all delegating to +/// the façade. Genesis has no fold (accumulator passthrough). Excludes the untimed fixture preparation. +fn measure_path( + filter: Option<&str>, + is_genesis: bool, + name: &'static str, + env: &IvcBenchEnv, + step: &PreparedStep, +) -> PathTimings { + // Each id doubles as the stderr progress label, so the log line matches the `--list` ids exactly. + let prove_poseidon = { + let id = format!("ivc/{name}/prove/poseidon"); + timed(&id, selected(filter, &id), || { + env.prove_poseidon(step).expect("poseidon prove should succeed") + }) + }; + let prove_blake2b = { + let id = format!("ivc/{name}/prove/blake2b"); + timed(&id, selected(filter, &id), || { + env.prove_blake2b(step).expect("blake2b prove should succeed") + }) + }; + let verify_full = { + let id = format!("ivc/{name}/verify/full"); + timed(&id, selected(filter, &id), || { + env.verify_full(step).expect("full verification should succeed") + }) + }; + let verify_kzg_opening = { + let id = format!("ivc/{name}/verify/kzg_opening"); + timed(&id, selected(filter, &id), || { + env.verify_kzg_opening(step).expect("kzg opening should succeed") + }) + }; + let fold = if is_genesis { + None + } else { + let id = format!("ivc/{name}/fold"); + timed(&id, selected(filter, &id), || { + env.fold_accumulators(step).expect("non-genesis fold present") + }) + }; + + PathTimings { + name, + proof_size: step.proof_size(), + prove_poseidon, + prove_blake2b, + verify_full, + verify_kzg_opening, + fold, + } +} + +/// Formats one cell: seconds for the multi-second prover, milliseconds otherwise; `—` when unmeasured. +fn cell(duration: Option) -> String { + match duration { + None => "—".to_string(), + Some(d) if d.as_secs_f64() >= 1.0 => format!("{:.3} s", d.as_secs_f64()), + Some(d) => format!("{:.3} ms", d.as_secs_f64() * 1000.0), + } +} + +/// Prints the consolidated report: the one-off setup (cold environment build) then a per-path table. +/// Every number is a single observation — a coarse baseline for the SNARK book, not a statistic. +fn print_report(setup: Duration, rows: &[PathTimings]) { + println!( + "\nIVC recursive circuit (degree 19) — single observation per cell; one keygen shared across paths" + ); + println!( + "setup (cold environment: unsafe SRS + certificate/IVC keygen + fixed bases + verifier/global \ + init): {:.3} s\n", + setup.as_secs_f64() + ); + println!( + "{:<12} {:>13} {:>13} {:>12} {:>12} {:>11} {:>9}", + "path", "prove/pos", "prove/blake", "verify/full", "verify/kzg", "fold", "proof" + ); + for row in rows { + println!( + "{:<12} {:>13} {:>13} {:>12} {:>12} {:>11} {:>9}", + row.name, + cell(row.prove_poseidon), + cell(row.prove_blake2b), + cell(row.verify_full), + cell(row.verify_kzg_opening), + cell(row.fold), + format!("{} B", row.proof_size), + ); + } + println!( + "\nproof = committed verification proof size (the freshly generated Poseidon/Blake2b proofs share \ + this fixed format/size)." + ); +} + +fn run(filter: Option) { + let filter = filter.as_deref(); + // Per-path benches share one recursive-keygen environment; the setup benches build their own caches. + // A no-filter run therefore does the shared keygen AND the setup benches' own keygen(s). + if any_path_bench_selected(filter) { + run_path_benches(filter); + } + if any_setup_bench_selected(filter) { + run_setup_benches(filter); + } +} + +/// Per-path prove/verify/fold, sharing one environment (one recursive keygen), printed as a table. +fn run_path_benches(filter: Option<&str>) { + // One shared environment (a full recursive keygen) for the whole run; `cache` stays in scope (and + // thus on disk) for the entire block. The keygen is timed once as the one-off `setup` cost. + let cache = TempDir::new().expect("bench cache tempdir"); + eprintln!("building shared environment (recursive keygen); this dominates the runtime …"); + let setup_start = Instant::now(); + let env = IvcBenchEnv::new(cache.path()).expect("IVC bench environment should build"); + let setup = setup_start.elapsed(); + eprintln!( + "environment ready in {:.1}s; measuring paths …", + setup.as_secs_f64() + ); + + let rows: Vec = PATHS + .iter() + .filter(|(path, name)| path_selected(filter, *path, name)) + .map(|(path, name)| { + eprintln!("[{name}] preparing fixture (untimed) …"); + let step = env.prepare_step(*path).expect("fixture preparation should succeed"); + measure_path( + filter, + matches!(path, TransitionPath::Genesis), + name, + &env, + &step, + ) + }) + .collect(); + + print_report(setup, &rows); +} + +/// Times one operation once (a single observation), `black_box`-ing the result. +fn observe(operation: impl FnOnce() -> T) -> Duration { + let start = Instant::now(); + black_box(operation()); + start.elapsed() +} + +/// Setup cold/warm measurements, each on its own throwaway cache: the cold call populates the +/// cache, the warm call reads it back — both through the production providers. Independent of the shared +/// per-path environment. +fn run_setup_benches(filter: Option<&str>) { + let srs = selected(filter, "ivc/setup/srs").then(|| { + eprintln!("measuring setup: ivc/setup/srs (cold then warm) …"); + let dir = TempDir::new().expect("bench cache tempdir"); + let cold = observe(|| { + IvcBenchEnv::measure_srs_cold_start(dir.path()).expect("srs cold-start should succeed") + }); + let warm = observe(|| { + IvcBenchEnv::measure_srs_warm_start(dir.path()).expect("srs warm-start should succeed") + }); + (cold, warm) + }); + let keys = selected(filter, "ivc/setup/keys").then(|| { + eprintln!( + "measuring setup: ivc/setup/keys (cold then warm) — the cold run does a full recursive \ + keygen (minutes, GB-scale RAM) …" + ); + let dir = TempDir::new().expect("bench cache tempdir"); + let srs = IvcBenchEnv::measure_srs_cold_start(dir.path()) + .expect("srs seed for the keys measurement should succeed"); + let cold = observe(|| { + IvcBenchEnv::measure_keys(dir.path(), &srs).expect("keys cold derive should succeed") + }); + let warm = observe(|| { + IvcBenchEnv::measure_keys(dir.path(), &srs).expect("keys warm load should succeed") + }); + (cold, warm) + }); + print_setup_report(srs, keys); +} + +/// Prints the setup cold-vs-warm table: cold = generate/derive from scratch, warm = load from the +/// on-disk cache. Single observation each. +fn print_setup_report(srs: Option<(Duration, Duration)>, keys: Option<(Duration, Duration)>) { + if srs.is_none() && keys.is_none() { + return; + } + println!("\nsetup cache: cold vs warm (single observation each)"); + println!("{:<18} {:>13} {:>13}", "stage", "cold", "warm"); + if let Some((cold, warm)) = srs { + println!( + "{:<18} {:>13} {:>13}", + "srs", + cell(Some(cold)), + cell(Some(warm)) + ); + } + if let Some((cold, warm)) = keys { + println!( + "{:<18} {:>13} {:>13}", + "keys (cert+ivc)", + cell(Some(cold)), + cell(Some(warm)) + ); + } + println!( + "cold = generate/derive from scratch; warm = load from the on-disk cache (both via the production \ + providers)." + ); +} + +fn main() { + match cli::parse(std::env::args().skip(1)) { + Err(message) => die(&message), + Ok(BenchCli::Help) => print_usage(), + Ok(BenchCli::Version) => println!("{}", env!("CARGO_PKG_VERSION")), + Ok(BenchCli::List) => print_all_ids(), + Ok(BenchCli::Run(filter)) => run(filter), + } +} diff --git a/mithril-stm/src/circuits/halo2/bench_helpers.rs b/mithril-stm/src/circuits/halo2/bench/helpers.rs similarity index 100% rename from mithril-stm/src/circuits/halo2/bench_helpers.rs rename to mithril-stm/src/circuits/halo2/bench/helpers.rs diff --git a/mithril-stm/src/circuits/halo2/bench/mod.rs b/mithril-stm/src/circuits/halo2/bench/mod.rs new file mode 100644 index 00000000000..a412cfadce3 --- /dev/null +++ b/mithril-stm/src/circuits/halo2/bench/mod.rs @@ -0,0 +1,8 @@ +//! Benchmark-only façade for the non-recursive certificate circuit. +//! +//! Compiled under `test` or the `benchmark-internals` feature. Kept in the library rather than `benches/` +//! because `helpers` delegates to `pub(crate)`/private production code; the harnesses in +//! `benches/halo2_snark.rs` and `benches/halo2_prover_modes.rs` consume this module through the public API. + +/// Façade over the production non-recursive proof-system code. +pub mod helpers; diff --git a/mithril-stm/src/circuits/halo2/mod.rs b/mithril-stm/src/circuits/halo2/mod.rs index bb5e47db37e..3a4d1ec20c3 100644 --- a/mithril-stm/src/circuits/halo2/mod.rs +++ b/mithril-stm/src/circuits/halo2/mod.rs @@ -30,7 +30,7 @@ pub(crate) mod types; pub(crate) mod witness; #[cfg(any(test, feature = "benchmark-internals"))] -pub mod bench_helpers; +pub mod bench; #[cfg(test)] pub(crate) mod tests; diff --git a/mithril-stm/src/circuits/halo2_ivc/bench/cli.rs b/mithril-stm/src/circuits/halo2_ivc/bench/cli.rs new file mode 100644 index 00000000000..755da48ad4e --- /dev/null +++ b/mithril-stm/src/circuits/halo2_ivc/bench/cli.rs @@ -0,0 +1,153 @@ +//! Fail-closed CLI parsing for the `ivc_halo2_snark` benchmark. +//! +//! Split out (behind `benchmark-internals`) and kept **pure** — no printing, no `process::exit` — so the +//! fail-closed contract can be unit-tested: no option may consume a following token and turn a control +//! flag or filter into an accidental `Run(None)`, which would trigger a multi-minute keygen + prove run. +//! The bench binary turns the returned `BenchCli` into printing / exit. + +/// Valueless flags tolerated (e.g. `cargo bench` passes `--bench`). Every other `-`/`--` token is +/// rejected — nothing consumes a following value, so no token can be swallowed. +const FLAG_OPTS: &[&str] = &["--verbose", "--quiet", "--nocapture", "--bench"]; +/// Regex metacharacters rejected in the positional filter (it is matched literally). +const METACHARS: &[char] = &['.', '*', '+', '?', '[', ']', '(', ')', '{', '}', '|', '^', '$', '\\']; + +/// Validated CLI outcome, produced before the bench binary builds anything. +#[derive(Debug, PartialEq, Eq)] +pub enum BenchCli { + /// Run the selected benchmarks (optional single literal id/prefix filter). + Run(Option), + /// `--list`: print ids and return (no environment build). + List, + /// `--help` / `-h`: print usage and return. + Help, + /// `--version` / `-V`: print the crate version and return. + Version, +} + +/// Parse benchmark CLI arguments (already skipping `argv[0]`). Fail-closed: any unsupported option, a regex +/// metacharacter in the filter, or more than one positional returns `Err`. No option consumes a following +/// token, so a control flag or filter can never be swallowed into `Run(None)`. +pub fn parse(args: impl Iterator) -> Result { + let mut filter: Option = None; + let mut list = false; + let mut help = false; + let mut version = false; + + for token in args { + match token.as_str() { + "--exact" => { + return Err("--exact is not supported; pass a literal id or prefix".to_string()); + } + "--test" | "--profile-time" => { + return Err(format!( + "{token} is not supported: it would force a full recursive keygen for the shared \ + environment. Use the façade #[ignore] smoke test for a once-through run." + )); + } + "--list" => list = true, + "--help" | "-h" => help = true, + "--version" | "-V" => version = true, + flag if FLAG_OPTS.contains(&flag) => {} + other if other.starts_with('-') => { + return Err(format!( + "unsupported option {other}; run with --list to see the benchmark ids" + )); + } + positional => { + if filter.is_some() { + return Err("only one literal filter is supported".to_string()); + } + if positional.contains(METACHARS) { + return Err( + "regex filters are not supported; pass a literal id or prefix, e.g. \ + ivc/same_epoch/prove" + .to_string(), + ); + } + filter = Some(positional.to_string()); + } + } + } + + // `--help`/`--version` take precedence and never build anything. + if help { + return Ok(BenchCli::Help); + } + if version { + return Ok(BenchCli::Version); + } + if list { + return Ok(BenchCli::List); + } + Ok(BenchCli::Run(filter)) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn parse_args(args: &[&str]) -> Result { + parse(args.iter().map(|s| s.to_string())) + } + + #[test] + fn no_args_runs_everything() { + assert_eq!(parse_args(&[]), Ok(BenchCli::Run(None))); + } + + #[test] + fn a_single_literal_filter_is_kept() { + assert_eq!( + parse_args(&["ivc/same_epoch/prove"]), + Ok(BenchCli::Run(Some("ivc/same_epoch/prove".to_string()))) + ); + } + + #[test] + fn control_flags_map_to_their_variants() { + assert_eq!(parse_args(&["--list"]), Ok(BenchCli::List)); + assert_eq!(parse_args(&["--help"]), Ok(BenchCli::Help)); + assert_eq!(parse_args(&["-h"]), Ok(BenchCli::Help)); + assert_eq!(parse_args(&["--version"]), Ok(BenchCli::Version)); + assert_eq!(parse_args(&["-V"]), Ok(BenchCli::Version)); + } + + #[test] + fn tolerated_valueless_flags_do_not_disturb_parsing() { + assert_eq!(parse_args(&["--bench", "--list"]), Ok(BenchCli::List)); + assert_eq!( + parse_args(&["--nocapture", "ivc/genesis"]), + Ok(BenchCli::Run(Some("ivc/genesis".to_string()))) + ); + } + + // The regression the fail-closed fix closes: value-taking options must be rejected, never swallow the + // following token, and never fall through to `Run(None)` (which would trigger the full keygen+prove run). + #[test] + fn value_taking_options_are_rejected_not_swallowed() { + assert!(parse_args(&["--sample-size", "10"]).is_err()); + assert!(parse_args(&["--sample-size", "--list"]).is_err()); + assert!(parse_args(&["--color", "always"]).is_err()); + assert!(parse_args(&["--save-baseline", "ivc"]).is_err()); + } + + #[test] + fn unsupported_and_malformed_inputs_are_rejected() { + assert!(parse_args(&["--bogus"]).is_err()); + assert!(parse_args(&["--exact"]).is_err()); + assert!(parse_args(&["--test"]).is_err()); + assert!(parse_args(&["--profile-time", "30"]).is_err()); + assert!(parse_args(&["ivc/.*verify"]).is_err()); + assert!(parse_args(&["ivc/genesis", "ivc/same_epoch"]).is_err()); + // `--ignored` is not a supported option (no benches are ignored); it must be rejected, both on + // its own and alongside a valid control flag — never fall through to a run. + assert!(parse_args(&["--ignored"]).is_err()); + assert!(parse_args(&["--list", "--ignored"]).is_err()); + } + + #[test] + fn help_and_version_take_precedence_over_control_flags() { + assert_eq!(parse_args(&["--list", "--help"]), Ok(BenchCli::Help)); + assert_eq!(parse_args(&["--list", "--version"]), Ok(BenchCli::Version)); + } +} diff --git a/mithril-stm/src/circuits/halo2_ivc/bench/helpers.rs b/mithril-stm/src/circuits/halo2_ivc/bench/helpers.rs new file mode 100644 index 00000000000..aaae3d4d80f --- /dev/null +++ b/mithril-stm/src/circuits/halo2_ivc/bench/helpers.rs @@ -0,0 +1,656 @@ +//! Benchmark façade for the recursive IVC circuit. +//! +//! Exposes `IvcBenchEnv`, a thin `benchmark-internals`-gated wrapper that delegates to the +//! production IVC proof-system code so the benchmarks measure the same code paths run in +//! production: setup (`IvcSnarkProverSetup::load`), preparation (`IvcProverInput`), recursive +//! proving (`IvcProof::prove_with_transcript`), verification (`IvcProof::verify` and the KZG +//! opening), and off-circuit accumulator folding. +//! +//! Fixtures are the committed golden assets plus the additive genesis fixture, so no proving +//! witness is fabricated here. Each prepared fixture is validated once (outside the timed loops). +//! +//! Note on the SRS: the benchmarks use a deterministic *unsafe* SRS (production instead downloads a +//! trusted SRS). Both the SRS and the circuit keys are measured as a **cold start** (generate / derive) +//! and a **warm start** (load from the on-disk cache); the recursive circuit keys are the dominant +//! setup cost. + +use std::path::Path; + +use anyhow::{anyhow, ensure}; +use group::Group; +use midnight_circuits::{ + hash::poseidon::PoseidonState, + types::Instantiable, + verifier::{Accumulator, AssignedAccumulator, BlstrsEmulation}, +}; +use midnight_curves::{Bls12, G1Projective}; +use midnight_proofs::{ + plonk::prepare, + poly::kzg::{KZGCommitmentScheme, params::ParamsKZG}, + transcript::{Blake2b256, CircuitTranscript, Transcript}, +}; +use rand_core::OsRng; + +use crate::{ + AggregateVerificationKeyForSnark, MithrilMembershipDigest, Parameters, SnarkProof, StmResult, + circuits::{ + halo2::{circuit::StmCertificateCircuit, types::CircuitBase}, + halo2_ivc::{ + PREIMAGE_SIZE, RECURSIVE_CIRCUIT_DEGREE, + circuit::IvcCircuitData, + embedded_assets::{ + FollowingCertificateInEpochAsset, NextEpochStepOutputAsset, + load_embedded_following_certificate_in_epoch_asset, + load_embedded_genesis_benchmark_fixture, load_embedded_genesis_step_output_asset, + load_embedded_next_epoch_step_output_asset, + load_embedded_recursive_chain_state_asset, + }, + keys::RecursiveCircuitKeyGenerator, + state::{Global, State}, + types::{CertificateProofBytes, IvcProofBytes, ProtocolMessagePreimage}, + }, + key_provider::KeyProvider, + trusted_setup::TrustedSetupProvider, + }, + proof_system::ivc_halo2_snark::{ + IvcProverInput, IvcSnarkProverSetup, proof::IvcProof, rolling_state::IvcRollingState, + verifier_setup::IvcVerifierSetup, + }, +}; + +/// Deterministic certificate parameters matching the committed golden assets +/// (`tests::common::generators::setup` + `tests::common::CERTIFICATE_CIRCUIT_DEGREE`). If these +/// drift from the asset generators, `IvcSnarkProverSetup::load` derives a certificate verifying +/// key that does not match the committed certificate proofs, and fixture preparation fails its +/// validity assert — so a mismatch surfaces loudly rather than producing bogus numbers. +const QUORUM_SIZE: u64 = 2; +const NUMBER_OF_LOTTERIES: u64 = QUORUM_SIZE * 10; +const SIGNER_COUNT: usize = 3000; +const TOTAL_STAKE: u64 = 1_000_000; +const PHI_F: f64 = 0.2; + +/// A transition path benchmarked across prove / verify / accumulator-fold. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TransitionPath { + /// Genesis base case: no certificate, trivial accumulator passthrough. + Genesis, + /// Same-epoch step: certificate extends the current epoch. + SameEpoch, + /// Next-epoch step: certificate starts a new epoch. + NextEpoch, +} + +/// Everything one path needs across the measured operations, built untimed and validated once. +pub struct PreparedStep { + path: TransitionPath, + // Prover inputs: `create_proof` runs over this prebuilt circuit data and public inputs. + circuit_data: IvcCircuitData, + public_inputs: Vec, + // Verifier inputs: the committed step proof and the state/accumulator/message it is checked against. + proof_bytes: IvcProofBytes, + proof_state: State, + proof_accumulator: Accumulator, + message: Vec, + // Accumulator-fold inputs: the two already-collapsed accumulators plus the rolling one. + // `None` for genesis (passthrough — no certificate or previous-IVC accumulator to fold). + fold: Option, +} + +/// Pre-collapsed accumulators for the fold measurement (folding only, no proof verification). +struct FoldInputs { + rolling_accumulator: Accumulator, + certificate_collapsed_accumulator: Accumulator, + previous_ivc_proof_collapsed_accumulator: Accumulator, +} + +/// A committed same-/next-epoch step output in the common shape a certificate fixture needs. +/// +/// The same-epoch and next-epoch assets are distinct types with identical fields; the `From` impls +/// below normalise both into this shape so `prepare_certificate_step` can treat them uniformly. +struct CertificateStepAsset { + certificate_proof: CertificateProofBytes, + next_state: State, + next_accumulator: Accumulator, + ivc_proof: IvcProofBytes, + message: [u8; 32], + message_preimage: [u8; PREIMAGE_SIZE], + aggregate_verification_key_merkle_root: [u8; 32], +} + +impl From for CertificateStepAsset { + fn from(step: FollowingCertificateInEpochAsset) -> Self { + Self { + certificate_proof: step.certificate_proof, + next_state: step.next_state, + next_accumulator: step.next_accumulator, + ivc_proof: step.ivc_proof, + message: step.message, + message_preimage: step.message_preimage, + aggregate_verification_key_merkle_root: step.aggregate_verification_key_merkle_root, + } + } +} + +impl From for CertificateStepAsset { + fn from(step: NextEpochStepOutputAsset) -> Self { + Self { + certificate_proof: step.certificate_proof, + next_state: step.next_state, + next_accumulator: step.next_accumulator, + ivc_proof: step.ivc_proof, + message: step.message, + message_preimage: step.message_preimage, + aggregate_verification_key_merkle_root: step.aggregate_verification_key_merkle_root, + } + } +} + +impl PreparedStep { + /// Returns the transition path this fixture represents. + pub fn path(&self) -> TransitionPath { + self.path + } + + /// Byte length of the committed step proof used for verification. + pub fn proof_size(&self) -> usize { + self.proof_bytes.as_bytes().len() + } +} + +/// Deterministic certificate parameters used to derive the recursive setup. +fn benchmark_parameters() -> Parameters { + Parameters { + k: QUORUM_SIZE, + m: NUMBER_OF_LOTTERIES, + phi_f: PHI_F, + } +} + +/// Merkle-tree depth for the committed signer set. +fn merkle_tree_depth() -> u32 { + SIGNER_COUNT.next_power_of_two().trailing_zeros() +} + +/// Builds the unsafe-SRS trusted-setup provider rooted at `cache_dir`. +/// +/// `with_unsafe_srs` generates and stores the SRS on construction (there is no generate-on-miss +/// path), so constructing this provider is the SRS-generation cost. +fn trusted_setup_provider(cache_dir: &Path) -> TrustedSetupProvider { + TrustedSetupProvider::with_unsafe_srs(cache_dir, RECURSIVE_CIRCUIT_DEGREE + 1) +} + +/// Builds the recursive key provider (wrapping the certificate key provider) rooted at `cache_dir`. +fn recursive_key_provider( + cache_dir: &Path, +) -> StmResult> { + let certificate_provider = KeyProvider::new( + cache_dir.join("certificate"), + "non-recursive", + &[], + StmCertificateCircuit::try_new(&benchmark_parameters(), merkle_tree_depth())?, + ); + Ok(KeyProvider::new( + cache_dir.join("recursive"), + "recursive", + &[], + RecursiveCircuitKeyGenerator::new(certificate_provider), + )) +} + +/// Shared benchmark environment delegating to the production IVC proof-system code. +pub struct IvcBenchEnv { + setup: IvcSnarkProverSetup, + verifier_setup: IvcVerifierSetup, + global: Global, +} + +impl IvcBenchEnv { + /// Builds the shared benchmark environment: the full IVC setup (SRS + verifying/proving keys + + /// fixed bases), the verifier setup derived from the same unsafe SRS, and the `Global` built + /// from the committed genesis fixture. Rooted at `cache_dir` (a caller-managed directory). + pub fn new(cache_dir: &Path) -> StmResult { + let setup = IvcSnarkProverSetup::load( + &trusted_setup_provider(cache_dir), + &recursive_key_provider(cache_dir)?, + )?; + let verifier_setup = IvcVerifierSetup::from_ivc_setup_with_srs(&setup); + + let genesis_fixture = load_embedded_genesis_benchmark_fixture()?; + let global = Global::new( + genesis_fixture.genesis_message_hash(), + genesis_fixture.genesis_verification_key, + &setup.certificate_verifying_key, + &setup.ivc_verifying_key, + ); + + Ok(Self { + setup, + verifier_setup, + global, + }) + } + + /// Prepares the fixture for `path`. Untimed; the resulting proof is validated once. + pub fn prepare_step(&self, path: TransitionPath) -> StmResult { + let prepared = match path { + TransitionPath::Genesis => self.prepare_genesis_step()?, + TransitionPath::SameEpoch => self.prepare_certificate_step(path)?, + TransitionPath::NextEpoch => self.prepare_certificate_step(path)?, + }; + // Validate once, outside any timed loop: the committed step proof must verify. + IvcProof::::new( + prepared.proof_bytes.clone(), + prepared.proof_state.clone(), + prepared.proof_accumulator.clone(), + ) + .verify(&prepared.message, &self.global, &self.verifier_setup)?; + Ok(prepared) + } + + /// Builds the genesis base-case fixture from the additive genesis fixture and the committed + /// genesis step output (used for verification). + fn prepare_genesis_step(&self) -> StmResult { + let genesis_fixture = load_embedded_genesis_benchmark_fixture()?; + let genesis_output = load_embedded_genesis_step_output_asset()?; + let preimage = ProtocolMessagePreimage(genesis_fixture.genesis_protocol_message_preimage); + + let combined_fixed_base_names: Vec = + self.setup.combined_fixed_bases.keys().cloned().collect(); + let rolling_state = IvcRollingState::genesis( + genesis_fixture.genesis_signature, + &combined_fixed_base_names, + ); + let prover_input = + IvcProverInput::prepare_genesis(&rolling_state, &preimage, &self.global)?; + + let circuit_data = IvcCircuitData::try_new( + self.global.clone(), + rolling_state.state().clone(), + prover_input.witness, + CertificateProofBytes::empty(), + rolling_state.ivc_proof().clone(), + rolling_state.accumulator().clone(), + &self.setup.certificate_verifying_key, + &self.setup.ivc_verifying_key, + )?; + let public_inputs = + self.public_inputs_for(&prover_input.next_state, &prover_input.next_accumulator); + + // Untimed equivalence: the freshly prepared proving relation must be the same transition + // as the committed genesis step output that verification measures. + ensure!( + prover_input.next_state == genesis_output.next_state, + "prepared genesis next_state does not match the committed genesis step output" + ); + ensure!( + AssignedAccumulator::as_public_input(&prover_input.next_accumulator) + == AssignedAccumulator::as_public_input(&genesis_output.next_accumulator), + "prepared genesis next_accumulator does not match the committed genesis step output" + ); + + Ok(PreparedStep { + path: TransitionPath::Genesis, + circuit_data, + public_inputs, + proof_bytes: genesis_output.ivc_proof, + proof_state: genesis_output.next_state, + proof_accumulator: genesis_output.next_accumulator, + message: genesis_fixture.genesis_message.to_vec(), + fold: None, + }) + } + + /// Builds a same-epoch or next-epoch fixture from the committed recursive chain state and the + /// matching step output asset, mirroring the production `IvcProverInput::prepare` flow. + fn prepare_certificate_step(&self, path: TransitionPath) -> StmResult { + let chain_state = load_embedded_recursive_chain_state_asset()?; + let asset: CertificateStepAsset = match path { + TransitionPath::SameEpoch => { + load_embedded_following_certificate_in_epoch_asset()?.into() + } + TransitionPath::NextEpoch => load_embedded_next_epoch_step_output_asset()?.into(), + TransitionPath::Genesis => unreachable!("genesis handled by prepare_genesis_step"), + }; + + let snark_proof: SnarkProof = SnarkProof::new( + asset.certificate_proof.clone().into_vec(), + benchmark_parameters(), + merkle_tree_depth(), + ); + let aggregate_verification_key = + aggregate_verification_key_for_root(&asset.aggregate_verification_key_merkle_root)?; + let preimage = ProtocolMessagePreimage(asset.message_preimage); + let rolling_state = IvcRollingState::new( + chain_state.state, + chain_state.ivc_proof, + chain_state.accumulator, + chain_state.genesis_signature, + ); + + let prover_input = IvcProverInput::prepare( + &snark_proof, + &asset.message, + &aggregate_verification_key, + &self.global, + &preimage, + &rolling_state, + &self.setup, + )?; + + let circuit_data = IvcCircuitData::try_new( + self.global.clone(), + rolling_state.state().clone(), + prover_input.witness, + asset.certificate_proof, + rolling_state.ivc_proof().clone(), + rolling_state.accumulator().clone(), + &self.setup.certificate_verifying_key, + &self.setup.ivc_verifying_key, + )?; + let public_inputs = + self.public_inputs_for(&prover_input.next_state, &prover_input.next_accumulator); + + let fold_inputs = self.build_certificate_fold_inputs( + &snark_proof, + &asset.message, + &aggregate_verification_key, + &rolling_state, + )?; + + Self::ensure_certificate_prepared_matches_committed( + &prover_input.next_state, + &prover_input.next_accumulator, + &asset.next_state, + &asset.next_accumulator, + &fold_inputs, + )?; + + Ok(PreparedStep { + path, + circuit_data, + public_inputs, + proof_bytes: asset.ivc_proof, + proof_state: asset.next_state, + proof_accumulator: asset.next_accumulator, + message: asset.message.to_vec(), + fold: Some(fold_inputs), + }) + } + + /// Reconstructs the accumulator-fold inputs: the certificate and previous-IVC-proof KZG + /// openings collapsed into accumulators, plus the rolling accumulator carried in the state. + fn build_certificate_fold_inputs( + &self, + snark_proof: &SnarkProof, + message: &[u8], + aggregate_verification_key: &AggregateVerificationKeyForSnark, + rolling_state: &IvcRollingState, + ) -> StmResult { + let certificate_dual_msm = snark_proof.prepare_and_check( + message, + aggregate_verification_key, + &self.setup.certificate_verifying_key, + &self.setup.srs.verifier_params(), + )?; + let certificate_collapsed_accumulator = + self.setup.certificate_collapsed_accumulator(certificate_dual_msm)?; + let previous_ivc_proof_collapsed_accumulator = + self.setup.previous_ivc_proof_collapsed_accumulator( + rolling_state.ivc_proof().as_bytes(), + &rolling_state.previous_ivc_proof_public_inputs(&self.global), + )?; + Ok(FoldInputs { + rolling_accumulator: rolling_state.accumulator().clone(), + certificate_collapsed_accumulator, + previous_ivc_proof_collapsed_accumulator, + }) + } + + /// Untimed equivalence checks guarding fixture assembly: the freshly prepared transition must + /// match the committed step output that verification measures, and folding the reconstructed + /// accumulator inputs must reproduce the production-prepared accumulator — otherwise the façade + /// would benchmark one transition while verifying/folding another. + fn ensure_certificate_prepared_matches_committed( + prepared_next_state: &State, + prepared_next_accumulator: &Accumulator, + committed_next_state: &State, + committed_next_accumulator: &Accumulator, + fold_inputs: &FoldInputs, + ) -> StmResult<()> { + ensure!( + prepared_next_state == committed_next_state, + "prepared next_state does not match the committed step asset" + ); + ensure!( + AssignedAccumulator::as_public_input(prepared_next_accumulator) + == AssignedAccumulator::as_public_input(committed_next_accumulator), + "prepared next_accumulator does not match the committed step asset" + ); + let mut reproduced_fold = Accumulator::accumulate(&[ + fold_inputs.rolling_accumulator.clone(), + fold_inputs.certificate_collapsed_accumulator.clone(), + fold_inputs.previous_ivc_proof_collapsed_accumulator.clone(), + ]); + reproduced_fold.collapse(); + ensure!( + AssignedAccumulator::as_public_input(&reproduced_fold) + == AssignedAccumulator::as_public_input(prepared_next_accumulator), + "reconstructed accumulator fold does not match the production-prepared accumulator" + ); + Ok(()) + } + + /// Concatenates the public inputs `[global | next_state | next_accumulator]`. + fn public_inputs_for( + &self, + next_state: &State, + next_accumulator: &Accumulator, + ) -> Vec { + [ + self.global.as_public_input(), + next_state.as_public_input(), + AssignedAccumulator::as_public_input(next_accumulator), + ] + .concat() + } + + /// Recursive prover with the Poseidon transcript: `create_proof` over prebuilt + /// circuit data. Excludes the inner certificate prover and the preparation step. + pub fn prove_poseidon(&self, prepared: &PreparedStep) -> StmResult> { + IvcProof::>::prove_with_transcript( + &self.setup.srs, + &self.setup.ivc_proving_key, + &prepared.circuit_data, + &prepared.public_inputs, + &mut OsRng, + ) + } + + /// Recursive prover with the Blake2b transcript (on-chain / RISC0 verification path). + pub fn prove_blake2b(&self, prepared: &PreparedStep) -> StmResult> { + IvcProof::::prove_with_transcript( + &self.setup.srs, + &self.setup.ivc_proving_key, + &prepared.circuit_data, + &prepared.public_inputs, + &mut OsRng, + ) + } + + /// Full recursive verification: KZG opening plus the folded-accumulator pairing check. + pub fn verify_full(&self, prepared: &PreparedStep) -> StmResult<()> { + IvcProof::::new( + prepared.proof_bytes.clone(), + prepared.proof_state.clone(), + prepared.proof_accumulator.clone(), + ) + .verify(&prepared.message, &self.global, &self.verifier_setup) + } + + /// Recursive KZG-opening verification only. Bench-local replication of the opening + /// half of `IvcProof::verify` (prepare + `dual_msm.check`), kept here to avoid a production + /// refactor; the full-verification path above exercises the production code. + pub fn verify_kzg_opening(&self, prepared: &PreparedStep) -> StmResult<()> { + let public_inputs: Vec = [ + self.global.as_public_input(), + prepared.proof_state.as_public_input(), + AssignedAccumulator::as_public_input(&prepared.proof_accumulator), + ] + .concat(); + + let mut transcript = + CircuitTranscript::::init_from_bytes(prepared.proof_bytes.as_bytes()); + let dual_msm = + prepare::, CircuitTranscript>( + self.verifier_setup.ivc_verifying_key().verifying_key(), + &[&[G1Projective::identity()]], + &[&[&public_inputs]], + &mut transcript, + ) + .map_err(|_| anyhow!("bench: recursive KZG opening transcript preparation failed"))?; + transcript + .assert_empty() + .map_err(|_| anyhow!("bench: recursive KZG opening transcript not fully consumed"))?; + if !dual_msm.check(self.verifier_setup.verifier_params()) { + return Err(anyhow!("bench: recursive KZG opening check failed")); + } + Ok(()) + } + + /// Off-circuit accumulator fold: `accumulate` + `collapse` from the two already- + /// collapsed source accumulators and the rolling accumulator. Returns `None` for genesis + /// (passthrough — nothing to fold). + pub fn fold_accumulators( + &self, + prepared: &PreparedStep, + ) -> Option> { + let fold = prepared.fold.as_ref()?; + let mut accumulator = Accumulator::accumulate(&[ + fold.rolling_accumulator.clone(), + fold.certificate_collapsed_accumulator.clone(), + fold.previous_ivc_proof_collapsed_accumulator.clone(), + ]); + accumulator.collapse(); + Some(accumulator) + } + + /// Cold-start SRS: generates and stores the unsafe SRS at `cache_dir`, reads it back, and downsizes + /// it to `RECURSIVE_CIRCUIT_DEGREE` — the same downsized end state as production + /// `IvcSnarkProverSetup::load`. Paired with [`Self::measure_srs_warm_start`]. + pub fn measure_srs_cold_start(cache_dir: &Path) -> StmResult> { + let mut srs = trusted_setup_provider(cache_dir).get_trusted_setup_parameters()?; + srs.downsize(RECURSIVE_CIRCUIT_DEGREE); + Ok(srs) + } + + /// Warm-start SRS: reads an already-generated SRS from `cache_dir` through the production cached read + /// path (skips download + hash verification, only deserializes), then downsizes it exactly as + /// [`Self::measure_srs_cold_start`] and `IvcSnarkProverSetup::load` do — so cold and warm measure the + /// same downsized end state, with no network dependency. Precondition: a prior `measure_srs_cold_start` + /// (or any generation) has populated `cache_dir`. + pub fn measure_srs_warm_start(cache_dir: &Path) -> StmResult> { + let mut srs = + TrustedSetupProvider::new(cache_dir, "", "", std::time::Duration::from_secs(600)) + .get_trusted_setup_parameters()?; + srs.downsize(RECURSIVE_CIRCUIT_DEGREE); + Ok(srs) + } + + /// Certificate **and** recursive circuit keys from `cache_dir`, mirroring `IvcSnarkProverSetup::load`: + /// the certificate verifying key is accessed explicitly before the recursive key pair, so a + /// **cold-start** (empty cache) derives both layers and a **warm-start** (populated cache) loads both. + /// Without the explicit certificate access, only the cold path would touch the certificate cache (the + /// recursive keygen derives it via the wrapped provider), while the warm path would load the recursive + /// pair directly and skip it — making the two measure different work. The bench harness times the first + /// call (cold) and the second (warm), which then differ only in derive-vs-load. + pub fn measure_keys(cache_dir: &Path, srs: &ParamsKZG) -> StmResult<()> { + let provider = recursive_key_provider(cache_dir)?; + let _certificate_verifying_key = provider.generator().certificate_verifying_key(srs)?; + let _recursive_keys = provider.key_pair(srs)?; + Ok(()) + } +} + +/// Rebuilds the deterministic aggregate verification key from a committed 32-byte Merkle root and +/// the fixed total stake, matching the format the committed certificate proofs commit to. +fn aggregate_verification_key_for_root( + aggregate_verification_key_merkle_root: &[u8; 32], +) -> StmResult> { + let mut avk_bytes = [0u8; 40]; + avk_bytes[0..32].copy_from_slice(aggregate_verification_key_merkle_root); + avk_bytes[32..40].copy_from_slice(&TOTAL_STAKE.to_be_bytes()); + AggregateVerificationKeyForSnark::::from_bytes(&avk_bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + // Slow runtime smoke: builds the full IVC setup (recursive keygen) once and exercises every + // façade operation on all three transition paths, confirming fixture assembly, proving, + // verification, and folding all succeed. Opt-in — run with + // `cargo test -p mithril-stm --features future_snark,benchmark-internals -- --ignored` + // (the façade, and hence this test, is gated behind `benchmark-internals`). + #[test] + #[ignore = "slow: builds the full IVC setup via recursive keygen"] + fn facade_prepares_proves_and_verifies_all_paths() { + let cache_dir = std::env::temp_dir().join("mithril_ivc_bench_facade_smoke"); + println!("[smoke] building IVC setup (recursive keygen); this dominates the runtime..."); + let env = IvcBenchEnv::new(&cache_dir).expect("bench env should build"); + println!("[smoke] setup ready"); + + for path in [ + TransitionPath::Genesis, + TransitionPath::SameEpoch, + TransitionPath::NextEpoch, + ] { + println!("[smoke] {path:?}: preparing fixture (untimed equivalence checks)..."); + let prepared = env.prepare_step(path).expect("prepare should succeed"); + + println!("[smoke] {path:?}: proving (Poseidon)..."); + let poseidon_bytes = + env.prove_poseidon(&prepared).expect("poseidon prove should succeed"); + println!("[smoke] {path:?}: proving (Blake2b)..."); + let blake2b_bytes = env.prove_blake2b(&prepared).expect("blake2b prove should succeed"); + + // Verify the freshly generated proofs against the prepared public inputs, each with its + // matching transcript — not just the committed proof. + println!("[smoke] {path:?}: verifying generated Poseidon proof..."); + IvcProof::>::new( + IvcProofBytes::new(poseidon_bytes), + prepared.proof_state.clone(), + prepared.proof_accumulator.clone(), + ) + .verify(&prepared.message, &env.global, &env.verifier_setup) + .expect("generated Poseidon proof should verify"); + println!("[smoke] {path:?}: verifying generated Blake2b proof..."); + IvcProof::::new( + IvcProofBytes::new(blake2b_bytes), + prepared.proof_state.clone(), + prepared.proof_accumulator.clone(), + ) + .verify(&prepared.message, &env.global, &env.verifier_setup) + .expect("generated Blake2b proof should verify"); + + println!("[smoke] {path:?}: verifying committed proof (full + KZG opening)..."); + env.verify_full(&prepared).expect("full verification should succeed"); + env.verify_kzg_opening(&prepared).expect("kzg opening should succeed"); + + println!("[smoke] {path:?}: folding accumulators..."); + let folded = env.fold_accumulators(&prepared); + match path { + TransitionPath::Genesis => { + assert!(folded.is_none(), "genesis fold is a passthrough"); + } + TransitionPath::SameEpoch | TransitionPath::NextEpoch => { + assert!( + folded.is_some(), + "non-genesis fold should produce an accumulator" + ); + } + } + println!("[smoke] {path:?}: done"); + } + println!("[smoke] all paths passed"); + } +} diff --git a/mithril-stm/src/circuits/halo2_ivc/bench/mod.rs b/mithril-stm/src/circuits/halo2_ivc/bench/mod.rs new file mode 100644 index 00000000000..3caf15de83c --- /dev/null +++ b/mithril-stm/src/circuits/halo2_ivc/bench/mod.rs @@ -0,0 +1,13 @@ +//! Benchmark-only façade and CLI for the IVC recursive circuit. +//! +//! Compiled under `test` or the `benchmark-internals` feature: `cli` (the argument parser and its unit +//! tests) is available in both, while `helpers` (the façade) is `benchmark-internals`-only because it needs +//! the full setup. Kept in the library rather than `benches/` because `helpers` delegates to +//! `pub(crate)`/private production code (prover input, prover/verifier setup, embedded assets); the thin +//! harness in `benches/ivc_halo2_snark.rs` consumes this module through the public API. + +/// Fail-closed CLI parsing for the `ivc_halo2_snark` harness (also unit-tested under `cargo test`). +pub mod cli; +/// Façade over the production IVC proof-system code (feature-gated, needs the full setup). +#[cfg(feature = "benchmark-internals")] +pub mod helpers; diff --git a/mithril-stm/src/circuits/halo2_ivc/embedded_assets.rs b/mithril-stm/src/circuits/halo2_ivc/embedded_assets.rs new file mode 100644 index 00000000000..14ef1980745 --- /dev/null +++ b/mithril-stm/src/circuits/halo2_ivc/embedded_assets.rs @@ -0,0 +1,619 @@ +//! Read-only, embedded golden-asset layer for the recursive Halo2 IVC circuit. +//! +//! Holds the committed binary assets (`include_bytes!`), their typed representations, and the +//! byte decoders that load them. Gated for both `test` and `benchmark-internals` so the IVC +//! benchmarks can load the same fixtures the golden tests use, without pulling in the +//! asset-writing infrastructure — that stays test-only in +//! [`tests::common::asset_readers`](super::tests::common::asset_readers), which re-exports the +//! types and loaders defined here. + +use std::{ + collections::BTreeMap, + fs::File, + io::{BufReader, Cursor, Read}, + path::Path, +}; + +use anyhow::{Context, anyhow}; +use midnight_proofs::{ + poly::kzg::params::ParamsVerifierKZG, + utils::{SerdeFormat, helpers::ProcessedSerdeObject}, +}; +use midnight_zk_stdlib::MidnightVK; + +use crate::StmResult; +use crate::circuits::halo2::keys::NonRecursiveCircuitVerifyingKey; +use crate::circuits::halo2_ivc::keys::RecursiveCircuitVerifyingKey; +use crate::circuits::halo2_ivc::{ + Accumulator, EmulatedCurve, KZGCommitmentScheme, NativeField, PREIMAGE_SIZE, PairingEngine, + RecursiveEmulation, VerifyingKey, + circuit::IvcCircuitData, + io::Read as IvcRead, + state::State, + types::{ + CertificateProofBytes, EpochNumber, IvcProofBytes, MerkleTreeCommitment, MessageHash, + ProtocolParametersHash, StepCounter, + }, +}; +use crate::signature_scheme::{SchnorrVerificationKey, StandardSchnorrSignature}; + +/// Interprets 32 little-endian bytes as an integer and maps it to a Jubjub +/// base-field element. +/// +/// This intentionally uses `from_raw` instead of canonical decoding: SHA256 +/// digests and legacy asset/test bytes may be non-canonical, and `from_raw` +/// converts them into the congruent field element modulo the field order. +pub(crate) fn jubjub_base_from_raw_le_bytes(bytes: &[u8]) -> NativeField { + assert_eq!(bytes.len(), 32); + let mut limbs = [0u64; 4]; + for (limb, chunk) in limbs.iter_mut().zip(bytes.chunks_exact(8)) { + let mut le_bytes = [0u8; 8]; + le_bytes.copy_from_slice(chunk); + *limb = u64::from_le_bytes(le_bytes); + } + NativeField::from_raw(limbs) +} + +/// Stored recursive chain checkpoint used by the golden tests. +#[derive(Debug)] +pub(crate) struct RecursiveChainStateAsset { + /// Stored global public inputs for the recursive flow. + pub(crate) global_field_elements: Vec, + /// Stored recursive state checkpoint. + pub(crate) state: State, + /// Stored previous recursive proof bytes. + pub(crate) ivc_proof: IvcProofBytes, + /// Stored folded accumulator for the checkpoint. + pub(crate) accumulator: Accumulator, + /// Stored chain-specific Schnorr signature over the genesis state. + pub(crate) genesis_signature: StandardSchnorrSignature, +} + +/// Stored verifier-side context shared by the golden assets. +#[derive(Debug)] +pub(crate) struct VerificationContextAsset { + /// Shared global public inputs used by the committed assets. + pub(crate) global_field_elements: Vec, + /// Stored recursive verifying key. + pub(crate) recursive_verifying_key: RecursiveCircuitVerifyingKey, + /// Combined fixed bases needed to check recursive accumulators. + pub(crate) combined_fixed_bases: BTreeMap, + /// Shared verifier-side KZG parameters. + pub(crate) verifier_params: ParamsVerifierKZG, + /// Stored certificate verifying key, enabling MockProver tests to skip SRS generation. + pub(crate) certificate_verifying_key: NonRecursiveCircuitVerifyingKey, +} + +/// Stored data of the first certificate produced from `build_genesis_base_case_next_state()`. +/// Used to test `IvcProverInput::prepare` at the first real certificate step (where the rolling +/// state's `step_counter` is one, after the internal genesis IVC step has run). +#[derive(Debug)] +pub(crate) struct FirstCertificateInEpochAsset { + /// Certificate proof bytes (consumed by `prepare` via `SnarkProof`). + pub(crate) certificate_proof: CertificateProofBytes, + /// Expected next state after `prepare` advances by one step. + pub(crate) next_state: State, + /// SHA-256 hash that the certificate proof committed to. + pub(crate) message: [u8; 32], + /// Protocol-message preimage; `ProtocolMessagePreimage::current_epoch()` / `next_merkle_tree_commitment()` / `next_protocol_parameters()` decode the four + /// epoch fields. + pub(crate) message_preimage: [u8; PREIMAGE_SIZE], + /// Canonical encoding of the aggregate verification key merkle root the certificate + /// proof committed to. + pub(crate) aggregate_verification_key_merkle_root: [u8; 32], +} + +/// Stored output of the genesis base-case step. Carries no certificate; the message, +/// preimage, and aggregate-verification-key fields are zero-byte placeholders. +#[derive(Debug)] +pub(crate) struct GenesisStepOutputAsset { + /// Stored final recursive proof bytes for the genesis step. + pub(crate) ivc_proof: IvcProofBytes, + /// Stored folded accumulator after the genesis step. + pub(crate) next_accumulator: Accumulator, + /// Stored next recursive state. + pub(crate) next_state: State, + /// Empty: genesis carries no certificate proof. + pub(crate) certificate_proof: CertificateProofBytes, + /// Zero placeholder; no certificate exists at genesis. + pub(crate) message: [u8; 32], + /// Zero placeholder; no protocol-message preimage exists at genesis. + pub(crate) message_preimage: [u8; PREIMAGE_SIZE], + /// Zero placeholder; no aggregate verification key merkle root exists at genesis. + pub(crate) aggregate_verification_key_merkle_root: [u8; 32], +} + +/// Stored output of extending the recursive chain by one next-epoch step (the cert +/// is the first cert of a new epoch, transitioning the chain from epoch N to N+1). +#[derive(Debug)] +pub(crate) struct NextEpochStepOutputAsset { + /// Stored final recursive proof bytes for the next-epoch step. + pub(crate) ivc_proof: IvcProofBytes, + /// Stored folded accumulator after the next-epoch step. + pub(crate) next_accumulator: Accumulator, + /// Stored next recursive state. + pub(crate) next_state: State, + /// Stored certificate proof consumed by the next-epoch step. + pub(crate) certificate_proof: CertificateProofBytes, + /// SHA-256 hash that the certificate proof committed to. + pub(crate) message: [u8; 32], + /// Protocol-message preimage; `ProtocolMessagePreimage::current_epoch()` / `next_merkle_tree_commitment()` / `next_protocol_parameters()` decode the four + /// epoch fields. + pub(crate) message_preimage: [u8; PREIMAGE_SIZE], + /// Canonical encoding of the aggregate verification key merkle root the certificate + /// proof committed to. + pub(crate) aggregate_verification_key_merkle_root: [u8; 32], +} + +/// Stored output of extending the recursive chain by one same-epoch step (the cert +/// follows in the current epoch and does not change the epoch boundary). +#[derive(Debug)] +pub(crate) struct FollowingCertificateInEpochAsset { + /// Stored final recursive proof bytes for the same-epoch step. + pub(crate) ivc_proof: IvcProofBytes, + /// Stored folded accumulator after the same-epoch step. + pub(crate) next_accumulator: Accumulator, + /// Stored next recursive state. + pub(crate) next_state: State, + /// Stored certificate proof consumed by the same-epoch step. + pub(crate) certificate_proof: CertificateProofBytes, + /// SHA-256 hash that the certificate proof committed to. + pub(crate) message: [u8; 32], + /// Protocol-message preimage; `ProtocolMessagePreimage::current_epoch()` / `next_merkle_tree_commitment()` / `next_protocol_parameters()` decode the four + /// epoch fields. + pub(crate) message_preimage: [u8; PREIMAGE_SIZE], + /// Canonical encoding of the aggregate verification key merkle root the certificate + /// proof committed to. + pub(crate) aggregate_verification_key_merkle_root: [u8; 32], +} + +/// Deterministic genesis proving inputs for the IVC benchmarks. +/// +/// Unlike [`GenesisStepOutputAsset`] (which stores the genesis *output* proof with zero-placeholder +/// input fields), this additive fixture stores the genesis *inputs* the benchmark façade needs to +/// build a `Global` and run a genesis proving step: the raw genesis message bytes (the `msg` +/// argument to `IvcProof::verify`), the genesis Schnorr verification key, the trusted genesis +/// signature, and the genesis protocol-message preimage. It is produced deterministically from +/// `build_asset_generation_setup()` and is additive — no existing golden asset is affected. +#[derive(Debug)] +pub(crate) struct GenesisBenchmarkFixture { + /// Raw 32-byte genesis message, `Sha256(genesis_protocol_message_preimage)`; the `msg` + /// argument to `IvcProof::verify`. + pub(crate) genesis_message: [u8; 32], + /// Genesis Schnorr verification key used to build the `Global`. + pub(crate) genesis_verification_key: SchnorrVerificationKey, + /// Trusted genesis signature carried through the rolling state for in-circuit verification. + pub(crate) genesis_signature: StandardSchnorrSignature, + /// Genesis protocol-message preimage consumed by the genesis bootstrap step. + pub(crate) genesis_protocol_message_preimage: [u8; PREIMAGE_SIZE], +} + +impl GenesisBenchmarkFixture { + /// Derives the typed genesis message hash from the stored raw bytes. Equals + /// `AssetGenerationSetup::genesis_message`, since both apply `from_raw` to the same + /// `Sha256(preimage)` digest. + pub(crate) fn genesis_message_hash(&self) -> MessageHash { + MessageHash::from_field(jubjub_base_from_raw_le_bytes(&self.genesis_message)) + } +} + +const RECURSIVE_CHAIN_STATE_ASSET_BYTES: &[u8] = + include_bytes!("tests/assets/recursive_chain_state.bin"); +const VERIFICATION_CONTEXT_ASSET_BYTES: &[u8] = + include_bytes!("tests/assets/verification_context.bin"); +const NEXT_EPOCH_STEP_OUTPUT_ASSET_BYTES: &[u8] = + include_bytes!("tests/assets/recursive_step_output.bin"); +const GENESIS_STEP_OUTPUT_ASSET_BYTES: &[u8] = + include_bytes!("tests/assets/genesis_step_output.bin"); +const FOLLOWING_CERTIFICATE_IN_EPOCH_ASSET_BYTES: &[u8] = + include_bytes!("tests/assets/same_epoch_step_output.bin"); +const FIRST_CERTIFICATE_IN_EPOCH_ASSET_BYTES: &[u8] = + include_bytes!("tests/assets/first_step_cert.bin"); +const GENESIS_BENCHMARK_FIXTURE_ASSET_BYTES: &[u8] = + include_bytes!("tests/assets/genesis_benchmark_fixture.bin"); + +/// Opens a committed golden asset for buffered reading. +fn open_asset_file(path: &Path) -> StmResult> { + Ok(BufReader::new(File::open(path)?)) +} + +/// Reads one field element encoded as 32 little-endian bytes. +fn read_field_element(reader: &mut R) -> StmResult { + let mut bytes = [0u8; 32]; + reader.read_exact(&mut bytes)?; + Ok(jubjub_base_from_raw_le_bytes(&bytes)) +} + +/// Reads the seven public-input field elements that define a recursive state. +fn read_state_public_input(reader: &mut R) -> StmResult { + Ok(State::new( + StepCounter::from_field(read_field_element(reader)?), + MessageHash::from_field(read_field_element(reader)?), + MerkleTreeCommitment::from_field(read_field_element(reader)?), + MerkleTreeCommitment::from_field(read_field_element(reader)?), + ProtocolParametersHash::from_field(read_field_element(reader)?), + ProtocolParametersHash::from_field(read_field_element(reader)?), + EpochNumber::from_field(read_field_element(reader)?), + )) +} + +/// Reads a 64-byte `StandardSchnorrSignature` (response | challenge). +fn read_schnorr_signature(reader: &mut R) -> StmResult { + let mut bytes = [0u8; 64]; + reader.read_exact(&mut bytes)?; + StandardSchnorrSignature::from_bytes(&bytes) +} + +/// Reads exactly `N` bytes stored behind a 32-bit little-endian length prefix. Returns +/// an error if the prefix does not equal `N`. +fn read_fixed_length_prefixed( + reader: &mut R, + field_name: &str, +) -> StmResult<[u8; N]> { + let bytes = read_length_prefixed_proof(reader)?; + bytes + .try_into() + .map_err(|v: Vec| anyhow!("{field_name}: expected {N} bytes, got {} bytes", v.len())) +} + +/// Reads proof bytes stored behind a 32-bit little-endian length prefix. +fn read_length_prefixed_proof(reader: &mut R) -> StmResult> { + let mut len = [0u8; 4]; + reader.read_exact(&mut len)?; + let proof_len = u32::from_le_bytes(len) as usize; + + let mut proof = vec![0u8; proof_len]; + reader.read_exact(&mut proof)?; + + Ok(proof) +} + +/// Reads the named fixed-base map stored in the verification-context asset. +fn read_named_fixed_bases(reader: &mut R) -> StmResult> { + let mut count = [0u8; 4]; + reader.read_exact(&mut count)?; + let count = u32::from_le_bytes(count) as usize; + + let mut map = BTreeMap::new(); + for _ in 0..count { + let mut name_len = [0u8; 4]; + reader.read_exact(&mut name_len)?; + let name_len = u32::from_le_bytes(name_len) as usize; + + let mut name_bytes = vec![0u8; name_len]; + reader.read_exact(&mut name_bytes)?; + let name = String::from_utf8(name_bytes) + .map_err(|_| anyhow!("invalid UTF-8 key in verification-context fixed-base map"))?; + + let point = EmulatedCurve::read(reader, SerdeFormat::RawBytesUnchecked)?; + map.insert(name, point); + } + + Ok(map) +} + +/// Loads a recursive chain snapshot from the committed binary asset layout. +fn load_recursive_chain_state_asset_from_reader( + reader: &mut R, +) -> StmResult { + let global_field_elements = (0..5) + .map(|_| read_field_element(reader)) + .collect::, _>>()?; + let state = read_state_public_input(reader)?; + let ivc_proof = IvcProofBytes::new(read_length_prefixed_proof(reader)?); + let accumulator = + Accumulator::::read(reader, SerdeFormat::RawBytesUnchecked)?; + let genesis_signature = read_schnorr_signature(reader)?; + + Ok(RecursiveChainStateAsset { + global_field_elements, + state, + ivc_proof, + accumulator, + genesis_signature, + }) +} + +/// Loads the stored recursive chain snapshot from a committed asset file. +pub(crate) fn load_recursive_chain_state_asset(path: &Path) -> StmResult { + let mut reader = open_asset_file(path).with_context(|| { + format!( + "failed to open recursive chain state asset: {}", + path.display() + ) + })?; + load_recursive_chain_state_asset_from_reader(&mut reader).with_context(|| { + format!( + "failed to decode recursive chain state asset: {}", + path.display() + ) + }) +} + +/// Loads the embedded recursive chain snapshot compiled into the binary. +pub(crate) fn load_embedded_recursive_chain_state_asset() -> StmResult { + let mut reader = Cursor::new(RECURSIVE_CHAIN_STATE_ASSET_BYTES); + load_recursive_chain_state_asset_from_reader(&mut reader) + .context("failed to decode embedded recursive chain state asset") +} + +/// Loads a verifier-side context from the committed binary asset layout. +/// +/// Binary layout (v2): +/// `[global(5×32b) | recursive_vk(var) | combined_fixed_bases(var) | +/// verifier_params_len(4b LE) | verifier_params(var) | +/// certificate_vk_len(4b LE) | certificate_vk(var)]` +fn load_verification_context_asset_from_reader( + reader: &mut R, +) -> StmResult { + let global_field_elements = (0..5) + .map(|_| read_field_element(reader)) + .collect::, _>>()?; + let recursive_verifying_key = + VerifyingKey::>::read::<_, IvcCircuitData>( + reader, + SerdeFormat::RawBytesUnchecked, + (), + )?; + let combined_fixed_bases = read_named_fixed_bases(reader)?; + + // verifier_params is length-prefixed so the certificate verification key can follow it. + let verifier_param_bytes = read_length_prefixed_proof(reader)?; + + let mut verifier_params_reader = Cursor::new(&verifier_param_bytes); + let verifier_params = ParamsVerifierKZG::::read( + &mut verifier_params_reader, + SerdeFormat::RawBytesUnchecked, + )?; + + let certificate_verification_key_bytes = read_length_prefixed_proof(reader)?; + let certificate_verifying_key = MidnightVK::read( + &mut certificate_verification_key_bytes.as_slice(), + SerdeFormat::RawBytes, + )?; + + Ok(VerificationContextAsset { + global_field_elements, + recursive_verifying_key: RecursiveCircuitVerifyingKey::new(recursive_verifying_key), + combined_fixed_bases, + verifier_params, + certificate_verifying_key: NonRecursiveCircuitVerifyingKey::new(certificate_verifying_key), + }) +} + +/// Loads the embedded verifier-side context compiled into the binary. +pub(crate) fn load_embedded_verification_context_asset() -> StmResult { + let mut reader = Cursor::new(VERIFICATION_CONTEXT_ASSET_BYTES); + load_verification_context_asset_from_reader(&mut reader) + .context("failed to decode embedded verification context asset") +} + +/// Read-only view of the fields that generic test helpers need across the three +/// step output asset variants. Implemented by `GenesisStepOutputAsset`, +/// `NextEpochStepOutputAsset`, and `FollowingCertificateInEpochAsset`. +pub(crate) trait StepOutputAsset { + fn next_state(&self) -> &State; + fn next_accumulator(&self) -> &Accumulator; + fn ivc_proof(&self) -> &IvcProofBytes; +} + +impl StepOutputAsset for GenesisStepOutputAsset { + fn next_state(&self) -> &State { + &self.next_state + } + fn next_accumulator(&self) -> &Accumulator { + &self.next_accumulator + } + fn ivc_proof(&self) -> &IvcProofBytes { + &self.ivc_proof + } +} + +impl StepOutputAsset for NextEpochStepOutputAsset { + fn next_state(&self) -> &State { + &self.next_state + } + fn next_accumulator(&self) -> &Accumulator { + &self.next_accumulator + } + fn ivc_proof(&self) -> &IvcProofBytes { + &self.ivc_proof + } +} + +impl StepOutputAsset for FollowingCertificateInEpochAsset { + fn next_state(&self) -> &State { + &self.next_state + } + fn next_accumulator(&self) -> &Accumulator { + &self.next_accumulator + } + fn ivc_proof(&self) -> &IvcProofBytes { + &self.ivc_proof + } +} + +/// Shared on-the-wire shape for the three step output asset variants. Used as a +/// transport struct between the byte-level codec and the typed public assets. +struct StepOutputFields { + ivc_proof: IvcProofBytes, + next_accumulator: Accumulator, + next_state: State, + certificate_proof: CertificateProofBytes, + message: [u8; 32], + message_preimage: [u8; PREIMAGE_SIZE], + aggregate_verification_key_merkle_root: [u8; 32], +} + +impl From for GenesisStepOutputAsset { + fn from(f: StepOutputFields) -> Self { + Self { + ivc_proof: f.ivc_proof, + next_accumulator: f.next_accumulator, + next_state: f.next_state, + certificate_proof: f.certificate_proof, + message: f.message, + message_preimage: f.message_preimage, + aggregate_verification_key_merkle_root: f.aggregate_verification_key_merkle_root, + } + } +} + +impl From for NextEpochStepOutputAsset { + fn from(f: StepOutputFields) -> Self { + Self { + ivc_proof: f.ivc_proof, + next_accumulator: f.next_accumulator, + next_state: f.next_state, + certificate_proof: f.certificate_proof, + message: f.message, + message_preimage: f.message_preimage, + aggregate_verification_key_merkle_root: f.aggregate_verification_key_merkle_root, + } + } +} + +impl From for FollowingCertificateInEpochAsset { + fn from(f: StepOutputFields) -> Self { + Self { + ivc_proof: f.ivc_proof, + next_accumulator: f.next_accumulator, + next_state: f.next_state, + certificate_proof: f.certificate_proof, + message: f.message, + message_preimage: f.message_preimage, + aggregate_verification_key_merkle_root: f.aggregate_verification_key_merkle_root, + } + } +} + +/// Reads the step-output common shape from the committed binary asset layout. +fn read_step_output_fields_from_reader(reader: &mut R) -> StmResult { + let ivc_proof = IvcProofBytes::new(read_length_prefixed_proof(reader)?); + let next_accumulator = + Accumulator::::read(reader, SerdeFormat::RawBytesUnchecked)?; + let next_state = read_state_public_input(reader)?; + let certificate_proof = CertificateProofBytes::from_certificate_circuit_proof_bytes( + read_length_prefixed_proof(reader)?, + ); + let message = read_fixed_length_prefixed::<32, _>(reader, "message")?; + let message_preimage = + read_fixed_length_prefixed::(reader, "message_preimage")?; + let mut aggregate_verification_key_merkle_root = [0u8; 32]; + reader.read_exact(&mut aggregate_verification_key_merkle_root)?; + Ok(StepOutputFields { + ivc_proof, + next_accumulator, + next_state, + certificate_proof, + message, + message_preimage, + aggregate_verification_key_merkle_root, + }) +} + +/// Loads the embedded next-epoch step output compiled into the binary. +pub(crate) fn load_embedded_next_epoch_step_output_asset() -> StmResult { + let mut reader = Cursor::new(NEXT_EPOCH_STEP_OUTPUT_ASSET_BYTES); + Ok(read_step_output_fields_from_reader(&mut reader) + .context("failed to decode embedded next-epoch step output asset")? + .into()) +} + +/// Loads the embedded genesis step output compiled into the binary. +pub(crate) fn load_embedded_genesis_step_output_asset() -> StmResult { + let mut reader = Cursor::new(GENESIS_STEP_OUTPUT_ASSET_BYTES); + Ok(read_step_output_fields_from_reader(&mut reader) + .context("failed to decode embedded genesis step output asset")? + .into()) +} + +/// Loads the embedded following-certificate-in-epoch (same-epoch) step output compiled +/// into the binary. +pub(crate) fn load_embedded_following_certificate_in_epoch_asset() +-> StmResult { + let mut reader = Cursor::new(FOLLOWING_CERTIFICATE_IN_EPOCH_ASSET_BYTES); + Ok(read_step_output_fields_from_reader(&mut reader) + .context("failed to decode embedded following-certificate-in-epoch asset")? + .into()) +} + +/// Loads the first-certificate-in-epoch asset from the committed binary asset layout. +fn load_first_certificate_in_epoch_asset_from_reader( + reader: &mut R, +) -> StmResult { + let certificate_proof = CertificateProofBytes::from_certificate_circuit_proof_bytes( + read_length_prefixed_proof(reader)?, + ); + let next_state = read_state_public_input(reader)?; + let message = read_fixed_length_prefixed::<32, _>(reader, "message")?; + let message_preimage = + read_fixed_length_prefixed::(reader, "message_preimage")?; + let mut aggregate_verification_key_merkle_root = [0u8; 32]; + reader.read_exact(&mut aggregate_verification_key_merkle_root)?; + + Ok(FirstCertificateInEpochAsset { + certificate_proof, + next_state, + message, + message_preimage, + aggregate_verification_key_merkle_root, + }) +} + +/// Loads the embedded first-certificate-in-epoch asset compiled into the binary. +pub(crate) fn load_embedded_first_certificate_in_epoch_asset() +-> StmResult { + let mut reader = Cursor::new(FIRST_CERTIFICATE_IN_EPOCH_ASSET_BYTES); + load_first_certificate_in_epoch_asset_from_reader(&mut reader) + .context("failed to decode embedded first-certificate-in-epoch asset") +} + +/// Decodes the fixed-size genesis benchmark fixture layout: +/// `[genesis_message(32) | genesis_verification_key(64) | genesis_signature(64) | +/// genesis_protocol_message_preimage(PREIMAGE_SIZE)]`. +fn read_genesis_benchmark_fixture_from_reader( + reader: &mut R, +) -> StmResult { + let mut genesis_message = [0u8; 32]; + reader.read_exact(&mut genesis_message)?; + + let mut verification_key_bytes = [0u8; 64]; + reader.read_exact(&mut verification_key_bytes)?; + let genesis_verification_key = SchnorrVerificationKey::from_bytes(&verification_key_bytes)?; + + let mut signature_bytes = [0u8; 64]; + reader.read_exact(&mut signature_bytes)?; + let genesis_signature = StandardSchnorrSignature::from_bytes(&signature_bytes)?; + + let mut genesis_protocol_message_preimage = [0u8; PREIMAGE_SIZE]; + reader.read_exact(&mut genesis_protocol_message_preimage)?; + + Ok(GenesisBenchmarkFixture { + genesis_message, + genesis_verification_key, + genesis_signature, + genesis_protocol_message_preimage, + }) +} + +/// Exact byte length of the fixed-size genesis benchmark fixture layout +/// (`genesis_message(32) + genesis_verification_key(64) + genesis_signature(64) + +/// genesis_protocol_message_preimage(PREIMAGE_SIZE)`). +const GENESIS_BENCHMARK_FIXTURE_LEN: usize = 32 + 64 + 64 + PREIMAGE_SIZE; + +/// Loads the embedded genesis benchmark fixture compiled into the binary. +/// +/// Rejects any committed `.bin` whose length differs from the fixed layout, so trailing +/// bytes or truncation fail loudly rather than decoding a partially-correct fixture. +pub(crate) fn load_embedded_genesis_benchmark_fixture() -> StmResult { + if GENESIS_BENCHMARK_FIXTURE_ASSET_BYTES.len() != GENESIS_BENCHMARK_FIXTURE_LEN { + return Err(anyhow!( + "genesis benchmark fixture: expected {GENESIS_BENCHMARK_FIXTURE_LEN} bytes, got {}", + GENESIS_BENCHMARK_FIXTURE_ASSET_BYTES.len() + )); + } + let mut reader = Cursor::new(GENESIS_BENCHMARK_FIXTURE_ASSET_BYTES); + read_genesis_benchmark_fixture_from_reader(&mut reader) + .context("failed to decode embedded genesis benchmark fixture") +} diff --git a/mithril-stm/src/circuits/halo2_ivc/mod.rs b/mithril-stm/src/circuits/halo2_ivc/mod.rs index be28fbef547..53a561689b4 100644 --- a/mithril-stm/src/circuits/halo2_ivc/mod.rs +++ b/mithril-stm/src/circuits/halo2_ivc/mod.rs @@ -51,10 +51,15 @@ pub(crate) use midnight_proofs::{ }; pub(crate) mod accumulator; +#[cfg(any(test, feature = "benchmark-internals"))] +pub mod bench; pub(crate) mod certificate_proof; pub(crate) mod circuit; pub(crate) mod config; pub(crate) mod constraint_builder; +#[cfg(any(test, feature = "benchmark-internals"))] +#[cfg_attr(not(test), allow(dead_code))] +pub(crate) mod embedded_assets; pub(crate) mod errors; pub(crate) mod gadgets; pub(crate) mod io; diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/assets/genesis_benchmark_fixture.bin b/mithril-stm/src/circuits/halo2_ivc/tests/assets/genesis_benchmark_fixture.bin new file mode 100644 index 00000000000..5570f4f7bd1 Binary files /dev/null and b/mithril-stm/src/circuits/halo2_ivc/tests/assets/genesis_benchmark_fixture.bin differ diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/common/asset_readers.rs b/mithril-stm/src/circuits/halo2_ivc/tests/common/asset_readers.rs index b4d51fd336a..fcd08e3e5bf 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/common/asset_readers.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/common/asset_readers.rs @@ -1,164 +1,39 @@ +//! Filesystem asset writers for the recursive Halo2 IVC golden generators. +//! +//! The read-only, embedded asset layer (typed representations, `include_bytes!` constants, and +//! byte decoders) lives in [`crate::circuits::halo2_ivc::embedded_assets`] so the IVC benchmarks +//! can share it. This module keeps the test-only asset-writing infrastructure (the `store_*` +//! functions used by the golden generators) and re-exports the embedded types and loaders so the +//! `tests::common::asset_readers::{...}` import paths continue to resolve unchanged. + use std::{ collections::BTreeMap, fs::{self, File}, - io::{BufReader, BufWriter, Cursor, Read, Write}, + io::{BufWriter, Write}, path::Path, }; -use anyhow::{Context, anyhow}; -use midnight_proofs::{ - poly::kzg::params::ParamsVerifierKZG, - utils::{SerdeFormat, helpers::ProcessedSerdeObject}, -}; -use midnight_zk_stdlib::MidnightVK; +use anyhow::Context; +use midnight_proofs::utils::{SerdeFormat, helpers::ProcessedSerdeObject}; use crate::StmResult; -use crate::circuits::halo2::keys::NonRecursiveCircuitVerifyingKey; -use crate::circuits::halo2_ivc::keys::RecursiveCircuitVerifyingKey; use crate::circuits::halo2_ivc::{ - Accumulator, EmulatedCurve, KZGCommitmentScheme, NativeField, PREIMAGE_SIZE, PairingEngine, - RecursiveEmulation, VerifyingKey, - circuit::IvcCircuitData, - io::{Read as IvcRead, Write as IvcWrite}, + Accumulator, EmulatedCurve, NativeField, PREIMAGE_SIZE, RecursiveEmulation, + io::Write as IvcWrite, state::State, - types::{ - CertificateProofBytes, EpochNumber, IvcProofBytes, MerkleTreeCommitment, MessageHash, - ProtocolParametersHash, StepCounter, - }, + types::{CertificateProofBytes, IvcProofBytes}, }; use crate::signature_scheme::StandardSchnorrSignature; -use super::field_encoding::jubjub_base_from_raw_le_bytes; - -/// Stored recursive chain checkpoint used by the golden tests. -#[derive(Debug)] -pub(crate) struct RecursiveChainStateAsset { - /// Stored global public inputs for the recursive flow. - pub(crate) global_field_elements: Vec, - /// Stored recursive state checkpoint. - pub(crate) state: State, - /// Stored previous recursive proof bytes. - pub(crate) ivc_proof: IvcProofBytes, - /// Stored folded accumulator for the checkpoint. - pub(crate) accumulator: Accumulator, - /// Stored chain-specific Schnorr signature over the genesis state. - pub(crate) genesis_signature: StandardSchnorrSignature, -} - -/// Stored verifier-side context shared by the golden assets. -#[derive(Debug)] -pub(crate) struct VerificationContextAsset { - /// Shared global public inputs used by the committed assets. - pub(crate) global_field_elements: Vec, - /// Stored recursive verifying key. - pub(crate) recursive_verifying_key: RecursiveCircuitVerifyingKey, - /// Combined fixed bases needed to check recursive accumulators. - pub(crate) combined_fixed_bases: BTreeMap, - /// Shared verifier-side KZG parameters. - pub(crate) verifier_params: ParamsVerifierKZG, - /// Stored certificate verifying key, enabling MockProver tests to skip SRS generation. - pub(crate) certificate_verifying_key: NonRecursiveCircuitVerifyingKey, -} - -/// Stored data of the first certificate produced from `build_genesis_base_case_next_state()`. -/// Used to test `IvcProverInput::prepare` at the first real certificate step (where the rolling -/// state's `step_counter` is one, after the internal genesis IVC step has run). -#[derive(Debug)] -pub(crate) struct FirstCertificateInEpochAsset { - /// Certificate proof bytes (consumed by `prepare` via `SnarkProof`). - pub(crate) certificate_proof: CertificateProofBytes, - /// Expected next state after `prepare` advances by one step. - pub(crate) next_state: State, - /// SHA-256 hash that the certificate proof committed to. - pub(crate) message: [u8; 32], - /// Protocol-message preimage; `ProtocolMessagePreimage::current_epoch()` / `next_merkle_tree_commitment()` / `next_protocol_parameters()` decode the four - /// epoch fields. - pub(crate) message_preimage: [u8; PREIMAGE_SIZE], - /// Canonical encoding of the aggregate verification key merkle root the certificate - /// proof committed to. - pub(crate) aggregate_verification_key_merkle_root: [u8; 32], -} - -/// Stored output of the genesis base-case step. Carries no certificate; the message, -/// preimage, and aggregate-verification-key fields are zero-byte placeholders. -#[derive(Debug)] -pub(crate) struct GenesisStepOutputAsset { - /// Stored final recursive proof bytes for the genesis step. - pub(crate) ivc_proof: IvcProofBytes, - /// Stored folded accumulator after the genesis step. - pub(crate) next_accumulator: Accumulator, - /// Stored next recursive state. - pub(crate) next_state: State, - /// Empty: genesis carries no certificate proof. - pub(crate) certificate_proof: CertificateProofBytes, - /// Zero placeholder; no certificate exists at genesis. - pub(crate) message: [u8; 32], - /// Zero placeholder; no protocol-message preimage exists at genesis. - pub(crate) message_preimage: [u8; PREIMAGE_SIZE], - /// Zero placeholder; no aggregate verification key merkle root exists at genesis. - pub(crate) aggregate_verification_key_merkle_root: [u8; 32], -} - -/// Stored output of extending the recursive chain by one next-epoch step (the cert -/// is the first cert of a new epoch, transitioning the chain from epoch N to N+1). -#[derive(Debug)] -pub(crate) struct NextEpochStepOutputAsset { - /// Stored final recursive proof bytes for the next-epoch step. - pub(crate) ivc_proof: IvcProofBytes, - /// Stored folded accumulator after the next-epoch step. - pub(crate) next_accumulator: Accumulator, - /// Stored next recursive state. - pub(crate) next_state: State, - /// Stored certificate proof consumed by the next-epoch step. - pub(crate) certificate_proof: CertificateProofBytes, - /// SHA-256 hash that the certificate proof committed to. - pub(crate) message: [u8; 32], - /// Protocol-message preimage; `ProtocolMessagePreimage::current_epoch()` / `next_merkle_tree_commitment()` / `next_protocol_parameters()` decode the four - /// epoch fields. - pub(crate) message_preimage: [u8; PREIMAGE_SIZE], - /// Canonical encoding of the aggregate verification key merkle root the certificate - /// proof committed to. - pub(crate) aggregate_verification_key_merkle_root: [u8; 32], -} - -/// Stored output of extending the recursive chain by one same-epoch step (the cert -/// follows in the current epoch and does not change the epoch boundary). -#[derive(Debug)] -pub(crate) struct FollowingCertificateInEpochAsset { - /// Stored final recursive proof bytes for the same-epoch step. - pub(crate) ivc_proof: IvcProofBytes, - /// Stored folded accumulator after the same-epoch step. - pub(crate) next_accumulator: Accumulator, - /// Stored next recursive state. - pub(crate) next_state: State, - /// Stored certificate proof consumed by the same-epoch step. - pub(crate) certificate_proof: CertificateProofBytes, - /// SHA-256 hash that the certificate proof committed to. - pub(crate) message: [u8; 32], - /// Protocol-message preimage; `ProtocolMessagePreimage::current_epoch()` / `next_merkle_tree_commitment()` / `next_protocol_parameters()` decode the four - /// epoch fields. - pub(crate) message_preimage: [u8; PREIMAGE_SIZE], - /// Canonical encoding of the aggregate verification key merkle root the certificate - /// proof committed to. - pub(crate) aggregate_verification_key_merkle_root: [u8; 32], -} - -const RECURSIVE_CHAIN_STATE_ASSET_BYTES: &[u8] = - include_bytes!("../assets/recursive_chain_state.bin"); -const VERIFICATION_CONTEXT_ASSET_BYTES: &[u8] = - include_bytes!("../assets/verification_context.bin"); -const NEXT_EPOCH_STEP_OUTPUT_ASSET_BYTES: &[u8] = - include_bytes!("../assets/recursive_step_output.bin"); -const GENESIS_STEP_OUTPUT_ASSET_BYTES: &[u8] = include_bytes!("../assets/genesis_step_output.bin"); -const FOLLOWING_CERTIFICATE_IN_EPOCH_ASSET_BYTES: &[u8] = - include_bytes!("../assets/same_epoch_step_output.bin"); -const FIRST_CERTIFICATE_IN_EPOCH_ASSET_BYTES: &[u8] = - include_bytes!("../assets/first_step_cert.bin"); - -/// Opens a committed golden asset for buffered reading. -fn open_asset_file(path: &Path) -> StmResult> { - Ok(BufReader::new(File::open(path)?)) -} +pub(crate) use crate::circuits::halo2_ivc::embedded_assets::{ + FirstCertificateInEpochAsset, FollowingCertificateInEpochAsset, GenesisBenchmarkFixture, + GenesisStepOutputAsset, NextEpochStepOutputAsset, RecursiveChainStateAsset, StepOutputAsset, + VerificationContextAsset, load_embedded_first_certificate_in_epoch_asset, + load_embedded_following_certificate_in_epoch_asset, load_embedded_genesis_benchmark_fixture, + load_embedded_genesis_step_output_asset, load_embedded_next_epoch_step_output_asset, + load_embedded_recursive_chain_state_asset, load_embedded_verification_context_asset, + load_recursive_chain_state_asset, +}; /// Creates a golden asset file and its parent directory if needed. fn create_asset_file(path: &Path) -> StmResult> { @@ -168,32 +43,12 @@ fn create_asset_file(path: &Path) -> StmResult> { Ok(BufWriter::new(File::create(path)?)) } -/// Reads one field element encoded as 32 little-endian bytes. -fn read_field_element(reader: &mut R) -> StmResult { - let mut bytes = [0u8; 32]; - reader.read_exact(&mut bytes)?; - Ok(jubjub_base_from_raw_le_bytes(&bytes)) -} - /// Writes one field element as 32 little-endian bytes. fn write_field_element(writer: &mut W, value: &NativeField) -> StmResult<()> { writer.write_all(&value.to_bytes_le())?; Ok(()) } -/// Reads the seven public-input field elements that define a recursive state. -fn read_state_public_input(reader: &mut R) -> StmResult { - Ok(State::new( - StepCounter::from_field(read_field_element(reader)?), - MessageHash::from_field(read_field_element(reader)?), - MerkleTreeCommitment::from_field(read_field_element(reader)?), - MerkleTreeCommitment::from_field(read_field_element(reader)?), - ProtocolParametersHash::from_field(read_field_element(reader)?), - ProtocolParametersHash::from_field(read_field_element(reader)?), - EpochNumber::from_field(read_field_element(reader)?), - )) -} - /// Writes the seven public-input field elements of a recursive state. fn write_state_public_input(writer: &mut W, state: &State) -> StmResult<()> { for value in state.as_public_input() { @@ -202,13 +57,6 @@ fn write_state_public_input(writer: &mut W, state: &State) -> StmResul Ok(()) } -/// Reads a 64-byte `StandardSchnorrSignature` (response | challenge). -fn read_schnorr_signature(reader: &mut R) -> StmResult { - let mut bytes = [0u8; 64]; - reader.read_exact(&mut bytes)?; - StandardSchnorrSignature::from_bytes(&bytes) -} - /// Writes a 64-byte `StandardSchnorrSignature` (response | challenge). fn write_schnorr_signature( writer: &mut W, @@ -218,30 +66,6 @@ fn write_schnorr_signature( Ok(()) } -/// Reads exactly `N` bytes stored behind a 32-bit little-endian length prefix. Returns -/// an error if the prefix does not equal `N`. -fn read_fixed_length_prefixed( - reader: &mut R, - field_name: &str, -) -> StmResult<[u8; N]> { - let bytes = read_length_prefixed_proof(reader)?; - bytes - .try_into() - .map_err(|v: Vec| anyhow!("{field_name}: expected {N} bytes, got {} bytes", v.len())) -} - -/// Reads proof bytes stored behind a 32-bit little-endian length prefix. -fn read_length_prefixed_proof(reader: &mut R) -> StmResult> { - let mut len = [0u8; 4]; - reader.read_exact(&mut len)?; - let proof_len = u32::from_le_bytes(len) as usize; - - let mut proof = vec![0u8; proof_len]; - reader.read_exact(&mut proof)?; - - Ok(proof) -} - /// Writes proof bytes with a 32-bit little-endian length prefix. fn write_length_prefixed_proof(writer: &mut W, proof: &[u8]) -> StmResult<()> { writer.write_all(&(proof.len() as u32).to_le_bytes())?; @@ -249,30 +73,6 @@ fn write_length_prefixed_proof(writer: &mut W, proof: &[u8]) -> StmRes Ok(()) } -/// Reads the named fixed-base map stored in the verification-context asset. -fn read_named_fixed_bases(reader: &mut R) -> StmResult> { - let mut count = [0u8; 4]; - reader.read_exact(&mut count)?; - let count = u32::from_le_bytes(count) as usize; - - let mut map = BTreeMap::new(); - for _ in 0..count { - let mut name_len = [0u8; 4]; - reader.read_exact(&mut name_len)?; - let name_len = u32::from_le_bytes(name_len) as usize; - - let mut name_bytes = vec![0u8; name_len]; - reader.read_exact(&mut name_bytes)?; - let name = String::from_utf8(name_bytes) - .map_err(|_| anyhow!("invalid UTF-8 key in verification-context fixed-base map"))?; - - let point = EmulatedCurve::read(reader, SerdeFormat::RawBytesUnchecked)?; - map.insert(name, point); - } - - Ok(map) -} - /// Writes the named fixed-base map stored in the verification-context asset. fn write_named_fixed_bases( writer: &mut W, @@ -288,51 +88,6 @@ fn write_named_fixed_bases( Ok(()) } -/// Loads a recursive chain snapshot from the committed binary asset layout. -fn load_recursive_chain_state_asset_from_reader( - reader: &mut R, -) -> StmResult { - let global_field_elements = (0..5) - .map(|_| read_field_element(reader)) - .collect::, _>>()?; - let state = read_state_public_input(reader)?; - let ivc_proof = IvcProofBytes::new(read_length_prefixed_proof(reader)?); - let accumulator = - Accumulator::::read(reader, SerdeFormat::RawBytesUnchecked)?; - let genesis_signature = read_schnorr_signature(reader)?; - - Ok(RecursiveChainStateAsset { - global_field_elements, - state, - ivc_proof, - accumulator, - genesis_signature, - }) -} - -/// Loads the stored recursive chain snapshot from a committed asset file. -pub(crate) fn load_recursive_chain_state_asset(path: &Path) -> StmResult { - let mut reader = open_asset_file(path).with_context(|| { - format!( - "failed to open recursive chain state asset: {}", - path.display() - ) - })?; - load_recursive_chain_state_asset_from_reader(&mut reader).with_context(|| { - format!( - "failed to decode recursive chain state asset: {}", - path.display() - ) - }) -} - -/// Loads the embedded recursive chain snapshot compiled into the test binary. -pub(crate) fn load_embedded_recursive_chain_state_asset() -> StmResult { - let mut reader = Cursor::new(RECURSIVE_CHAIN_STATE_ASSET_BYTES); - load_recursive_chain_state_asset_from_reader(&mut reader) - .context("failed to decode embedded recursive chain state asset") -} - /// Writes the recursive chain state asset using the committed binary layout. pub(crate) fn store_recursive_chain_state_asset( path: &Path, @@ -361,60 +116,10 @@ pub(crate) fn store_recursive_chain_state_asset( Ok(()) } -/// Loads a verifier-side context from the committed binary asset layout. -/// -/// Binary layout (v2): -/// `[global(5×32b) | recursive_vk(var) | combined_fixed_bases(var) | -/// verifier_params_len(4b LE) | verifier_params(var) | -/// certificate_vk_len(4b LE) | certificate_vk(var)]` -fn load_verification_context_asset_from_reader( - reader: &mut R, -) -> StmResult { - let global_field_elements = (0..5) - .map(|_| read_field_element(reader)) - .collect::, _>>()?; - let recursive_verifying_key = - VerifyingKey::>::read::<_, IvcCircuitData>( - reader, - SerdeFormat::RawBytesUnchecked, - (), - )?; - let combined_fixed_bases = read_named_fixed_bases(reader)?; - - // verifier_params is length-prefixed so the certificate verification key can follow it. - let verifier_param_bytes = read_length_prefixed_proof(reader)?; - - let mut verifier_params_reader = Cursor::new(&verifier_param_bytes); - let verifier_params = ParamsVerifierKZG::::read( - &mut verifier_params_reader, - SerdeFormat::RawBytesUnchecked, - )?; - - let certificate_verification_key_bytes = read_length_prefixed_proof(reader)?; - let certificate_verifying_key = MidnightVK::read( - &mut certificate_verification_key_bytes.as_slice(), - SerdeFormat::RawBytes, - )?; - - Ok(VerificationContextAsset { - global_field_elements, - recursive_verifying_key: RecursiveCircuitVerifyingKey::new(recursive_verifying_key), - combined_fixed_bases, - verifier_params, - certificate_verifying_key: NonRecursiveCircuitVerifyingKey::new(certificate_verifying_key), - }) -} - -/// Loads the embedded verifier-side context compiled into the test binary. -pub(crate) fn load_embedded_verification_context_asset() -> StmResult { - let mut reader = Cursor::new(VERIFICATION_CONTEXT_ASSET_BYTES); - load_verification_context_asset_from_reader(&mut reader) - .context("failed to decode embedded verification context asset") -} - /// Writes the verification-context asset using the committed binary layout. /// -/// Binary layout (v2): see `load_verification_context_asset_from_reader`. +/// Binary layout (v2): see +/// [`crate::circuits::halo2_ivc::embedded_assets::load_embedded_verification_context_asset`]. pub(crate) fn store_verification_context_asset( path: &Path, asset: &VerificationContextAsset, @@ -458,130 +163,6 @@ pub(crate) fn store_verification_context_asset( Ok(()) } -/// Read-only view of the fields that generic test helpers need across the three -/// step output asset variants. Implemented by `GenesisStepOutputAsset`, -/// `NextEpochStepOutputAsset`, and `FollowingCertificateInEpochAsset`. -pub(crate) trait StepOutputAsset { - fn next_state(&self) -> &State; - fn next_accumulator(&self) -> &Accumulator; - fn ivc_proof(&self) -> &IvcProofBytes; -} - -impl StepOutputAsset for GenesisStepOutputAsset { - fn next_state(&self) -> &State { - &self.next_state - } - fn next_accumulator(&self) -> &Accumulator { - &self.next_accumulator - } - fn ivc_proof(&self) -> &IvcProofBytes { - &self.ivc_proof - } -} - -impl StepOutputAsset for NextEpochStepOutputAsset { - fn next_state(&self) -> &State { - &self.next_state - } - fn next_accumulator(&self) -> &Accumulator { - &self.next_accumulator - } - fn ivc_proof(&self) -> &IvcProofBytes { - &self.ivc_proof - } -} - -impl StepOutputAsset for FollowingCertificateInEpochAsset { - fn next_state(&self) -> &State { - &self.next_state - } - fn next_accumulator(&self) -> &Accumulator { - &self.next_accumulator - } - fn ivc_proof(&self) -> &IvcProofBytes { - &self.ivc_proof - } -} - -/// Shared on-the-wire shape for the three step output asset variants. Used as a -/// transport struct between the byte-level codec and the typed public assets. -struct StepOutputFields { - ivc_proof: IvcProofBytes, - next_accumulator: Accumulator, - next_state: State, - certificate_proof: CertificateProofBytes, - message: [u8; 32], - message_preimage: [u8; PREIMAGE_SIZE], - aggregate_verification_key_merkle_root: [u8; 32], -} - -impl From for GenesisStepOutputAsset { - fn from(f: StepOutputFields) -> Self { - Self { - ivc_proof: f.ivc_proof, - next_accumulator: f.next_accumulator, - next_state: f.next_state, - certificate_proof: f.certificate_proof, - message: f.message, - message_preimage: f.message_preimage, - aggregate_verification_key_merkle_root: f.aggregate_verification_key_merkle_root, - } - } -} - -impl From for NextEpochStepOutputAsset { - fn from(f: StepOutputFields) -> Self { - Self { - ivc_proof: f.ivc_proof, - next_accumulator: f.next_accumulator, - next_state: f.next_state, - certificate_proof: f.certificate_proof, - message: f.message, - message_preimage: f.message_preimage, - aggregate_verification_key_merkle_root: f.aggregate_verification_key_merkle_root, - } - } -} - -impl From for FollowingCertificateInEpochAsset { - fn from(f: StepOutputFields) -> Self { - Self { - ivc_proof: f.ivc_proof, - next_accumulator: f.next_accumulator, - next_state: f.next_state, - certificate_proof: f.certificate_proof, - message: f.message, - message_preimage: f.message_preimage, - aggregate_verification_key_merkle_root: f.aggregate_verification_key_merkle_root, - } - } -} - -/// Reads the step-output common shape from the committed binary asset layout. -fn read_step_output_fields_from_reader(reader: &mut R) -> StmResult { - let ivc_proof = IvcProofBytes::new(read_length_prefixed_proof(reader)?); - let next_accumulator = - Accumulator::::read(reader, SerdeFormat::RawBytesUnchecked)?; - let next_state = read_state_public_input(reader)?; - let certificate_proof = CertificateProofBytes::from_certificate_circuit_proof_bytes( - read_length_prefixed_proof(reader)?, - ); - let message = read_fixed_length_prefixed::<32, _>(reader, "message")?; - let message_preimage = - read_fixed_length_prefixed::(reader, "message_preimage")?; - let mut aggregate_verification_key_merkle_root = [0u8; 32]; - reader.read_exact(&mut aggregate_verification_key_merkle_root)?; - Ok(StepOutputFields { - ivc_proof, - next_accumulator, - next_state, - certificate_proof, - message, - message_preimage, - aggregate_verification_key_merkle_root, - }) -} - /// Writes the step-output common shape using the committed binary layout. Shared /// across the three step output asset writers. #[allow(clippy::too_many_arguments)] @@ -605,32 +186,6 @@ fn write_step_output_fields( Ok(()) } -/// Loads the embedded next-epoch step output compiled into the test binary. -pub(crate) fn load_embedded_next_epoch_step_output_asset() -> StmResult { - let mut reader = Cursor::new(NEXT_EPOCH_STEP_OUTPUT_ASSET_BYTES); - Ok(read_step_output_fields_from_reader(&mut reader) - .context("failed to decode embedded next-epoch step output asset")? - .into()) -} - -/// Loads the embedded genesis step output compiled into the test binary. -pub(crate) fn load_embedded_genesis_step_output_asset() -> StmResult { - let mut reader = Cursor::new(GENESIS_STEP_OUTPUT_ASSET_BYTES); - Ok(read_step_output_fields_from_reader(&mut reader) - .context("failed to decode embedded genesis step output asset")? - .into()) -} - -/// Loads the embedded following-certificate-in-epoch (same-epoch) step output compiled -/// into the test binary. -pub(crate) fn load_embedded_following_certificate_in_epoch_asset() --> StmResult { - let mut reader = Cursor::new(FOLLOWING_CERTIFICATE_IN_EPOCH_ASSET_BYTES); - Ok(read_step_output_fields_from_reader(&mut reader) - .context("failed to decode embedded following-certificate-in-epoch asset")? - .into()) -} - /// Writes the next-epoch step output asset using the committed binary layout. pub(crate) fn store_next_epoch_step_output_asset( path: &Path, @@ -722,37 +277,6 @@ pub(crate) fn store_following_certificate_in_epoch_asset( Ok(()) } -/// Loads the first-certificate-in-epoch asset from the committed binary asset layout. -fn load_first_certificate_in_epoch_asset_from_reader( - reader: &mut R, -) -> StmResult { - let certificate_proof = CertificateProofBytes::from_certificate_circuit_proof_bytes( - read_length_prefixed_proof(reader)?, - ); - let next_state = read_state_public_input(reader)?; - let message = read_fixed_length_prefixed::<32, _>(reader, "message")?; - let message_preimage = - read_fixed_length_prefixed::(reader, "message_preimage")?; - let mut aggregate_verification_key_merkle_root = [0u8; 32]; - reader.read_exact(&mut aggregate_verification_key_merkle_root)?; - - Ok(FirstCertificateInEpochAsset { - certificate_proof, - next_state, - message, - message_preimage, - aggregate_verification_key_merkle_root, - }) -} - -/// Loads the embedded first-certificate-in-epoch asset compiled into the test binary. -pub(crate) fn load_embedded_first_certificate_in_epoch_asset() --> StmResult { - let mut reader = Cursor::new(FIRST_CERTIFICATE_IN_EPOCH_ASSET_BYTES); - load_first_certificate_in_epoch_asset_from_reader(&mut reader) - .context("failed to decode embedded first-certificate-in-epoch asset") -} - /// Writes the first-certificate-in-epoch asset using the committed binary layout. pub(crate) fn store_first_certificate_in_epoch_asset( path: &Path, @@ -778,3 +302,30 @@ pub(crate) fn store_first_certificate_in_epoch_asset( })?; Ok(()) } + +/// Writes the additive genesis benchmark fixture using the committed binary layout. +/// +/// Fixed-size layout: `[genesis_message(32) | genesis_verification_key(64) | +/// genesis_signature(64) | genesis_protocol_message_preimage(PREIMAGE_SIZE)]`. +pub(crate) fn store_genesis_benchmark_fixture( + path: &Path, + fixture: &GenesisBenchmarkFixture, +) -> StmResult<()> { + let mut writer = create_asset_file(path).with_context(|| { + format!( + "failed to create genesis benchmark fixture file: {}", + path.display() + ) + })?; + writer.write_all(&fixture.genesis_message)?; + writer.write_all(&fixture.genesis_verification_key.to_bytes())?; + writer.write_all(&fixture.genesis_signature.to_bytes())?; + writer.write_all(&fixture.genesis_protocol_message_preimage)?; + writer.flush().with_context(|| { + format!( + "failed to flush genesis benchmark fixture: {}", + path.display() + ) + })?; + Ok(()) +} diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/common/field_encoding.rs b/mithril-stm/src/circuits/halo2_ivc/tests/common/field_encoding.rs index e1dda4832f9..83a05967e6e 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/common/field_encoding.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/common/field_encoding.rs @@ -1,17 +1,7 @@ -use crate::circuits::halo2_ivc::NativeField; +//! Field-encoding helpers for the recursive Halo2 IVC test infrastructure. +//! +//! The implementation lives in [`crate::circuits::halo2_ivc::embedded_assets`] so it is available +//! to both the tests and the `benchmark-internals` asset layer; it is re-exported here to keep the +//! existing `field_encoding::jubjub_base_from_raw_le_bytes` import paths stable. -/// Interprets 32 little-endian bytes as an integer and maps it to a Jubjub -/// base-field element. -/// -/// This intentionally uses `from_raw` instead of canonical decoding: SHA256 -/// digests and legacy asset/test bytes may be non-canonical, and `from_raw` -/// converts them into the congruent field element modulo the field order. -pub(crate) fn jubjub_base_from_raw_le_bytes(bytes: &[u8]) -> NativeField { - assert_eq!(bytes.len(), 32); - NativeField::from_raw([ - u64::from_le_bytes(bytes[0..8].try_into().unwrap()), - u64::from_le_bytes(bytes[8..16].try_into().unwrap()), - u64::from_le_bytes(bytes[16..24].try_into().unwrap()), - u64::from_le_bytes(bytes[24..32].try_into().unwrap()), - ]) -} +pub(crate) use crate::circuits::halo2_ivc::embedded_assets::jubjub_base_from_raw_le_bytes; diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/asset_generation.rs b/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/asset_generation.rs index 8baa395ee67..6899eda9f8c 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/asset_generation.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/asset_generation.rs @@ -14,14 +14,16 @@ use super::setup::{ }; use super::transitions::{ build_genesis_base_case_next_state, build_genesis_base_case_witness, - build_next_certificate_asset_data, build_same_epoch_certificate_asset_data, + build_genesis_protocol_message_preimage, build_next_certificate_asset_data, + build_same_epoch_certificate_asset_data, }; use crate::circuits::halo2_ivc::IVC_FIXED_BASES_PREFIX; use crate::circuits::halo2_ivc::tests::common::asset_readers::{ - FirstCertificateInEpochAsset, FollowingCertificateInEpochAsset, GenesisStepOutputAsset, - NextEpochStepOutputAsset, RecursiveChainStateAsset, VerificationContextAsset, - load_recursive_chain_state_asset, store_first_certificate_in_epoch_asset, - store_following_certificate_in_epoch_asset, store_genesis_step_output_asset, + FirstCertificateInEpochAsset, FollowingCertificateInEpochAsset, GenesisBenchmarkFixture, + GenesisStepOutputAsset, NextEpochStepOutputAsset, RecursiveChainStateAsset, + VerificationContextAsset, load_recursive_chain_state_asset, + store_first_certificate_in_epoch_asset, store_following_certificate_in_epoch_asset, + store_genesis_benchmark_fixture, store_genesis_step_output_asset, store_next_epoch_step_output_asset, store_recursive_chain_state_asset, store_verification_context_asset, }; @@ -821,6 +823,37 @@ pub(crate) fn generate_first_step_cert_asset(setup: &AssetGenerationSetup, paths ); } +/// Generates and writes the additive genesis benchmark fixture asset — the genesis proving +/// inputs (raw message, verification key, signature, protocol-message preimage) the IVC +/// benchmarks need to build a `Global` and run a genesis proving step. Deterministic and +/// additive: existing golden assets are unaffected. +pub(crate) fn generate_genesis_benchmark_fixture_asset( + setup: &AssetGenerationSetup, + paths: &AssetPaths, +) { + println!( + "generate_genesis_benchmark_fixture: start -> {}", + paths.genesis_benchmark_fixture.display() + ); + let genesis_protocol_message_preimage: [u8; PREIMAGE_SIZE] = + build_genesis_protocol_message_preimage(setup) + .try_into() + .expect("genesis protocol message preimage should be PREIMAGE_SIZE bytes"); + let genesis_message: [u8; 32] = Sha256::digest(genesis_protocol_message_preimage).into(); + let fixture = GenesisBenchmarkFixture { + genesis_message, + genesis_verification_key: setup.genesis_verification_key, + genesis_signature: setup.genesis_signature, + genesis_protocol_message_preimage, + }; + store_genesis_benchmark_fixture(&paths.genesis_benchmark_fixture, &fixture) + .expect("failed to write genesis benchmark fixture asset"); + println!( + "generate_genesis_benchmark_fixture: done -> {}", + paths.genesis_benchmark_fixture.display() + ); +} + // These ignored tests are manual asset-generation entrypoints for the committed // golden assets. They are intentionally excluded from normal test runs because // they rewrite binary files rather than asserting behavior. @@ -877,6 +910,16 @@ fn generate_first_step_cert_only() { generate_first_step_cert_asset(&build_asset_generation_setup(), &AssetPaths::default()); } +#[test] +#[ignore] +fn generate_genesis_benchmark_fixture_only() { + use super::setup::{AssetPaths, build_asset_generation_setup}; + generate_genesis_benchmark_fixture_asset( + &build_asset_generation_setup(), + &AssetPaths::default(), + ); +} + #[test] #[ignore] fn generate_recursive_step_output_accumulator_bytes_only() { diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/setup.rs b/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/setup.rs index 3cee411c9e3..1bdb4286f15 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/setup.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/setup.rs @@ -57,6 +57,8 @@ pub(super) struct AssetPaths { pub(super) same_epoch_step_output: PathBuf, /// Path to the stored first-step certificate asset. pub(super) first_step_cert: PathBuf, + /// Path to the additive genesis benchmark fixture asset. + pub(super) genesis_benchmark_fixture: PathBuf, } impl AssetPaths { @@ -69,6 +71,7 @@ impl AssetPaths { genesis_step_output: base_dir.join("genesis_step_output.bin"), same_epoch_step_output: base_dir.join("same_epoch_step_output.bin"), first_step_cert: base_dir.join("first_step_cert.bin"), + genesis_benchmark_fixture: base_dir.join("genesis_benchmark_fixture.bin"), } } } diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/golden/positive.rs b/mithril-stm/src/circuits/halo2_ivc/tests/golden/positive.rs index 6ad9d4d61cb..81ce0646c81 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/golden/positive.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/golden/positive.rs @@ -7,15 +7,17 @@ //! which use the full prover output: a valid proof implies all constraints hold. use midnight_circuits::types::Instantiable; +use sha2::{Digest, Sha256}; use crate::circuits::halo2_ivc::tests::common::{ asset_readers::{ - load_embedded_next_epoch_step_output_asset, load_embedded_recursive_chain_state_asset, - load_embedded_verification_context_asset, + load_embedded_genesis_benchmark_fixture, load_embedded_next_epoch_step_output_asset, + load_embedded_recursive_chain_state_asset, load_embedded_verification_context_asset, }, generators::{ GENESIS_EPOCH, build_asset_generation_setup, build_genesis_base_case_next_state, - build_genesis_base_case_witness, next_message_and_preimage_for_step, next_state_for_step, + build_genesis_base_case_witness, build_genesis_protocol_message_preimage, + next_message_and_preimage_for_step, next_state_for_step, }, helpers::{ assert_recursive_mock_prover_accepts_with_label, build_mock_prover_public_inputs, @@ -25,6 +27,7 @@ use crate::circuits::halo2_ivc::tests::common::{ }, }; use crate::circuits::halo2_ivc::{AssignedAccumulator, state::State}; +use crate::signature_scheme::BaseFieldElement; #[test] fn recursive_chain_state_asset_proof_and_accumulator_are_valid() { @@ -95,6 +98,48 @@ fn recursive_step_output_asset_proof_and_accumulator_are_valid() { ); } +#[test] +fn genesis_benchmark_fixture_is_deterministic_and_valid() { + // Guards the additive genesis benchmark fixture: the committed bytes must match the + // deterministic generator output, be internally consistent, and carry a valid genesis + // signature. Fails loudly if the committed `.bin` drifts from `build_asset_generation_setup`. + let setup = build_asset_generation_setup(); + let fixture = + load_embedded_genesis_benchmark_fixture().expect("genesis benchmark fixture should load"); + + let expected_preimage = build_genesis_protocol_message_preimage(&setup); + assert_eq!( + fixture.genesis_protocol_message_preimage.as_slice(), + expected_preimage.as_slice(), + "fixture preimage should match the deterministic generator output" + ); + + let expected_message: [u8; 32] = Sha256::digest(&expected_preimage).into(); + assert_eq!( + fixture.genesis_message, expected_message, + "fixture genesis message should be Sha256 of the preimage" + ); + assert_eq!( + fixture.genesis_message_hash(), + setup.genesis_message, + "fixture genesis message hash should match the deterministic setup" + ); + assert_eq!( + fixture.genesis_verification_key, setup.genesis_verification_key, + "fixture genesis verification key should match the deterministic setup" + ); + assert_eq!( + fixture.genesis_signature, setup.genesis_signature, + "fixture genesis signature should match the deterministic setup" + ); + + let message = BaseFieldElement::from(fixture.genesis_message_hash().as_field()); + fixture + .genesis_signature + .verify(&[message], &fixture.genesis_verification_key) + .expect("fixture genesis signature should verify under the fixture verification key"); +} + mod slow { use super::*; diff --git a/mithril-stm/src/circuits/halo2_ivc/types.rs b/mithril-stm/src/circuits/halo2_ivc/types.rs index 86b9f5fd41d..d22a104a2ca 100644 --- a/mithril-stm/src/circuits/halo2_ivc/types.rs +++ b/mithril-stm/src/circuits/halo2_ivc/types.rs @@ -74,7 +74,7 @@ macro_rules! u64_wrapper { NativeField::from(self.0) } - #[cfg(test)] + #[cfg(any(test, feature = "benchmark-internals"))] pub(crate) fn from_field(value: NativeField) -> Self { let bytes = value.to_bytes_le(); let low_bytes = bytes[0..8] diff --git a/mithril-stm/src/circuits/trusted_setup.rs b/mithril-stm/src/circuits/trusted_setup.rs index a5364ec749b..bbb482cb908 100644 --- a/mithril-stm/src/circuits/trusted_setup.rs +++ b/mithril-stm/src/circuits/trusted_setup.rs @@ -11,7 +11,7 @@ use midnight_proofs::{poly::kzg::params::ParamsKZG, utils::SerdeFormat}; use sha2::{Digest, Sha256}; use crate::{StmResult, circuits::MITHRIL_CIRCUIT_CACHE_FOLDER}; -#[cfg(test)] +#[cfg(any(test, feature = "benchmark-internals"))] use {rand_chacha::ChaCha20Rng, rand_core::SeedableRng, std::fs::create_dir_all}; /// Constant storing the hash of the SRS of degree 22 used to create proof in production. @@ -195,14 +195,14 @@ impl Default for TrustedSetupProvider { /// fold in this seed so they stay correct if it ever changes. The IVC setup cache also folds in the /// SRS degree; the certificate-key cache omits it, since keygen downsizes the seed-pinned SRS to the /// certificate circuit's own degree, so the oversized degree never affects the certificate key. -#[cfg(test)] +#[cfg(any(test, feature = "benchmark-internals"))] pub(crate) const UNSAFE_SRS_SEED: u64 = 42; -#[cfg(test)] +#[cfg(any(test, feature = "benchmark-internals"))] impl TrustedSetupProvider { /// Builds a `TrustedSetupProvider` backed by a freshly generated unsafe SRS of degree `k`, written /// to `base_dir/srs/srs-parameters` with a matching SHA256 hash so the provider's hash check passes. - /// For tests only. + /// For tests and benchmarks only. pub(crate) fn with_unsafe_srs(base_dir: &std::path::Path, k: u32) -> Self { let srs = ParamsKZG::::unsafe_setup(k, ChaCha20Rng::seed_from_u64(UNSAFE_SRS_SEED)); let mut srs_bytes = Vec::new(); diff --git a/mithril-stm/src/proof_system/ivc_halo2_snark/mod.rs b/mithril-stm/src/proof_system/ivc_halo2_snark/mod.rs index f1382ba08e8..1f755983bfa 100644 --- a/mithril-stm/src/proof_system/ivc_halo2_snark/mod.rs +++ b/mithril-stm/src/proof_system/ivc_halo2_snark/mod.rs @@ -6,4 +6,6 @@ mod prover_setup; pub(crate) mod rolling_state; pub(crate) mod verifier_setup; +#[cfg(feature = "benchmark-internals")] +pub(crate) use prover_input::IvcProverInput; pub(crate) use prover_setup::IvcSnarkProverSetup; diff --git a/mithril-stm/src/proof_system/ivc_halo2_snark/proof.rs b/mithril-stm/src/proof_system/ivc_halo2_snark/proof.rs index 025f460808f..ec834c02711 100644 --- a/mithril-stm/src/proof_system/ivc_halo2_snark/proof.rs +++ b/mithril-stm/src/proof_system/ivc_halo2_snark/proof.rs @@ -242,7 +242,7 @@ where /// `&[&[&[], public_inputs]]` (one circuit, one instance group, empty committed /// instance, then the field-element public inputs). Returns the finalised transcript /// bytes on success. - fn prove_with_transcript( + pub(crate) fn prove_with_transcript( srs: &ParamsKZG, proving_key: &RecursiveCircuitProvingKey, circuit_data: &IvcCircuitData, diff --git a/mithril-stm/src/proof_system/ivc_halo2_snark/verifier_setup.rs b/mithril-stm/src/proof_system/ivc_halo2_snark/verifier_setup.rs index 8cacde9f556..a701b2e28b5 100644 --- a/mithril-stm/src/proof_system/ivc_halo2_snark/verifier_setup.rs +++ b/mithril-stm/src/proof_system/ivc_halo2_snark/verifier_setup.rs @@ -95,10 +95,9 @@ impl IvcVerifierSetup { } /// Derive from an already-built [`IvcSnarkProverSetup`], extracting verifier params directly - /// from its SRS rather than the embedded constant. Only for tests — production code must - /// not load the full SRS just to verify a proof. - #[cfg(test)] - #[allow(dead_code)] + /// from its SRS rather than the embedded constant. For tests and benchmarks only + /// (`benchmark-internals`) — production code must not load the full SRS just to verify a proof. + #[cfg(any(test, feature = "benchmark-internals"))] pub(crate) fn from_ivc_setup_with_srs(ivc_setup: &IvcSnarkProverSetup) -> Self { let verifier_params = ivc_setup.srs.verifier_params(); Self {