From 60b4e2641be989eb883f89e2aa557576bd95d8da Mon Sep 17 00:00:00 2001 From: Hamza Jeljeli Date: Thu, 13 Aug 2026 17:54:51 +0900 Subject: [PATCH 1/9] refactor(stm): share the unsafe SRS cache with the IVC circuit test generators --- .../tests/common/generators/setup.rs | 21 ++++++++++++++++++- .../circuits/halo2_ivc/tests/common/mod.rs | 5 +++++ 2 files changed, 25 insertions(+), 1 deletion(-) 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 1012e33e571..0dea823f2bc 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 @@ -22,6 +22,8 @@ use crate::circuits::halo2_ivc::{ CERTIFICATE_FIXED_BASES_PREFIX, EmulatedCurve, IVC_FIXED_BASES_PREFIX, NativeField, PairingEngine, circuit::IvcCircuitData, state::Global, }; +use crate::circuits::test_utils::file_mutex::FileMutex; +use crate::circuits::trusted_setup::{TrustedSetupProvider, UNSAFE_SRS_SEED}; use crate::membership_commitment::{MerkleTree as StmMerkleTree, MerkleTreeSnarkLeaf}; use crate::signature_scheme::{ BaseFieldElement, SchnorrSigningKey, SchnorrVerificationKey, StandardSchnorrSignature, @@ -173,12 +175,29 @@ pub(crate) fn build_deterministic_params(circuit_degree: u32) -> ParamsKZG::unsafe_setup(circuit_degree, ChaCha20Rng::seed_from_u64(ASSET_SEED)) } +/// Loads the shared unsafe SRS of degree `circuit_degree` from the content-keyed test cache, +/// generating and persisting it on a miss. +/// +/// The cache entry is the one [`IvcSnarkProverSetup::build_for_test`] writes: both derive their SRS +/// from the same seed, so the bytes are identical and the generation cost is paid once per degree +/// across the whole test suite. The lock is released as soon as the parameters are loaded, so a +/// caller can then take a second cache lock without holding two at once. +fn load_shared_unsafe_srs(circuit_degree: u32) -> ParamsKZG { + let srs_cache = FileMutex::for_shared_cache("unsafe-srs", &[&UNSAFE_SRS_SEED.to_le_bytes()]); + let srs_directory = srs_cache.directory().to_path_buf(); + let _srs_cache_lock = srs_cache.lock().expect("the shared unsafe SRS cache should lock"); + + TrustedSetupProvider::with_unsafe_srs(&srs_directory, circuit_degree) + .get_trusted_setup_parameters() + .expect("the shared unsafe SRS should load from the test cache") +} + /// Builds the shared verifier-side recursive setup from the deterministic SRS. pub(crate) fn build_shared_recursive_context( setup: &AssetGenerationSetup, ) -> SharedRecursiveContext { let shared_srs_degree = RECURSIVE_CIRCUIT_DEGREE.max(CERTIFICATE_CIRCUIT_DEGREE); - let universal_kzg_parameters = build_deterministic_params(shared_srs_degree); + let universal_kzg_parameters = load_shared_unsafe_srs(shared_srs_degree); let universal_verifier_params = universal_kzg_parameters.verifier_params(); let params_for = |degree| { diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/common/mod.rs b/mithril-stm/src/circuits/halo2_ivc/tests/common/mod.rs index 0062e898714..ac182454c67 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/common/mod.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/common/mod.rs @@ -6,6 +6,11 @@ pub(crate) const ASSET_SEED: u64 = 42; pub(crate) const CERTIFICATE_CIRCUIT_DEGREE: u32 = 13; +// The committed assets are derived from an SRS generated with `ASSET_SEED`, and the generators read +// that SRS from the cache written under `UNSAFE_SRS_SEED`. The two must stay equal, otherwise the +// assets would silently be rebuilt from a different SRS. +const _: () = assert!(ASSET_SEED == crate::circuits::trusted_setup::UNSAFE_SRS_SEED); + pub(crate) mod asset_readers; pub(crate) mod field_encoding; pub(crate) mod generators; From 9586b7d310ec37f4db571e3a7555853d93465223 Mon Sep 17 00:00:00 2001 From: Hamza Jeljeli Date: Fri, 14 Aug 2026 08:42:46 +0900 Subject: [PATCH 2/9] refactor(stm): cache the recursive verifying key for the IVC circuit behavior tests --- .../halo2_ivc/tests/common/generators/mod.rs | 2 +- .../tests/common/generators/setup.rs | 283 +++++++++++++++++- .../halo2_ivc/tests/common/helpers.rs | 4 +- 3 files changed, 274 insertions(+), 15 deletions(-) diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/mod.rs b/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/mod.rs index c7aac3be36f..6a5ab304002 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/mod.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/mod.rs @@ -11,7 +11,7 @@ pub(crate) use proofs::{ }; pub(crate) use setup::{ AssetGenerationSetup, GENESIS_EPOCH, build_asset_generation_setup, build_recursive_fixed_bases, - build_recursive_global, build_shared_recursive_context, + build_recursive_global, build_shared_recursive_context_from_cache, }; pub(crate) use transitions::{ build_genesis_base_case_next_state, build_genesis_base_case_witness, 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 0dea823f2bc..3f04e80ed4a 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 @@ -1,4 +1,8 @@ -use std::{collections::BTreeMap, path::PathBuf}; +use std::{ + collections::BTreeMap, + io::Write, + path::{Path, PathBuf}, +}; use ff::Field; use midnight_curves::Bls12; @@ -20,10 +24,12 @@ use crate::circuits::halo2_ivc::keys::{RecursiveCircuitProvingKey, RecursiveCirc use crate::circuits::halo2_ivc::types::MessageHash; use crate::circuits::halo2_ivc::{ CERTIFICATE_FIXED_BASES_PREFIX, EmulatedCurve, IVC_FIXED_BASES_PREFIX, NativeField, - PairingEngine, circuit::IvcCircuitData, state::Global, + PairingEngine, RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, circuit::IvcCircuitData, + state::Global, }; use crate::circuits::test_utils::file_mutex::FileMutex; use crate::circuits::trusted_setup::{TrustedSetupProvider, UNSAFE_SRS_SEED}; +use crate::codec::{TryFromBytes, TryToBytes}; use crate::membership_commitment::{MerkleTree as StmMerkleTree, MerkleTreeSnarkLeaf}; use crate::signature_scheme::{ BaseFieldElement, SchnorrSigningKey, SchnorrVerificationKey, StandardSchnorrSignature, @@ -192,9 +198,111 @@ fn load_shared_unsafe_srs(circuit_degree: u32) -> ParamsKZG { .expect("the shared unsafe SRS should load from the test cache") } -/// Builds the shared verifier-side recursive setup from the deterministic SRS. +/// File holding the cached recursive verifying key inside its fingerprinted cache directory. +const RECURSIVE_VERIFYING_KEY_CACHE_FILE: &str = "recursive-verifying-key"; + +/// Where the expensive recursive verifying key comes from. +enum RecursiveVerifyingKeySource { + /// Always derived. Required wherever the result is written to a committed asset. + Derived, + /// Loaded from the content-keyed test cache, derived only on a miss. + Cached, +} + +/// Derives the recursive verifying key for the default IVC circuit shape (about 8.9 s). +fn derive_recursive_verifying_key( + recursive_commitment_parameters: &ParamsKZG, + certificate_verifying_key: &NonRecursiveCircuitVerifyingKey, +) -> RecursiveCircuitVerifyingKey { + let default_ivc_circuit = + IvcCircuitData::unknown(certificate_verifying_key).expect("valid IvcCircuitData unknown"); + RecursiveCircuitVerifyingKey::new( + keygen_vk_with_k( + recursive_commitment_parameters, + &default_ivc_circuit, + RECURSIVE_CIRCUIT_DEGREE, + ) + .expect("recursive verifying key generation should not fail"), + ) +} + +/// Reads `cache_file`, or builds the value and publishes it there on a miss. +/// +/// An absent file, a decode failure, or any byte difference on re-encoding counts as a miss and is +/// rebuilt rather than reported: a corrupt test cache must never fail a test run. This is +/// deliberately unlike [`KeyProvider`](crate::circuits::key_provider::KeyProvider), which +/// propagates deserialization errors. The re-encode comparison is what rejects trailing or +/// non-canonical bytes, since the verifying-key codec stops at the end of the key and ignores +/// whatever follows it. +fn load_or_build(cache_file: &Path, build: impl FnOnce() -> T) -> T { + if let Some(cached) = read_cache_file(cache_file) { + return cached; + } + + let value = build(); + store_cache_file(cache_file, &value); + value +} + +/// Returns the cached value, or `None` when the entry is absent, undecodable, or not byte-identical +/// to a re-encoding of what it decodes to. +fn read_cache_file(cache_file: &Path) -> Option { + let bytes = std::fs::read(cache_file).ok()?; + let value = T::try_from_bytes(&bytes).ok()?; + (value.to_bytes_vec().ok()? == bytes).then_some(value) +} + +/// Publishes `value` at `cache_file` durably: a per-process temporary sibling is written, fsynced, +/// then renamed, so a reader sees either no file or the whole value. +fn store_cache_file(cache_file: &Path, value: &T) { + let bytes = value.to_bytes_vec().expect("the cached value should serialize"); + let directory = cache_file + .parent() + .expect("the cache file should have a parent directory"); + std::fs::create_dir_all(directory).expect("the cache directory should be created"); + + let temporary_file = directory.join(format!( + "{RECURSIVE_VERIFYING_KEY_CACHE_FILE}.{}.temp", + std::process::id() + )); + let mut file = std::fs::File::create(&temporary_file).expect("the temporary file should open"); + file.write_all(&bytes).expect("the cached value should be written"); + file.sync_all().expect("the cached value should be flushed"); + drop(file); + std::fs::rename(&temporary_file, cache_file).expect("the cached value should be published"); + + // Makes the rename itself durable. Best effort: on platforms where a directory cannot be + // opened this is a no-op, and losing a test-cache entry only costs a rebuild. + let _ = std::fs::File::open(directory).and_then(|directory_file| directory_file.sync_all()); +} + +/// Builds the shared verifier-side recursive setup, **always deriving** the recursive verifying key. +/// +/// Asset generators must use this: they write committed assets, and a stale cached key would +/// silently produce assets derived from it. Behavior tests that only read should call +/// [`build_shared_recursive_context_from_cache`]. pub(crate) fn build_shared_recursive_context( setup: &AssetGenerationSetup, +) -> SharedRecursiveContext { + build_shared_recursive_context_with(setup, RecursiveVerifyingKeySource::Derived) +} + +/// Builds the shared verifier-side recursive setup, taking the recursive verifying key from the +/// content-keyed test cache when one is present. +/// +/// The cache address folds in the freshly derived certificate verifying key, the committed +/// production recursive key, both circuit degrees, and the SRS seed, so a change to the certificate +/// circuit or a regenerated production key resolves to a different entry. **Never call this from an +/// asset generator** — see [`build_shared_recursive_context`]. +pub(crate) fn build_shared_recursive_context_from_cache( + setup: &AssetGenerationSetup, +) -> SharedRecursiveContext { + build_shared_recursive_context_with(setup, RecursiveVerifyingKeySource::Cached) +} + +fn build_shared_recursive_context_with( + setup: &AssetGenerationSetup, + recursive_verifying_key_source: RecursiveVerifyingKeySource, ) -> SharedRecursiveContext { let shared_srs_degree = RECURSIVE_CIRCUIT_DEGREE.max(CERTIFICATE_CIRCUIT_DEGREE); let universal_kzg_parameters = load_shared_unsafe_srs(shared_srs_degree); @@ -213,20 +321,45 @@ pub(crate) fn build_shared_recursive_context( params_for(RECURSIVE_CIRCUIT_DEGREE), ); + // Derived on every call: at about 93 ms it is not worth caching, and its bytes are what make + // the recursive key's cache address sensitive to the certificate circuit. let certificate_verifying_key = NonRecursiveCircuitVerifyingKey::new(zk_lib::setup_vk( &certificate_commitment_parameters, &setup.certificate_relation, )); - let default_ivc_circuit = - IvcCircuitData::unknown(&certificate_verifying_key).expect("valid IvcCircuitData unknown"); - let recursive_verifying_key = RecursiveCircuitVerifyingKey::new( - keygen_vk_with_k( + + let recursive_verifying_key = match recursive_verifying_key_source { + RecursiveVerifyingKeySource::Derived => derive_recursive_verifying_key( &recursive_commitment_parameters, - &default_ivc_circuit, - RECURSIVE_CIRCUIT_DEGREE, - ) - .expect("recursive verifying key generation should not fail"), - ); + &certificate_verifying_key, + ), + RecursiveVerifyingKeySource::Cached => { + let certificate_verifying_key_bytes = certificate_verifying_key + .to_bytes_vec() + .expect("the certificate verifying key should serialize"); + let key_cache = FileMutex::for_shared_cache( + "ivc-recursive-verifying-key-v1", + &[ + &certificate_verifying_key_bytes, + RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, + &RECURSIVE_CIRCUIT_DEGREE.to_le_bytes(), + &CERTIFICATE_CIRCUIT_DEGREE.to_le_bytes(), + &UNSAFE_SRS_SEED.to_le_bytes(), + ], + ); + let cache_file = key_cache.directory().join(RECURSIVE_VERIFYING_KEY_CACHE_FILE); + let _key_cache_lock = key_cache + .lock() + .expect("the recursive verifying key cache should lock"); + + load_or_build(&cache_file, || { + derive_recursive_verifying_key( + &recursive_commitment_parameters, + &certificate_verifying_key, + ) + }) + } + }; SharedRecursiveContext { universal_kzg_parameters, @@ -367,3 +500,129 @@ pub(crate) fn build_asset_generation_setup() -> AssetGenerationSetup { genesis_next_protocol_parameters, } } + +#[cfg(test)] +mod tests { + use std::cell::Cell; + + use tempfile::TempDir; + + use super::*; + use crate::StmResult; + + /// Stand-in for a cached key: cheap to build, and its encoding is exact, so the tests exercise + /// the cache protocol itself rather than a verifying key's cost. + #[derive(Debug, PartialEq, Eq)] + struct CachedValue(Vec); + + impl TryToBytes for CachedValue { + fn to_bytes_vec(&self) -> StmResult> { + Ok(self.0.clone()) + } + } + + impl TryFromBytes for CachedValue { + fn try_from_bytes(bytes: &[u8]) -> StmResult { + if bytes.len() < 4 { + return Err(anyhow::anyhow!("a cached value is at least four bytes")); + } + // Decodes a fixed-width prefix and ignores the rest, mirroring the verifying-key codec + // that stops at the end of the key. + Ok(Self(bytes[..4].to_vec())) + } + } + + /// Builder that records how many times it ran, so a cache hit is observable without inspecting + /// or mutating key material. + struct CountingBuilder { + calls: Cell, + } + + impl CountingBuilder { + fn new() -> Self { + Self { + calls: Cell::new(0), + } + } + + fn build(&self) -> CachedValue { + self.calls.set(self.calls.get() + 1); + CachedValue(vec![1, 2, 3, 4]) + } + } + + fn cache_file_in(directory: &TempDir) -> PathBuf { + directory.path().join(RECURSIVE_VERIFYING_KEY_CACHE_FILE) + } + + #[test] + fn cold_cache_builds_the_value_and_publishes_it() { + let directory = TempDir::new().expect("temporary directory"); + let cache_file = cache_file_in(&directory); + let builder = CountingBuilder::new(); + + let value = load_or_build(&cache_file, || builder.build()); + + assert_eq!(builder.calls.get(), 1, "a cold cache must build once"); + assert_eq!(value, CachedValue(vec![1, 2, 3, 4])); + assert!(cache_file.exists(), "the entry must be published"); + } + + #[test] + fn warm_cache_returns_the_stored_value_without_building() { + let directory = TempDir::new().expect("temporary directory"); + let cache_file = cache_file_in(&directory); + let builder = CountingBuilder::new(); + + let first = load_or_build(&cache_file, || builder.build()); + let second = load_or_build(&cache_file, || builder.build()); + + assert_eq!(builder.calls.get(), 1, "a warm cache must not rebuild"); + assert_eq!(first, second); + } + + #[test] + fn truncated_entry_is_treated_as_a_miss() { + let directory = TempDir::new().expect("temporary directory"); + let cache_file = cache_file_in(&directory); + let builder = CountingBuilder::new(); + load_or_build(&cache_file, || builder.build()); + + std::fs::write(&cache_file, [1, 2]).expect("the entry should be truncated"); + let value = load_or_build(&cache_file, || builder.build()); + + assert_eq!(builder.calls.get(), 2, "a truncated entry must be rebuilt"); + assert_eq!(value, CachedValue(vec![1, 2, 3, 4])); + } + + #[test] + fn trailing_bytes_are_rejected_rather_than_ignored() { + let directory = TempDir::new().expect("temporary directory"); + let cache_file = cache_file_in(&directory); + let builder = CountingBuilder::new(); + load_or_build(&cache_file, || builder.build()); + + // The decoder itself would accept these bytes and silently ignore the tail; the re-encode + // comparison is what rejects them. + std::fs::write(&cache_file, [1, 2, 3, 4, 99]).expect("the entry should gain a tail"); + let value = load_or_build(&cache_file, || builder.build()); + + assert_eq!(builder.calls.get(), 2, "trailing bytes must be rebuilt"); + assert_eq!(value, CachedValue(vec![1, 2, 3, 4])); + } + + #[test] + fn distinct_fingerprints_resolve_to_distinct_entries() { + let seed_bytes = UNSAFE_SRS_SEED.to_le_bytes(); + let one = + FileMutex::for_shared_cache("ivc-recursive-verifying-key-v1", &[b"a", &seed_bytes]); + let other = + FileMutex::for_shared_cache("ivc-recursive-verifying-key-v1", &[b"b", &seed_bytes]); + + assert_ne!( + one.directory(), + other.directory(), + "a fingerprint change must resolve elsewhere" + ); + } +} diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/common/helpers.rs b/mithril-stm/src/circuits/halo2_ivc/tests/common/helpers.rs index ad5238d16bf..256f534dba2 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/common/helpers.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/common/helpers.rs @@ -32,7 +32,7 @@ use super::{ }, generators::{ AssetGenerationSetup, build_recursive_fixed_bases, build_recursive_global, - build_shared_recursive_context, certificate_public_inputs_for_step, + build_shared_recursive_context_from_cache, certificate_public_inputs_for_step, }, }; @@ -102,7 +102,7 @@ pub(crate) struct RecursiveMockProverSetup { pub(crate) fn build_recursive_mock_prover_setup( setup: &AssetGenerationSetup, ) -> RecursiveMockProverSetup { - let context = build_shared_recursive_context(setup); + let context = build_shared_recursive_context_from_cache(setup); let (certificate_fixed_bases, recursive_fixed_bases, combined_fixed_bases) = build_recursive_fixed_bases( &context.certificate_verifying_key, From 52946787c227a80f7d7d5ea3a7a708b32334e827 Mon Sep 17 00:00:00 2001 From: Hamza Jeljeli Date: Mon, 17 Aug 2026 09:07:12 +0900 Subject: [PATCH 3/9] refactor(stm): cache the certificate golden circuit keys on disk --- mithril-stm/src/circuits/halo2/errors.rs | 4 - mithril-stm/src/circuits/halo2/keys.rs | 5 - .../circuits/halo2/tests/golden/helpers.rs | 277 ++++++++++++++---- mithril-stm/src/circuits/test_utils/setup.rs | 5 +- 4 files changed, 220 insertions(+), 71 deletions(-) diff --git a/mithril-stm/src/circuits/halo2/errors.rs b/mithril-stm/src/circuits/halo2/errors.rs index 33f4eb3a164..b34518805c6 100644 --- a/mithril-stm/src/circuits/halo2/errors.rs +++ b/mithril-stm/src/circuits/halo2/errors.rs @@ -101,10 +101,6 @@ pub enum StmCircuitError { #[error("Failed to create params assets directory")] ParamsAssetsDirCreate, - /// In-memory circuit key cache lock is poisoned. - #[error("Circuit keys cache lock poisoned ({operation})")] - CircuitKeysCacheLockPoisoned { operation: &'static str }, - /// Signature generation failed while preparing witness. #[error("Signature generation failed")] SignatureGenerationFailed, diff --git a/mithril-stm/src/circuits/halo2/keys.rs b/mithril-stm/src/circuits/halo2/keys.rs index d84135098d7..bb62ced34a3 100644 --- a/mithril-stm/src/circuits/halo2/keys.rs +++ b/mithril-stm/src/circuits/halo2/keys.rs @@ -76,11 +76,6 @@ mod midnight_verifying_key_serde { } impl NonRecursiveCircuitProvingKey { - /// Wraps a Midnight proving key. - pub(crate) fn new(midnight_pk: MidnightPK) -> Self { - Self(midnight_pk) - } - /// Borrows the wrapped Midnight proving key, for proof generation. pub(crate) fn midnight_pk(&self) -> &MidnightPK { &self.0 diff --git a/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs b/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs index e9f3dc410ec..ab26b346bbc 100644 --- a/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs +++ b/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs @@ -1,8 +1,6 @@ -use std::collections::HashMap; use std::fs::create_dir_all; use std::panic::{self, AssertUnwindSafe}; use std::path::PathBuf; -use std::sync::{Arc, LazyLock, RwLock}; use std::time::Instant; use anyhow::{Context, anyhow}; @@ -15,6 +13,7 @@ use midnight_zk_stdlib::MidnightCircuit; use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; +use crate::circuits::halo2::NON_RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION; use crate::circuits::halo2::circuit::StmCertificateCircuit; use crate::circuits::halo2::errors::StmCircuitError; use crate::circuits::halo2::keys::{ @@ -25,7 +24,10 @@ use crate::circuits::halo2::witness::{ CircuitMerkleTreeLeaf, CircuitWitnessEntry, LotteryTargetValue as CircuitLotteryTargetValue, MerklePath, MerkleRoot, SignedMessageWithoutPrefix, }; +use crate::circuits::key_provider::KeyProvider; +use crate::circuits::test_utils::file_mutex::FileMutex; use crate::circuits::test_utils::setup::{generate_params, load_params}; +use crate::circuits::trusted_setup::UNSAFE_SRS_SEED; use crate::hash::poseidon::MidnightPoseidonDigest; use crate::membership_commitment::{ MerkleTree as StmMerkleTree, MerkleTreeSnarkLeaf as StmMerkleTreeSnarkLeaf, @@ -42,13 +44,11 @@ pub(crate) const LOTTERIES_PER_K: u32 = 10; /// Default message value used by golden test cases. const DEFAULT_TEST_MSG: u64 = 42; -/// Verification/proving key pair cached per STM circuit configuration. +/// Verification/proving key pair derived for an STM circuit configuration. type CircuitVerificationAndProvingKeyPair = ( NonRecursiveCircuitVerifyingKey, NonRecursiveCircuitProvingKey, ); -/// Cache map for verification/proving keys keyed by STM circuit configuration. -type CircuitKeysCache = HashMap>; fn checked_len_u32(actual: usize) -> u32 { u32::try_from(actual).unwrap_or(u32::MAX) @@ -93,15 +93,6 @@ fn validate_relation_for_setup(relation: &StmCertificateCircuit) -> StmResult<() .with_context(|| "Circuit parameter validation failed before setup") } -/// Cache key derived from the STM circuit configuration. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -struct StmCircuitConfig { - circuit_degree: u32, - k: u32, - m: u32, - merkle_tree_depth: u32, -} - /// Shared environment for STM circuit golden cases (SRS, relation, keys, sizing). pub(crate) struct StmCircuitEnv { /// Structured reference string used by the Halo2/KZG proving system. @@ -553,20 +544,13 @@ pub(crate) fn setup_stm_circuit_env( println!("{:?}", zk::cost_model(&relation, None)); } - let config = StmCircuitConfig { - circuit_degree, - k, - m, - merkle_tree_depth: depth, - }; - let key_pair = get_or_build_circuit_keys(config, &relation, &srs)?; - let (vk, pk) = (&key_pair.0, &key_pair.1); + let (vk, pk) = get_or_build_circuit_keys(&stm_params, depth, circuit_degree, &relation, &srs)?; Ok(StmCircuitEnv { srs, relation, - vk: vk.clone(), - pk: pk.clone(), + vk, + pk, num_signers, m, }) @@ -650,7 +634,11 @@ pub(crate) fn run_stm_circuit_case( fn load_or_generate_params(circuit_degree: u32) -> StmResult> { let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let assets_dir = manifest_dir.join("src").join("circuits").join("halo2").join("assets"); - let path = assets_dir.join(format!("params_kzg_unsafe_{}", circuit_degree)); + // The seed belongs in the name: a file identified only by degree would be reused after a seed + // change, pairing an old-seed SRS with keys cached under the new seed. + let path = assets_dir.join(format!( + "params_kzg_unsafe_degree_{circuit_degree}_seed_{UNSAFE_SRS_SEED}" + )); if path.exists() { return Ok(load_params( @@ -674,48 +662,76 @@ fn load_or_generate_params(circuit_degree: u32) -> StmResult> { )) } -/// Get cached verification/proving keys or build and insert them if missing. +/// Content-keyed cache entry holding the certificate keys derived from these inputs. +/// +/// Every input that changes the derived keys is a parameter, so the address is a pure function of +/// them and a test can vary each one: the committed production verifying key as a circuit-version +/// salt, the protocol parameters, the Merkle-tree depth, the circuit degree, and the seed pinning +/// the unsafe SRS. Distinct configurations therefore never share a directory, which is what lets +/// [`KeyProvider`] be built with no expected verifying key. +fn certificate_golden_key_cache( + production_verifying_key: &[u8], + parameters: &Parameters, + merkle_tree_depth: u32, + circuit_degree: u32, + unsafe_srs_seed: u64, +) -> StmResult { + Ok(FileMutex::for_shared_cache( + "certificate-golden-keys", + &[ + production_verifying_key, + ¶meters.to_bytes()?, + &merkle_tree_depth.to_le_bytes(), + &circuit_degree.to_le_bytes(), + &unsafe_srs_seed.to_le_bytes(), + ], + )) +} + +/// Loads the verification/proving key pair for this configuration from the on-disk cache, deriving +/// and storing it on a miss. +/// +/// The cache is shared across processes, unlike the in-process map this replaced, which amortized +/// nothing under the nextest process-per-test model. The lock is taken before the lookup so that +/// parallel processes racing a cold miss derive the pair once rather than once each — `KeyProvider` +/// does not serialize its writers. fn get_or_build_circuit_keys( - config: StmCircuitConfig, + parameters: &Parameters, + merkle_tree_depth: u32, + circuit_degree: u32, relation: &StmCertificateCircuit, srs: &ParamsKZG, -) -> StmResult> { - static STM_CIRCUIT_KEYS_CACHE: LazyLock> = - LazyLock::new(|| RwLock::new(HashMap::new())); - if let Some(key_pair) = STM_CIRCUIT_KEYS_CACHE - .read() - .map_err(|_| anyhow!(StmCircuitError::CircuitKeysCacheLockPoisoned { operation: "read" }))? - .get(&config) - .cloned() - { - return Ok(key_pair); - } +) -> StmResult { + let key_cache = certificate_golden_key_cache( + NON_RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, + parameters, + merkle_tree_depth, + circuit_degree, + UNSAFE_SRS_SEED, + )?; + let key_provider = KeyProvider::new( + key_cache.directory().to_path_buf(), + "non-recursive", + &[], + relation.clone(), + ); + let _key_cache_lock = key_cache.lock()?; let start = Instant::now(); - let (vk, pk) = panic::catch_unwind(AssertUnwindSafe(|| { - let vk = zk::setup_vk(srs, relation); - let pk = zk::setup_pk(relation, &vk); - (vk, pk) - })) - .map_err(|panic_payload| { - let details = panic_payload - .downcast_ref::<&str>() - .map(|message| (*message).to_owned()) - .or_else(|| panic_payload.downcast_ref::().cloned()) - .unwrap_or_else(|| "non-string panic payload".to_string()); - anyhow!("Midnight setup panicked before proving: {details}") - })?; - let duration = start.elapsed(); - println!("\nvk pk generation took: {:?}", duration); + // Midnight's setup panics rather than returning an error on some malformed relations; keep + // converting that into a test failure with the payload attached. + let key_pair = panic::catch_unwind(AssertUnwindSafe(|| key_provider.key_pair(srs))).map_err( + |panic_payload| { + let details = panic_payload + .downcast_ref::<&str>() + .map(|message| (*message).to_owned()) + .or_else(|| panic_payload.downcast_ref::().cloned()) + .unwrap_or_else(|| "non-string panic payload".to_string()); + anyhow!("Midnight setup panicked before proving: {details}") + }, + )??; + println!("\nvk pk load or generation took: {:?}", start.elapsed()); - let key_pair = Arc::new(( - NonRecursiveCircuitVerifyingKey::new(vk), - NonRecursiveCircuitProvingKey::new(pk), - )); - STM_CIRCUIT_KEYS_CACHE - .write() - .map_err(|_| anyhow!(StmCircuitError::CircuitKeysCacheLockPoisoned { operation: "write" }))? - .insert(config, key_pair.clone()); Ok(key_pair) } @@ -738,3 +754,142 @@ pub(crate) fn compute_unsafe_circuit_verification_key( .unwrap(); buf_cvk } + +#[cfg(test)] +mod tests { + use super::*; + + const BASELINE_PRODUCTION_KEY: &[u8] = b"production-verifying-key"; + const BASELINE_DEPTH: u32 = 12; + const BASELINE_DEGREE: u32 = 13; + const BASELINE_SEED: u64 = 42; + + fn parameters(k: u64, m: u64, phi_f: f64) -> Parameters { + Parameters { k, m, phi_f } + } + + fn baseline_parameters() -> Parameters { + parameters(3, 30, 0.2) + } + + fn cache_directory( + production_verifying_key: &[u8], + parameters: &Parameters, + merkle_tree_depth: u32, + circuit_degree: u32, + unsafe_srs_seed: u64, + ) -> PathBuf { + certificate_golden_key_cache( + production_verifying_key, + parameters, + merkle_tree_depth, + circuit_degree, + unsafe_srs_seed, + ) + .expect("the cache entry should resolve") + .directory() + .to_path_buf() + } + + fn baseline_cache_directory() -> PathBuf { + cache_directory( + BASELINE_PRODUCTION_KEY, + &baseline_parameters(), + BASELINE_DEPTH, + BASELINE_DEGREE, + BASELINE_SEED, + ) + } + + /// Exercises the production cache address with every input varied in turn, so dropping any input + /// from [`certificate_golden_key_cache`] makes this fail rather than silently sharing an entry. + #[test] + fn every_input_changes_the_cache_address() { + let baseline = baseline_cache_directory(); + + let variations = [ + ( + "production verifying key", + cache_directory( + b"a-different-production-verifying-key", + &baseline_parameters(), + BASELINE_DEPTH, + BASELINE_DEGREE, + BASELINE_SEED, + ), + ), + ( + "quorum size", + cache_directory( + BASELINE_PRODUCTION_KEY, + ¶meters(4, 30, 0.2), + BASELINE_DEPTH, + BASELINE_DEGREE, + BASELINE_SEED, + ), + ), + ( + "lottery count", + cache_directory( + BASELINE_PRODUCTION_KEY, + ¶meters(3, 40, 0.2), + BASELINE_DEPTH, + BASELINE_DEGREE, + BASELINE_SEED, + ), + ), + ( + "phi_f", + cache_directory( + BASELINE_PRODUCTION_KEY, + ¶meters(3, 30, 0.3), + BASELINE_DEPTH, + BASELINE_DEGREE, + BASELINE_SEED, + ), + ), + ( + "merkle tree depth", + cache_directory( + BASELINE_PRODUCTION_KEY, + &baseline_parameters(), + BASELINE_DEPTH + 1, + BASELINE_DEGREE, + BASELINE_SEED, + ), + ), + ( + "circuit degree", + cache_directory( + BASELINE_PRODUCTION_KEY, + &baseline_parameters(), + BASELINE_DEPTH, + BASELINE_DEGREE + 1, + BASELINE_SEED, + ), + ), + ( + "unsafe srs seed", + cache_directory( + BASELINE_PRODUCTION_KEY, + &baseline_parameters(), + BASELINE_DEPTH, + BASELINE_DEGREE, + BASELINE_SEED + 1, + ), + ), + ]; + + for (label, directory) in variations { + assert_ne!( + baseline, directory, + "a change of {label} must resolve to a different cache entry" + ); + } + } + + #[test] + fn the_same_configuration_resolves_to_the_same_cache_address() { + assert_eq!(baseline_cache_directory(), baseline_cache_directory()); + } +} diff --git a/mithril-stm/src/circuits/test_utils/setup.rs b/mithril-stm/src/circuits/test_utils/setup.rs index 5a0d25eb869..462b7d1e5e0 100644 --- a/mithril-stm/src/circuits/test_utils/setup.rs +++ b/mithril-stm/src/circuits/test_utils/setup.rs @@ -8,9 +8,12 @@ use rand_chacha::ChaCha20Rng; use rand_core::SeedableRng; use tempfile::NamedTempFile; +use crate::circuits::trusted_setup::UNSAFE_SRS_SEED; + pub(crate) fn generate_params(k: u32, path: &str, format: SerdeFormat) -> ParamsKZG { let parent = std::path::Path::new(path).parent().expect("No parent directory."); - let params: ParamsKZG = ParamsKZG::unsafe_setup(k, ChaCha20Rng::seed_from_u64(42)); + let params: ParamsKZG = + ParamsKZG::unsafe_setup(k, ChaCha20Rng::seed_from_u64(UNSAFE_SRS_SEED)); fs::create_dir_all(parent).expect("Failed to create the directories."); // Uses the name of the higher level test calling the function to create a temporary file // storing the srs and renames the file once it is done being written From 215990ae1cfe12fbe4fb9558ed02a168ca62aa35 Mon Sep 17 00:00:00 2001 From: Hamza Jeljeli Date: Mon, 17 Aug 2026 09:57:15 +0900 Subject: [PATCH 4/9] refactor(stm): cache the deterministic signer fixture for the IVC circuit tests --- .../halo2_ivc/tests/common/generators/mod.rs | 5 +- .../tests/common/generators/setup.rs | 496 +++++++++++++++--- .../halo2_ivc/tests/encoding/negative.rs | 10 +- .../halo2_ivc/tests/encoding/positive.rs | 6 +- .../halo2_ivc/tests/golden/positive.rs | 14 +- .../halo2_ivc/tests/in_circuit/accumulator.rs | 6 +- .../tests/in_circuit/genesis_gating.rs | 8 +- .../tests/in_circuit/public_inputs.rs | 10 +- .../tests/in_circuit/state_transition.rs | 8 +- .../tests/transitions/negative/genesis.rs | 6 +- .../tests/transitions/negative/next_epoch.rs | 9 +- .../tests/transitions/negative/same_epoch.rs | 8 +- .../src/proof_system/ivc_halo2_snark/proof.rs | 16 +- .../ivc_halo2_snark/prover_input.rs | 8 +- 14 files changed, 473 insertions(+), 137 deletions(-) diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/mod.rs b/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/mod.rs index 6a5ab304002..83abf4f11be 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/mod.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/mod.rs @@ -10,8 +10,9 @@ pub(crate) use proofs::{ try_verify_prepare_poseidon_ivc, verify_prepare_blake2b_ivc, verify_prepare_poseidon_ivc, }; pub(crate) use setup::{ - AssetGenerationSetup, GENESIS_EPOCH, build_asset_generation_setup, build_recursive_fixed_bases, - build_recursive_global, build_shared_recursive_context_from_cache, + AssetGenerationSetup, GENESIS_EPOCH, build_asset_generation_setup, + build_asset_generation_setup_from_cache, build_recursive_fixed_bases, build_recursive_global, + build_shared_recursive_context_from_cache, }; pub(crate) use transitions::{ build_genesis_base_case_next_state, build_genesis_base_case_witness, 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 3f04e80ed4a..3325dd48e6e 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 @@ -13,6 +13,7 @@ use midnight_proofs::{ use midnight_zk_stdlib as zk_lib; use rand_chacha::ChaCha20Rng; use rand_core::{CryptoRng, RngCore, SeedableRng}; +use serde::{Deserialize, Serialize}; use sha2::{Digest as Sha2Digest, Sha256}; use crate::AggregateVerificationKeyForSnark; @@ -29,12 +30,12 @@ use crate::circuits::halo2_ivc::{ }; use crate::circuits::test_utils::file_mutex::FileMutex; use crate::circuits::trusted_setup::{TrustedSetupProvider, UNSAFE_SRS_SEED}; -use crate::codec::{TryFromBytes, TryToBytes}; +use crate::codec::{TryFromBytes, TryToBytes, from_versioned_bytes, to_cbor_bytes}; use crate::membership_commitment::{MerkleTree as StmMerkleTree, MerkleTreeSnarkLeaf}; use crate::signature_scheme::{ BaseFieldElement, SchnorrSigningKey, SchnorrVerificationKey, StandardSchnorrSignature, }; -use crate::{MembershipDigest, MithrilMembershipDigest, Parameters}; +use crate::{MembershipDigest, MithrilMembershipDigest, Parameters, StmResult}; use super::super::field_encoding::jubjub_base_from_raw_le_bytes; use super::super::{ASSET_SEED, CERTIFICATE_CIRCUIT_DEGREE}; @@ -226,16 +227,77 @@ fn derive_recursive_verifying_key( ) } +/// Content-keyed cache entry holding the recursive verifying key derived from these inputs. +/// +/// Every input is a parameter, so the address is a pure function of them and a test can vary each +/// one: the freshly derived certificate verifying key (which tracks the certificate circuit), the +/// committed production recursive key as a circuit-version salt, both circuit degrees, and the seed +/// pinning the unsafe SRS. +fn recursive_verifying_key_cache( + certificate_verifying_key_bytes: &[u8], + production_recursive_verifying_key: &[u8], + recursive_circuit_degree: u32, + certificate_circuit_degree: u32, + unsafe_srs_seed: u64, +) -> FileMutex { + FileMutex::for_shared_cache( + "ivc-recursive-verifying-key-v1", + &[ + certificate_verifying_key_bytes, + production_recursive_verifying_key, + &recursive_circuit_degree.to_le_bytes(), + &certificate_circuit_degree.to_le_bytes(), + &unsafe_srs_seed.to_le_bytes(), + ], + ) +} + +/// Content-keyed cache entry holding the signer fixture built from these inputs. +/// +/// Every input is a parameter for the same reason as +/// [`recursive_verifying_key_cache`]: the address is testable input by input. +fn signer_fixture_cache( + signer_count: usize, + asset_seed: u64, + total_stake: u64, + genesis_epoch: u64, + genesis_next_protocol_parameters: u64, +) -> FileMutex { + FileMutex::for_shared_cache( + "ivc-signer-fixture-v1", + &[ + &signer_count.to_le_bytes(), + &asset_seed.to_le_bytes(), + &total_stake.to_le_bytes(), + &genesis_epoch.to_le_bytes(), + &genesis_next_protocol_parameters.to_le_bytes(), + ], + ) +} + /// Reads `cache_file`, or builds the value and publishes it there on a miss. /// -/// An absent file, a decode failure, or any byte difference on re-encoding counts as a miss and is -/// rebuilt rather than reported: a corrupt test cache must never fail a test run. This is -/// deliberately unlike [`KeyProvider`](crate::circuits::key_provider::KeyProvider), which -/// propagates deserialization errors. The re-encode comparison is what rejects trailing or -/// non-canonical bytes, since the verifying-key codec stops at the end of the key and ignores -/// whatever follows it. -fn load_or_build(cache_file: &Path, build: impl FnOnce() -> T) -> T { - if let Some(cached) = read_cache_file(cache_file) { +/// Rebuilds from exactly five conditions: an absent file, an unreadable one, a decode failure, bytes +/// that are not the canonical encoding of what they decode to (which covers truncation and trailing +/// bytes, since the verifying-key codec stops at the end of the key and ignores whatever follows), +/// and a rejection by `is_valid`. **It makes no wider claim: a canonically encoded value that +/// satisfies `is_valid` is trusted.** Canonical encoding shows the bytes are self-consistent, not +/// who wrote them, so an entry that is well-formed but semantically wrong is accepted — acceptable +/// only because this cache is disposable, is not exposed to a hostile writer, and is never read by +/// anything that produces committed assets. +/// +/// Rebuilding rather than reporting is deliberate, and unlike +/// [`KeyProvider`](crate::circuits::key_provider::KeyProvider), which propagates deserialization +/// errors: a spoiled test cache must not fail a test run. +/// +/// `is_valid` carries any invariant the bytes alone cannot express. Values whose encoding is +/// self-contained pass `|_| true`. +fn load_or_build( + cache_file: &Path, + is_valid: impl FnOnce(&T) -> bool, + build: impl FnOnce() -> T, +) -> T { + if let Some(cached) = read_cache_file(cache_file).filter(is_valid) { return cached; } @@ -261,10 +323,11 @@ fn store_cache_file(cache_file: &Path, value: &T) { .expect("the cache file should have a parent directory"); std::fs::create_dir_all(directory).expect("the cache directory should be created"); - let temporary_file = directory.join(format!( - "{RECURSIVE_VERIFYING_KEY_CACHE_FILE}.{}.temp", - std::process::id() - )); + let file_name = cache_file + .file_name() + .expect("the cache file should have a name") + .to_string_lossy(); + let temporary_file = directory.join(format!("{file_name}.{}.temp", std::process::id())); let mut file = std::fs::File::create(&temporary_file).expect("the temporary file should open"); file.write_all(&bytes).expect("the cached value should be written"); file.sync_all().expect("the cached value should be flushed"); @@ -337,27 +400,29 @@ fn build_shared_recursive_context_with( let certificate_verifying_key_bytes = certificate_verifying_key .to_bytes_vec() .expect("the certificate verifying key should serialize"); - let key_cache = FileMutex::for_shared_cache( - "ivc-recursive-verifying-key-v1", - &[ - &certificate_verifying_key_bytes, - RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, - &RECURSIVE_CIRCUIT_DEGREE.to_le_bytes(), - &CERTIFICATE_CIRCUIT_DEGREE.to_le_bytes(), - &UNSAFE_SRS_SEED.to_le_bytes(), - ], + let key_cache = recursive_verifying_key_cache( + &certificate_verifying_key_bytes, + RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, + RECURSIVE_CIRCUIT_DEGREE, + CERTIFICATE_CIRCUIT_DEGREE, + UNSAFE_SRS_SEED, ); let cache_file = key_cache.directory().join(RECURSIVE_VERIFYING_KEY_CACHE_FILE); let _key_cache_lock = key_cache .lock() .expect("the recursive verifying key cache should lock"); - load_or_build(&cache_file, || { - derive_recursive_verifying_key( - &recursive_commitment_parameters, - &certificate_verifying_key, - ) - }) + load_or_build( + &cache_file, + // A verifying key is self-describing: the byte round trip is the whole check. + |_| true, + || { + derive_recursive_verifying_key( + &recursive_commitment_parameters, + &certificate_verifying_key, + ) + }, + ) } }; @@ -427,25 +492,58 @@ pub(crate) fn build_recursive_global( ) } -/// Builds the deterministic shared setup used by all asset generators. -pub(crate) fn build_asset_generation_setup() -> AssetGenerationSetup { - let mut rng = ChaCha20Rng::seed_from_u64(ASSET_SEED); +/// Value committed by the genesis message as the next protocol parameters. +const GENESIS_NEXT_PROTOCOL_PARAMETERS: u64 = 7; - let depth = SIGNER_COUNT.next_power_of_two().trailing_zeros(); - let number_of_lotteries = QUORUM_SIZE * 10; - let total_stake = TOTAL_STAKE; +/// File holding the cached signer fixture inside its fingerprinted cache directory. +const SIGNER_FIXTURE_CACHE_FILE: &str = "signer-fixture"; - let certificate_relation = StmCertificateCircuit::try_new( - &Parameters { - k: QUORUM_SIZE as u64, - m: number_of_lotteries as u64, - phi_f: 0.2, - }, - depth, - ) - .expect("certificate relation construction should not fail"); - let (signing_keys, merkle_tree_leaves, merkle_tree) = build_merkle_tree(&mut rng, SIGNER_COUNT); - let genesis_next_merkle_tree_commitment = merkle_tree_commitment_from_stm_tree(&merkle_tree); +/// The random-generator-derived half of [`AssetGenerationSetup`]. +/// +/// The builder threads one seeded generator through the signer keys **and then** the genesis signing +/// key and signature, so a cache that restored only the tree would leave the generator at a different +/// position and silently change the genesis material — which the committed assets embed. Everything +/// drawn from the generator is therefore cached together; every other field of +/// [`AssetGenerationSetup`] is a pure function of these values and the module constants, and is +/// recomputed on both paths. +#[derive(Serialize, Deserialize)] +struct CachedSignerFixture { + signing_keys: Vec, + merkle_tree_leaves: Vec, + merkle_tree: SignerRegistrationMerkleTree, + genesis_verification_key: SchnorrVerificationKey, + genesis_signature: StandardSchnorrSignature, +} + +impl TryToBytes for CachedSignerFixture { + fn to_bytes_vec(&self) -> StmResult> { + to_cbor_bytes(self) + } +} + +impl TryFromBytes for CachedSignerFixture { + fn try_from_bytes(bytes: &[u8]) -> StmResult { + from_versioned_bytes(bytes, |_| { + Err(anyhow::anyhow!( + "the cached signer fixture is not in the current codec version" + )) + }) + } +} + +/// The genesis values every consumer needs, all derived from the signer Merkle tree and constants. +struct DerivedGenesisData { + aggregate_verification_key: AggregateVerificationKeyForSnark, + genesis_next_merkle_tree_commitment: NativeField, + genesis_next_protocol_parameters: NativeField, + genesis_message: NativeField, +} + +/// Recomputes the derived genesis values from the signer Merkle tree. +/// +/// Used on both the cached and uncached paths, so the two can never disagree. +fn derive_genesis_data(merkle_tree: &SignerRegistrationMerkleTree) -> DerivedGenesisData { + let genesis_next_merkle_tree_commitment = merkle_tree_commitment_from_stm_tree(merkle_tree); let aggregate_verification_key = { let commitment = merkle_tree.to_merkle_tree_commitment(); @@ -455,22 +553,18 @@ pub(crate) fn build_asset_generation_setup() -> AssetGenerationSetup { // production-compatible message-part format. let mut avk_input = [0u8; 40]; avk_input[0..32].copy_from_slice(&commitment.root); - avk_input[32..40].copy_from_slice(&total_stake.to_be_bytes()); + avk_input[32..40].copy_from_slice(&TOTAL_STAKE.to_be_bytes()); AggregateVerificationKeyForSnark::::from_bytes(&avk_input) .expect("deterministic aggregate verification key should decode") }; - let genesis_signing_key = SchnorrSigningKey::generate(&mut rng); - let genesis_verification_key = - SchnorrVerificationKey::new_from_signing_key(genesis_signing_key.clone()); - let genesis_epoch = GENESIS_EPOCH; - let genesis_next_protocol_parameters = NativeField::from(7u64); + let genesis_next_protocol_parameters = NativeField::from(GENESIS_NEXT_PROTOCOL_PARAMETERS); let genesis_message = { let protocol_message = build_genesis_protocol_message( &aggregate_verification_key, genesis_next_protocol_parameters.to_bytes_le(), - genesis_epoch, + GENESIS_EPOCH, ); let preimage = protocol_message .try_rigid_preimage() @@ -479,28 +573,126 @@ pub(crate) fn build_asset_generation_setup() -> AssetGenerationSetup { jubjub_base_from_raw_le_bytes(message_hash.as_ref()) }; - let genesis_message_base = BaseFieldElement::from(genesis_message); + DerivedGenesisData { + aggregate_verification_key, + genesis_next_merkle_tree_commitment, + genesis_next_protocol_parameters, + genesis_message, + } +} + +/// Runs the deterministic generator sequence: signer keys and tree first, then the genesis key and +/// its signature over the message derived from that tree. +fn build_signer_fixture() -> CachedSignerFixture { + let mut random_generator = ChaCha20Rng::seed_from_u64(ASSET_SEED); + let (signing_keys, merkle_tree_leaves, merkle_tree) = + build_merkle_tree(&mut random_generator, SIGNER_COUNT); + + let genesis_signing_key = SchnorrSigningKey::generate(&mut random_generator); + let genesis_verification_key = + SchnorrVerificationKey::new_from_signing_key(genesis_signing_key.clone()); + + let genesis_message_base = + BaseFieldElement::from(derive_genesis_data(&merkle_tree).genesis_message); let genesis_signature = genesis_signing_key - .sign_standard(&[genesis_message_base], &mut rng) + .sign_standard(&[genesis_message_base], &mut random_generator) .expect("deterministic genesis signature should be produced"); genesis_signature .verify(&[genesis_message_base], &genesis_verification_key) .expect("deterministic genesis signature should verify"); - AssetGenerationSetup { - certificate_relation, + CachedSignerFixture { + signing_keys, + merkle_tree_leaves, + merkle_tree, genesis_verification_key, - genesis_message: MessageHash::from_field(genesis_message), genesis_signature, - merkle_tree, - merkle_tree_leaves, - signing_keys, - aggregate_verification_key, - genesis_next_merkle_tree_commitment, - genesis_next_protocol_parameters, } } +/// Whether a decoded fixture is internally consistent. +/// +/// The signature check is the strong one: it is made over the genesis message derived from the +/// cached tree, so a tampered tree, a tampered key or a tampered signature all fail it. +fn signer_fixture_is_valid(fixture: &CachedSignerFixture) -> bool { + if fixture.signing_keys.len() != SIGNER_COUNT + || fixture.merkle_tree_leaves.len() != SIGNER_COUNT + { + return false; + } + + let genesis_message_base = + BaseFieldElement::from(derive_genesis_data(&fixture.merkle_tree).genesis_message); + fixture + .genesis_signature + .verify(&[genesis_message_base], &fixture.genesis_verification_key) + .is_ok() +} + +/// Assembles the full setup around a signer fixture, deriving everything that is a pure function of +/// it and the module constants. +fn assemble_asset_generation_setup(fixture: CachedSignerFixture) -> AssetGenerationSetup { + let depth = SIGNER_COUNT.next_power_of_two().trailing_zeros(); + let number_of_lotteries = QUORUM_SIZE * 10; + + // Rebuilt on every call: it is derived from constants, not from the random generator. + let certificate_relation = StmCertificateCircuit::try_new( + &Parameters { + k: QUORUM_SIZE as u64, + m: number_of_lotteries as u64, + phi_f: 0.2, + }, + depth, + ) + .expect("certificate relation construction should not fail"); + + let derived = derive_genesis_data(&fixture.merkle_tree); + + AssetGenerationSetup { + certificate_relation, + genesis_verification_key: fixture.genesis_verification_key, + genesis_message: MessageHash::from_field(derived.genesis_message), + genesis_signature: fixture.genesis_signature, + merkle_tree: fixture.merkle_tree, + merkle_tree_leaves: fixture.merkle_tree_leaves, + signing_keys: fixture.signing_keys, + aggregate_verification_key: derived.aggregate_verification_key, + genesis_next_merkle_tree_commitment: derived.genesis_next_merkle_tree_commitment, + genesis_next_protocol_parameters: derived.genesis_next_protocol_parameters, + } +} + +/// Builds the deterministic asset-generation setup, **always running the generator sequence**. +/// +/// Asset writers and the committed-fixture drift guard must use this: they produce or check committed +/// bytes, and a stale cached fixture would let them do so from outdated signer data. Behavior tests +/// that only consume the setup should call [`build_asset_generation_setup_from_cache`]. +pub(crate) fn build_asset_generation_setup() -> AssetGenerationSetup { + assemble_asset_generation_setup(build_signer_fixture()) +} + +/// Builds the deterministic asset-generation setup, taking the signer fixture from the content-keyed +/// test cache when a valid one is present. +/// +/// **Never call this from an asset writer** — see [`build_asset_generation_setup`]. +pub(crate) fn build_asset_generation_setup_from_cache() -> AssetGenerationSetup { + let fixture_cache = signer_fixture_cache( + SIGNER_COUNT, + ASSET_SEED, + TOTAL_STAKE, + GENESIS_EPOCH, + GENESIS_NEXT_PROTOCOL_PARAMETERS, + ); + let cache_file = fixture_cache.directory().join(SIGNER_FIXTURE_CACHE_FILE); + let fixture = { + let _fixture_cache_lock = + fixture_cache.lock().expect("the signer fixture cache should lock"); + load_or_build(&cache_file, signer_fixture_is_valid, build_signer_fixture) + }; + + assemble_asset_generation_setup(fixture) +} + #[cfg(test)] mod tests { use std::cell::Cell; @@ -508,7 +700,6 @@ mod tests { use tempfile::TempDir; use super::*; - use crate::StmResult; /// Stand-in for a cached key: cheap to build, and its encoding is exact, so the tests exercise /// the cache protocol itself rather than a verifying key's cost. @@ -561,7 +752,7 @@ mod tests { let cache_file = cache_file_in(&directory); let builder = CountingBuilder::new(); - let value = load_or_build(&cache_file, || builder.build()); + let value = load_or_build(&cache_file, |_| true, || builder.build()); assert_eq!(builder.calls.get(), 1, "a cold cache must build once"); assert_eq!(value, CachedValue(vec![1, 2, 3, 4])); @@ -574,22 +765,40 @@ mod tests { let cache_file = cache_file_in(&directory); let builder = CountingBuilder::new(); - let first = load_or_build(&cache_file, || builder.build()); - let second = load_or_build(&cache_file, || builder.build()); + let first = load_or_build(&cache_file, |_| true, || builder.build()); + let second = load_or_build(&cache_file, |_| true, || builder.build()); assert_eq!(builder.calls.get(), 1, "a warm cache must not rebuild"); assert_eq!(first, second); } + #[test] + fn a_value_rejected_by_the_validator_is_treated_as_a_miss() { + let directory = TempDir::new().expect("temporary directory"); + let cache_file = cache_file_in(&directory); + let builder = CountingBuilder::new(); + load_or_build(&cache_file, |_| true, || builder.build()); + + // The bytes are intact and decode cleanly; only the semantic check rejects them. + let value = load_or_build(&cache_file, |_| false, || builder.build()); + + assert_eq!( + builder.calls.get(), + 2, + "a value failing validation must be rebuilt" + ); + assert_eq!(value, CachedValue(vec![1, 2, 3, 4])); + } + #[test] fn truncated_entry_is_treated_as_a_miss() { let directory = TempDir::new().expect("temporary directory"); let cache_file = cache_file_in(&directory); let builder = CountingBuilder::new(); - load_or_build(&cache_file, || builder.build()); + load_or_build(&cache_file, |_| true, || builder.build()); std::fs::write(&cache_file, [1, 2]).expect("the entry should be truncated"); - let value = load_or_build(&cache_file, || builder.build()); + let value = load_or_build(&cache_file, |_| true, || builder.build()); assert_eq!(builder.calls.get(), 2, "a truncated entry must be rebuilt"); assert_eq!(value, CachedValue(vec![1, 2, 3, 4])); @@ -600,29 +809,148 @@ mod tests { let directory = TempDir::new().expect("temporary directory"); let cache_file = cache_file_in(&directory); let builder = CountingBuilder::new(); - load_or_build(&cache_file, || builder.build()); + load_or_build(&cache_file, |_| true, || builder.build()); // The decoder itself would accept these bytes and silently ignore the tail; the re-encode // comparison is what rejects them. std::fs::write(&cache_file, [1, 2, 3, 4, 99]).expect("the entry should gain a tail"); - let value = load_or_build(&cache_file, || builder.build()); + let value = load_or_build(&cache_file, |_| true, || builder.build()); assert_eq!(builder.calls.get(), 2, "trailing bytes must be rebuilt"); assert_eq!(value, CachedValue(vec![1, 2, 3, 4])); } + /// The real validator, not the generic hook: a freshly built fixture must be accepted. #[test] - fn distinct_fingerprints_resolve_to_distinct_entries() { - let seed_bytes = UNSAFE_SRS_SEED.to_le_bytes(); - let one = - FileMutex::for_shared_cache("ivc-recursive-verifying-key-v1", &[b"a", &seed_bytes]); - let other = - FileMutex::for_shared_cache("ivc-recursive-verifying-key-v1", &[b"b", &seed_bytes]); - - assert_ne!( - one.directory(), - other.directory(), - "a fingerprint change must resolve elsewhere" - ); + fn a_freshly_built_signer_fixture_is_valid() { + assert!(signer_fixture_is_valid(&build_signer_fixture())); + } + + #[test] + fn a_signer_fixture_with_wrong_vector_lengths_is_rejected() { + let mut fixture = build_signer_fixture(); + fixture.signing_keys.pop(); + assert!(!signer_fixture_is_valid(&fixture), "short signing keys"); + + let mut fixture = build_signer_fixture(); + fixture.merkle_tree_leaves.pop(); + assert!(!signer_fixture_is_valid(&fixture), "short leaves"); + } + + #[test] + fn a_signer_fixture_with_a_changed_tree_is_rejected() { + let mut fixture = build_signer_fixture(); + // A different tree over the same signers changes the root, so the genesis message derived + // from it no longer matches the one the cached signature was made over. + let mut reordered_leaves = fixture.merkle_tree_leaves.clone(); + reordered_leaves.reverse(); + fixture.merkle_tree = SignerRegistrationMerkleTree::new(&reordered_leaves); + + assert!(!signer_fixture_is_valid(&fixture)); + } + + #[test] + fn a_signer_fixture_with_a_changed_genesis_key_or_signature_is_rejected() { + let mut random_generator = ChaCha20Rng::seed_from_u64(ASSET_SEED + 1); + let other_signing_key = SchnorrSigningKey::generate(&mut random_generator); + + let mut fixture = build_signer_fixture(); + fixture.genesis_verification_key = + SchnorrVerificationKey::new_from_signing_key(other_signing_key.clone()); + assert!(!signer_fixture_is_valid(&fixture), "changed genesis key"); + + let mut fixture = build_signer_fixture(); + let unrelated_message = BaseFieldElement::from(NativeField::from(1u64)); + fixture.genesis_signature = other_signing_key + .sign_standard(&[unrelated_message], &mut random_generator) + .expect("a signature over an unrelated message should be produced"); + assert!(!signer_fixture_is_valid(&fixture), "changed signature"); + } + + /// Exercises the production cache address, so dropping an input from + /// [`recursive_verifying_key_cache`] makes this fail rather than silently sharing an entry. + #[test] + fn every_recursive_verifying_key_cache_input_changes_the_address() { + let directory = |certificate_key: &[u8], + production_key: &[u8], + recursive_degree: u32, + certificate_degree: u32, + seed: u64| { + recursive_verifying_key_cache( + certificate_key, + production_key, + recursive_degree, + certificate_degree, + seed, + ) + .directory() + .to_path_buf() + }; + let baseline = directory(b"certificate-key", b"production-key", 19, 13, 42); + + for (label, varied) in [ + ( + "certificate verifying key", + directory(b"another-certificate-key", b"production-key", 19, 13, 42), + ), + ( + "production recursive key", + directory(b"certificate-key", b"another-production-key", 19, 13, 42), + ), + ( + "recursive circuit degree", + directory(b"certificate-key", b"production-key", 20, 13, 42), + ), + ( + "certificate circuit degree", + directory(b"certificate-key", b"production-key", 19, 14, 42), + ), + ( + "unsafe srs seed", + directory(b"certificate-key", b"production-key", 19, 13, 43), + ), + ] { + assert_ne!( + baseline, varied, + "a change of {label} must resolve to a different cache entry" + ); + } + } + + /// Same guard for the signer fixture address. + #[test] + fn every_signer_fixture_cache_input_changes_the_address() { + let directory = |signer_count: usize, + seed: u64, + total_stake: u64, + genesis_epoch: u64, + next_protocol_parameters: u64| { + signer_fixture_cache( + signer_count, + seed, + total_stake, + genesis_epoch, + next_protocol_parameters, + ) + .directory() + .to_path_buf() + }; + let baseline = directory(3000, 42, 1_000_000, 5, 7); + + for (label, varied) in [ + ("signer count", directory(3001, 42, 1_000_000, 5, 7)), + ("asset seed", directory(3000, 43, 1_000_000, 5, 7)), + ("total stake", directory(3000, 42, 1_000_001, 5, 7)), + ("genesis epoch", directory(3000, 42, 1_000_000, 6, 7)), + ( + "next protocol parameters", + directory(3000, 42, 1_000_000, 5, 8), + ), + ] { + assert_ne!( + baseline, varied, + "a change of {label} must resolve to a different cache entry" + ); + } } } diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/encoding/negative.rs b/mithril-stm/src/circuits/halo2_ivc/tests/encoding/negative.rs index 70ea84e5b77..511a709153b 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/encoding/negative.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/encoding/negative.rs @@ -14,8 +14,8 @@ use crate::circuits::halo2_ivc::{ load_embedded_next_epoch_step_output_asset, load_embedded_verification_context_asset, }, generators::{ - GENESIS_EPOCH, build_asset_generation_setup, build_genesis_base_case_next_state, - build_genesis_base_case_witness, + GENESIS_EPOCH, build_asset_generation_setup_from_cache, + build_genesis_base_case_next_state, build_genesis_base_case_witness, }, helpers::{ assert_recursive_mock_prover_rejects_with_label, build_mock_prover_public_inputs, @@ -198,7 +198,7 @@ mod slow { fn circuit_rejects_wrong_next_merkle_tree_commitment_byte_range() { // MockProver constraint check: filling PREIMAGE_NEXT_MERKLE_TREE_COMMITMENT_BYTES with 0xff // must violate the in-circuit byte-extraction constraint for that preimage region. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let next_state = build_genesis_base_case_next_state(&setup, GENESIS_EPOCH); let public_inputs = build_mock_prover_public_inputs(&mock_prover_setup, &next_state); @@ -219,7 +219,7 @@ mod slow { fn circuit_rejects_wrong_next_protocol_parameters_byte_range() { // MockProver constraint check: filling PREIMAGE_NEXT_PROTOCOL_PARAMETERS_BYTES with 0xff // must violate the in-circuit byte-extraction constraint for that preimage region. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let next_state = build_genesis_base_case_next_state(&setup, GENESIS_EPOCH); let public_inputs = build_mock_prover_public_inputs(&mock_prover_setup, &next_state); @@ -239,7 +239,7 @@ mod slow { fn circuit_rejects_wrong_current_epoch_byte_range() { // MockProver constraint check: filling PREIMAGE_CURRENT_EPOCH_BYTES with 0xff // must violate the in-circuit byte-extraction constraint for that preimage region. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let next_state = build_genesis_base_case_next_state(&setup, GENESIS_EPOCH); let public_inputs = build_mock_prover_public_inputs(&mock_prover_setup, &next_state); diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/encoding/positive.rs b/mithril-stm/src/circuits/halo2_ivc/tests/encoding/positive.rs index 5d3b639e83b..df9ba72324e 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/encoding/positive.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/encoding/positive.rs @@ -18,7 +18,9 @@ use crate::circuits::halo2_ivc::{ load_embedded_next_epoch_step_output_asset, load_embedded_recursive_chain_state_asset, load_embedded_verification_context_asset, }, - generators::{build_asset_generation_setup, build_genesis_protocol_message_preimage}, + generators::{ + build_asset_generation_setup_from_cache, build_genesis_protocol_message_preimage, + }, helpers::build_recursive_proof_accumulator_from_assets, }, types::{EpochNumber, MerkleTreeCommitment, MessageHash, ProtocolParametersHash, StepCounter}, @@ -86,7 +88,7 @@ fn preimage_length_is_190_bytes() { // Off-circuit check that the genesis protocol message serializer produces // exactly PREIMAGE_SIZE bytes, matching the fixed byte ranges the circuit // reads for next_merkle_tree_commitment, next_protocol_parameters, and current_epoch. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let preimage = build_genesis_protocol_message_preimage(&setup); assert_eq!(preimage.len(), PREIMAGE_SIZE); } 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 81ce0646c81..2f3b68ea721 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/golden/positive.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/golden/positive.rs @@ -15,9 +15,10 @@ use crate::circuits::halo2_ivc::tests::common::{ 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, build_genesis_protocol_message_preimage, - next_message_and_preimage_for_step, next_state_for_step, + GENESIS_EPOCH, build_asset_generation_setup, build_asset_generation_setup_from_cache, + build_genesis_base_case_next_state, 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, @@ -102,7 +103,8 @@ fn recursive_step_output_asset_proof_and_accumulator_are_valid() { 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`. + // signature. Fails loudly if the committed `.bin` drifts from the deterministic generator. + // Builds fresh on purpose: a drift guard must not read the fixture cache it is guarding. let setup = build_asset_generation_setup(); let fixture = load_embedded_genesis_benchmark_fixture().expect("genesis benchmark fixture should load"); @@ -151,7 +153,7 @@ mod slow { // `recursive_chain_state_asset_proof_and_accumulator_are_valid` and // `recursive_step_output_asset_proof_and_accumulator_are_valid` above — a valid // full proof implies all constraints held when the proof was generated. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let next_state = build_genesis_base_case_next_state(&setup, GENESIS_EPOCH); let ivc_circuit_data = build_trivial_mock_prover_circuit( @@ -173,7 +175,7 @@ mod slow { // committed recursive_step_output asset matches an independent recomputation // of the next state and folded accumulator, confirming the generator is // deterministic and self-consistent. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_recursive_mock_prover_setup(&setup); let recursive_chain_state = load_embedded_recursive_chain_state_asset() .expect("recursive chain state asset should load"); diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/accumulator.rs b/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/accumulator.rs index 269564e12f3..2ce40d7b1fe 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/accumulator.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/accumulator.rs @@ -95,7 +95,9 @@ mod slow { circuit::IvcCircuitData, tests::common::{ asset_readers::load_embedded_recursive_chain_state_asset, - generators::{build_asset_generation_setup, build_same_epoch_certificate_asset_data}, + generators::{ + build_asset_generation_setup_from_cache, build_same_epoch_certificate_asset_data, + }, helpers::{ assert_recursive_mock_prover_rejects, build_recursive_mock_prover_setup, compute_expected_next_accumulator, @@ -108,7 +110,7 @@ mod slow { // MockProver check that the in-circuit accumulator update constraint holds for // a same-epoch step; substituting a wrong next_accumulator in the public inputs // causes MockProver to detect the arithmetic constraint violation. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_recursive_mock_prover_setup(&setup); let recursive_chain_state = load_embedded_recursive_chain_state_asset() diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/genesis_gating.rs b/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/genesis_gating.rs index 626c360121e..2e858e11792 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/genesis_gating.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/genesis_gating.rs @@ -13,8 +13,8 @@ use crate::circuits::halo2_ivc::{ state::State, tests::common::{ generators::{ - GENESIS_EPOCH, build_asset_generation_setup, build_genesis_base_case_next_state, - build_genesis_base_case_witness, + GENESIS_EPOCH, build_asset_generation_setup_from_cache, + build_genesis_base_case_next_state, build_genesis_base_case_witness, }, helpers::{ assert_recursive_mock_prover_accepts_with_label, build_mock_prover_setup_from_assets, @@ -31,7 +31,7 @@ mod slow { // MockProver constraint check: at genesis (step_counter = 0) the circuit gates the // certificate accumulator contribution to the group identity via scale_by_bit(0, acc), // so 64 garbage bytes in the certificate slot must not violate any constraint. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let public_inputs = [ mock_prover_setup.global.as_public_input(), @@ -62,7 +62,7 @@ mod slow { // MockProver constraint check: at genesis (step_counter = 0) the circuit gates the // IVC accumulator contribution to the group identity via scale_by_bit(0, acc), // so 64 garbage bytes in the IVC slot must not violate any constraint. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let public_inputs = [ mock_prover_setup.global.as_public_input(), diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/public_inputs.rs b/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/public_inputs.rs index ed4aa8a2ab5..a2d32b4a987 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/public_inputs.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/public_inputs.rs @@ -12,8 +12,8 @@ use crate::circuits::halo2_ivc::{ load_embedded_genesis_step_output_asset, load_embedded_verification_context_asset, }, generators::{ - GENESIS_EPOCH, build_asset_generation_setup, build_genesis_base_case_next_state, - build_genesis_base_case_witness, + GENESIS_EPOCH, build_asset_generation_setup_from_cache, + build_genesis_base_case_next_state, build_genesis_base_case_witness, }, helpers::{ assert_recursive_mock_prover_rejects_with_label, build_mock_prover_setup_from_assets, @@ -126,7 +126,7 @@ mod slow { fn circuit_rejects_wrong_genesis_message_global_field() { // MockProver constraint check: global[0] (genesis_message) set to ONE must violate // the in-circuit constraint that pins the genesis message to the global public input. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let next_state = build_genesis_base_case_next_state(&setup, GENESIS_EPOCH); let witness = build_genesis_base_case_witness(&setup); @@ -148,7 +148,7 @@ mod slow { fn circuit_rejects_wrong_certificate_circuit_verification_key_representation_global_field() { // MockProver constraint check: global[3] (certificate_circuit_verification_key_representation) set to ONE // must violate the in-circuit constraint that pins the certificate circuit verification key representation. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let next_state = build_genesis_base_case_next_state(&setup, GENESIS_EPOCH); let witness = build_genesis_base_case_witness(&setup); @@ -170,7 +170,7 @@ mod slow { fn circuit_rejects_wrong_ivc_circuit_verification_key_representation_global_field() { // MockProver constraint check: global[4] (ivc_circuit_verification_key_representation) set to ONE // must violate the in-circuit constraint that pins the IVC circuit verification key representation. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let next_state = build_genesis_base_case_next_state(&setup, GENESIS_EPOCH); let witness = build_genesis_base_case_witness(&setup); diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/state_transition.rs b/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/state_transition.rs index 32c0037fe96..5775559e1e9 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/state_transition.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/in_circuit/state_transition.rs @@ -20,7 +20,7 @@ mod slow { tests::common::{ asset_readers::load_embedded_recursive_chain_state_asset, generators::{ - build_asset_generation_setup, same_epoch_message_and_preimage_for_step, + build_asset_generation_setup_from_cache, same_epoch_message_and_preimage_for_step, same_epoch_next_state_for_step, }, helpers::{ @@ -37,7 +37,7 @@ mod slow { fn circuit_rejects_wrong_same_epoch_next_merkle_tree_commitment() { // MockProver constraint check: next_merkle_tree_commitment set to ONE must violate the // in-circuit constraint that pins it to prev_state.next_merkle_tree_commitment. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let prev_state = load_embedded_recursive_chain_state_asset() .expect("recursive chain state asset should load") @@ -70,7 +70,7 @@ mod slow { fn circuit_rejects_wrong_same_epoch_next_protocol_parameters() { // MockProver constraint check: next_protocol_parameters set to ONE must violate the // in-circuit constraint that pins it to prev_state.next_protocol_parameters. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let prev_state = load_embedded_recursive_chain_state_asset() .expect("recursive chain state asset should load") @@ -104,7 +104,7 @@ mod slow { // MockProver constraint check: message set to ONE must violate the in-circuit // Blake2b constraint enforcing message = Blake2b(message_preimage). The same gate // is exercised by both same-epoch and next-epoch paths, so testing it once suffices. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let prev_state = load_embedded_recursive_chain_state_asset() .expect("recursive chain state asset should load") diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/transitions/negative/genesis.rs b/mithril-stm/src/circuits/halo2_ivc/tests/transitions/negative/genesis.rs index 47c8ada5c48..d0bd22c84cb 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/transitions/negative/genesis.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/transitions/negative/genesis.rs @@ -5,8 +5,8 @@ use crate::circuits::halo2_ivc::{ tests::common::{ asset_readers::load_embedded_genesis_step_output_asset, generators::{ - GENESIS_EPOCH, build_asset_generation_setup, build_genesis_base_case_next_state, - build_genesis_base_case_witness, + GENESIS_EPOCH, build_asset_generation_setup_from_cache, + build_genesis_base_case_next_state, build_genesis_base_case_witness, }, helpers::{ assert_recursive_mock_prover_rejects, build_mock_prover_public_inputs, @@ -101,7 +101,7 @@ mod slow { /// `tamper` receives the correct genesis next state before it is passed to the /// public inputs, allowing each test to corrupt exactly the field it wants to verify. fn assert_genesis_circuit_rejects_tampered_next_state(tamper: impl FnOnce(&mut State)) { - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let witness = build_genesis_base_case_witness(&setup); diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/transitions/negative/next_epoch.rs b/mithril-stm/src/circuits/halo2_ivc/tests/transitions/negative/next_epoch.rs index e435258f6d9..ca0420a3923 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/transitions/negative/next_epoch.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/transitions/negative/next_epoch.rs @@ -7,7 +7,8 @@ use crate::circuits::halo2_ivc::{ load_embedded_next_epoch_step_output_asset, load_embedded_recursive_chain_state_asset, }, generators::{ - build_asset_generation_setup, next_message_and_preimage_for_step, next_state_for_step, + build_asset_generation_setup_from_cache, next_message_and_preimage_for_step, + next_state_for_step, }, helpers::{ assert_recursive_mock_prover_rejects_with_label, build_mock_prover_public_inputs, @@ -104,7 +105,7 @@ mod slow { fn circuit_rejects_protocol_parameters_non_advance_in_next_epoch_step() { // MockProver constraint check: in a next-epoch transition the circuit must advance // protocol_parameters to prev_state.next_protocol_parameters. Setting it to ONE violates that. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let prev_state = load_embedded_recursive_chain_state_asset() .expect("recursive chain state asset should load") @@ -136,7 +137,7 @@ mod slow { fn circuit_rejects_merkle_tree_commitment_non_advance_in_next_epoch_step() { // MockProver constraint check: in a next-epoch transition the circuit must advance // merkle_tree_commitment to prev_state.next_merkle_tree_commitment. Setting it to ONE violates that. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let prev_state = load_embedded_recursive_chain_state_asset() .expect("recursive chain state asset should load") @@ -168,7 +169,7 @@ mod slow { fn circuit_rejects_epoch_non_increment_in_next_epoch_step() { // MockProver constraint check: in a next-epoch transition the circuit must increment // current_epoch by exactly one. Decrementing it violates that constraint. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let prev_state = load_embedded_recursive_chain_state_asset() .expect("recursive chain state asset should load") diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/transitions/negative/same_epoch.rs b/mithril-stm/src/circuits/halo2_ivc/tests/transitions/negative/same_epoch.rs index 4f33d8cf07a..a2165498bc5 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/transitions/negative/same_epoch.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/transitions/negative/same_epoch.rs @@ -8,7 +8,7 @@ use crate::circuits::halo2_ivc::{ load_embedded_recursive_chain_state_asset, }, generators::{ - build_asset_generation_setup, same_epoch_message_and_preimage_for_step, + build_asset_generation_setup_from_cache, same_epoch_message_and_preimage_for_step, same_epoch_next_state_for_step, }, helpers::{ @@ -106,7 +106,7 @@ mod slow { fn circuit_rejects_merkle_tree_commitment_carry_violation_in_same_epoch_step() { // MockProver constraint check: in a same-epoch transition the circuit must carry // merkle_tree_commitment unchanged from prev_state. Setting it to ONE violates that constraint. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let prev_state = load_embedded_recursive_chain_state_asset() .expect("recursive chain state asset should load") @@ -139,7 +139,7 @@ mod slow { fn circuit_rejects_protocol_parameters_carry_violation_in_same_epoch_step() { // MockProver constraint check: in a same-epoch transition the circuit must carry // protocol_parameters unchanged from prev_state. Setting it to ONE violates that constraint. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let prev_state = load_embedded_recursive_chain_state_asset() .expect("recursive chain state asset should load") @@ -172,7 +172,7 @@ mod slow { fn circuit_rejects_epoch_advance_in_same_epoch_step() { // MockProver constraint check: in a same-epoch transition the circuit must keep // current_epoch equal to prev_state.current_epoch. Incrementing it violates that constraint. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let mock_prover_setup = build_mock_prover_setup_from_assets(&setup); let prev_state = load_embedded_recursive_chain_state_asset() .expect("recursive chain state asset should load") 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 7cfb169c269..6d8a0860a42 100644 --- a/mithril-stm/src/proof_system/ivc_halo2_snark/proof.rs +++ b/mithril-stm/src/proof_system/ivc_halo2_snark/proof.rs @@ -502,7 +502,7 @@ mod tests { load_embedded_recursive_chain_state_asset, load_embedded_verification_context_asset, }, - generators::{build_asset_generation_setup, build_recursive_global}, + generators::{build_asset_generation_setup_from_cache, build_recursive_global}, }, types::{IvcProofBytes, MessageHash}, }, @@ -529,7 +529,7 @@ mod tests { fn build_proof_verifier_context() -> (Global, IvcVerifierSetup) { let ctx = load_embedded_verification_context_asset() .expect("verification context asset should load"); - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let global = build_recursive_global( &setup, &ctx.certificate_verifying_key, @@ -553,7 +553,7 @@ mod tests { let step_output = load_embedded_next_epoch_step_output_asset() .expect("recursive step output asset should load"); - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let global = build_recursive_global( &setup, &verification_context.certificate_verifying_key, @@ -648,7 +648,7 @@ mod tests { let mut wrong_msg = STEP_OUTPUT_MSG; wrong_msg[0] ^= 0xff; - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let global = build_recursive_global( &setup, &verification_context.certificate_verifying_key, @@ -845,7 +845,7 @@ mod tests { .expect("verification context asset should load"); let step_output = load_embedded_next_epoch_step_output_asset() .expect("recursive step output asset should load"); - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_cache(); let global = build_recursive_global( &setup, &ctx.certificate_verifying_key, @@ -1000,8 +1000,8 @@ mod tests { load_embedded_verification_context_asset, }, generators::{ - build_asset_generation_setup, build_genesis_protocol_message_preimage, - build_recursive_global, + build_asset_generation_setup_from_cache, + build_genesis_protocol_message_preimage, build_recursive_global, setup::{AssetGenerationSetup, QUORUM_SIZE, SIGNER_COUNT, TOTAL_STAKE}, }, }, @@ -1463,7 +1463,7 @@ mod tests { let verification_context = load_embedded_verification_context_asset() .expect("verification context asset should load"); - let asset_setup = build_asset_generation_setup(); + let asset_setup = build_asset_generation_setup_from_cache(); assert_eq!( verification_context diff --git a/mithril-stm/src/proof_system/ivc_halo2_snark/prover_input.rs b/mithril-stm/src/proof_system/ivc_halo2_snark/prover_input.rs index 803c51d0d13..3435ed18969 100644 --- a/mithril-stm/src/proof_system/ivc_halo2_snark/prover_input.rs +++ b/mithril-stm/src/proof_system/ivc_halo2_snark/prover_input.rs @@ -168,9 +168,9 @@ mod test { load_embedded_verification_context_asset, }, generators::{ - build_asset_generation_setup, build_genesis_base_case_next_state, - build_genesis_base_case_witness, build_genesis_protocol_message_preimage, - build_recursive_global, + build_asset_generation_setup_from_cache, + build_genesis_base_case_next_state, build_genesis_base_case_witness, + build_genesis_protocol_message_preimage, build_recursive_global, setup::{ AssetGenerationSetup, GENESIS_EPOCH, QUORUM_SIZE, SIGNER_COUNT, TOTAL_STAKE, @@ -203,7 +203,7 @@ mod test { fn shared_asset_setup() -> &'static AssetGenerationSetup { static CELL: OnceLock = OnceLock::new(); - CELL.get_or_init(build_asset_generation_setup) + CELL.get_or_init(build_asset_generation_setup_from_cache) } fn shared_verification_context() -> &'static VerificationContextAsset { From a0f43da8496544ae2922c8cbd624d5d8e2e439aa Mon Sep 17 00:00:00 2001 From: Hamza Jeljeli Date: Thu, 20 Aug 2026 11:29:37 +0900 Subject: [PATCH 5/9] refactor(stm): read the certificate SRS from the shared unsafe SRS cache --- .../src/circuits/halo2_ivc/tests/common/generators/setup.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3325dd48e6e..315305e22aa 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 @@ -375,7 +375,7 @@ fn build_shared_recursive_context_with( if degree == shared_srs_degree { universal_kzg_parameters.clone() } else { - build_deterministic_params(degree) + load_shared_unsafe_srs(degree) } }; From ad4611629edf1a28632945bbe6a793e400e5d3f5 Mon Sep 17 00:00:00 2001 From: Hamza Jeljeli Date: Thu, 20 Aug 2026 12:40:28 +0900 Subject: [PATCH 6/9] docs(stm): trim the circuit test cache comments to the essential rationale --- .../circuits/halo2/tests/golden/helpers.rs | 13 +--- .../tests/common/generators/setup.rs | 73 +++++++------------ 2 files changed, 29 insertions(+), 57 deletions(-) diff --git a/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs b/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs index ab26b346bbc..0a913f96dfc 100644 --- a/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs +++ b/mithril-stm/src/circuits/halo2/tests/golden/helpers.rs @@ -664,11 +664,8 @@ fn load_or_generate_params(circuit_degree: u32) -> StmResult> { /// Content-keyed cache entry holding the certificate keys derived from these inputs. /// -/// Every input that changes the derived keys is a parameter, so the address is a pure function of -/// them and a test can vary each one: the committed production verifying key as a circuit-version -/// salt, the protocol parameters, the Merkle-tree depth, the circuit degree, and the seed pinning -/// the unsafe SRS. Distinct configurations therefore never share a directory, which is what lets -/// [`KeyProvider`] be built with no expected verifying key. +/// Distinct configurations never share a directory, which is what lets [`KeyProvider`] be built +/// with no expected verifying key. fn certificate_golden_key_cache( production_verifying_key: &[u8], parameters: &Parameters, @@ -691,10 +688,8 @@ fn certificate_golden_key_cache( /// Loads the verification/proving key pair for this configuration from the on-disk cache, deriving /// and storing it on a miss. /// -/// The cache is shared across processes, unlike the in-process map this replaced, which amortized -/// nothing under the nextest process-per-test model. The lock is taken before the lookup so that -/// parallel processes racing a cold miss derive the pair once rather than once each — `KeyProvider` -/// does not serialize its writers. +/// The lock is taken before the lookup so parallel processes racing a cold miss derive the pair +/// once rather than once each. fn get_or_build_circuit_keys( parameters: &Parameters, merkle_tree_depth: u32, 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 315305e22aa..b84f3d68cf5 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 @@ -185,10 +185,8 @@ pub(crate) fn build_deterministic_params(circuit_degree: u32) -> ParamsKZG ParamsKZG { let srs_cache = FileMutex::for_shared_cache("unsafe-srs", &[&UNSAFE_SRS_SEED.to_le_bytes()]); let srs_directory = srs_cache.directory().to_path_buf(); @@ -204,13 +202,16 @@ const RECURSIVE_VERIFYING_KEY_CACHE_FILE: &str = "recursive-verifying-key"; /// Where the expensive recursive verifying key comes from. enum RecursiveVerifyingKeySource { - /// Always derived. Required wherever the result is written to a committed asset. + /// Always derived. Required wherever the result is written to a committed asset, so today it is + /// reached only from the asset generators. Derived, - /// Loaded from the content-keyed test cache, derived only on a miss. + /// Loaded from the content-keyed test cache, derived only on a miss. The path for tests, which + /// read committed assets rather than write them. Cached, } -/// Derives the recursive verifying key for the default IVC circuit shape (about 8.9 s). +/// Derives the recursive verifying key for the default IVC circuit shape, on the order of ten +/// seconds. fn derive_recursive_verifying_key( recursive_commitment_parameters: &ParamsKZG, certificate_verifying_key: &NonRecursiveCircuitVerifyingKey, @@ -230,9 +231,7 @@ fn derive_recursive_verifying_key( /// Content-keyed cache entry holding the recursive verifying key derived from these inputs. /// /// Every input is a parameter, so the address is a pure function of them and a test can vary each -/// one: the freshly derived certificate verifying key (which tracks the certificate circuit), the -/// committed production recursive key as a circuit-version salt, both circuit degrees, and the seed -/// pinning the unsafe SRS. +/// one. fn recursive_verifying_key_cache( certificate_verifying_key_bytes: &[u8], production_recursive_verifying_key: &[u8], @@ -253,9 +252,6 @@ fn recursive_verifying_key_cache( } /// Content-keyed cache entry holding the signer fixture built from these inputs. -/// -/// Every input is a parameter for the same reason as -/// [`recursive_verifying_key_cache`]: the address is testable input by input. fn signer_fixture_cache( signer_count: usize, asset_seed: u64, @@ -277,21 +273,10 @@ fn signer_fixture_cache( /// Reads `cache_file`, or builds the value and publishes it there on a miss. /// -/// Rebuilds from exactly five conditions: an absent file, an unreadable one, a decode failure, bytes -/// that are not the canonical encoding of what they decode to (which covers truncation and trailing -/// bytes, since the verifying-key codec stops at the end of the key and ignores whatever follows), -/// and a rejection by `is_valid`. **It makes no wider claim: a canonically encoded value that -/// satisfies `is_valid` is trusted.** Canonical encoding shows the bytes are self-consistent, not -/// who wrote them, so an entry that is well-formed but semantically wrong is accepted — acceptable -/// only because this cache is disposable, is not exposed to a hostile writer, and is never read by -/// anything that produces committed assets. -/// -/// Rebuilding rather than reporting is deliberate, and unlike -/// [`KeyProvider`](crate::circuits::key_provider::KeyProvider), which propagates deserialization -/// errors: a spoiled test cache must not fail a test run. -/// -/// `is_valid` carries any invariant the bytes alone cannot express. Values whose encoding is -/// self-contained pass `|_| true`. +/// An entry that is absent, unreadable, not canonically encoded or rejected by `is_valid` is +/// rebuilt rather than reported: a spoiled test cache must not fail a test run. Anything that does +/// pass is trusted, which is acceptable only because this cache is disposable and is never read by +/// anything that writes committed assets. fn load_or_build( cache_file: &Path, is_valid: impl FnOnce(&T) -> bool, @@ -339,11 +324,10 @@ fn store_cache_file(cache_file: &Path, value: &T) { let _ = std::fs::File::open(directory).and_then(|directory_file| directory_file.sync_all()); } -/// Builds the shared verifier-side recursive setup, **always deriving** the recursive verifying key. +/// Builds the shared verifier-side recursive setup, always deriving the recursive verifying key. /// -/// Asset generators must use this: they write committed assets, and a stale cached key would -/// silently produce assets derived from it. Behavior tests that only read should call -/// [`build_shared_recursive_context_from_cache`]. +/// Asset generators must use this: a stale cached key would silently produce assets derived from +/// it. Read-only behavior tests should call [`build_shared_recursive_context_from_cache`]. pub(crate) fn build_shared_recursive_context( setup: &AssetGenerationSetup, ) -> SharedRecursiveContext { @@ -353,10 +337,7 @@ pub(crate) fn build_shared_recursive_context( /// Builds the shared verifier-side recursive setup, taking the recursive verifying key from the /// content-keyed test cache when one is present. /// -/// The cache address folds in the freshly derived certificate verifying key, the committed -/// production recursive key, both circuit degrees, and the SRS seed, so a change to the certificate -/// circuit or a regenerated production key resolves to a different entry. **Never call this from an -/// asset generator** — see [`build_shared_recursive_context`]. +/// **Never call this from an asset generator** — see [`build_shared_recursive_context`]. pub(crate) fn build_shared_recursive_context_from_cache( setup: &AssetGenerationSetup, ) -> SharedRecursiveContext { @@ -384,8 +365,8 @@ fn build_shared_recursive_context_with( params_for(RECURSIVE_CIRCUIT_DEGREE), ); - // Derived on every call: at about 93 ms it is not worth caching, and its bytes are what make - // the recursive key's cache address sensitive to the certificate circuit. + // Derived on every call: on the order of 100 milliseconds, so not worth caching, and its bytes + // are what make the recursive key's cache address sensitive to the certificate circuit. let certificate_verifying_key = NonRecursiveCircuitVerifyingKey::new(zk_lib::setup_vk( &certificate_commitment_parameters, &setup.certificate_relation, @@ -500,12 +481,9 @@ const SIGNER_FIXTURE_CACHE_FILE: &str = "signer-fixture"; /// The random-generator-derived half of [`AssetGenerationSetup`]. /// -/// The builder threads one seeded generator through the signer keys **and then** the genesis signing -/// key and signature, so a cache that restored only the tree would leave the generator at a different -/// position and silently change the genesis material — which the committed assets embed. Everything -/// drawn from the generator is therefore cached together; every other field of -/// [`AssetGenerationSetup`] is a pure function of these values and the module constants, and is -/// recomputed on both paths. +/// One seeded generator produces the signer keys **and then** the genesis key and signature, so +/// caching only the tree would leave it at a different position and silently change the genesis +/// material the committed assets embed. Everything drawn from the generator is cached together. #[derive(Serialize, Deserialize)] struct CachedSignerFixture { signing_keys: Vec, @@ -662,11 +640,10 @@ fn assemble_asset_generation_setup(fixture: CachedSignerFixture) -> AssetGenerat } } -/// Builds the deterministic asset-generation setup, **always running the generator sequence**. +/// Builds the deterministic asset-generation setup, always running the generator sequence. /// -/// Asset writers and the committed-fixture drift guard must use this: they produce or check committed -/// bytes, and a stale cached fixture would let them do so from outdated signer data. Behavior tests -/// that only consume the setup should call [`build_asset_generation_setup_from_cache`]. +/// Asset writers and the drift guard must use this: a stale cached fixture would let them produce +/// or check committed bytes from outdated signer data. pub(crate) fn build_asset_generation_setup() -> AssetGenerationSetup { assemble_asset_generation_setup(build_signer_fixture()) } From 76eea75fd0c17e7d3701735308097036efa14c60 Mon Sep 17 00:00:00 2001 From: Hamza Jeljeli Date: Thu, 20 Aug 2026 12:40:41 +0900 Subject: [PATCH 7/9] refactor(stm): drop the redundant version suffix from the test cache labels --- .../src/circuits/halo2_ivc/tests/common/generators/setup.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b84f3d68cf5..53b208f692f 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 @@ -240,7 +240,7 @@ fn recursive_verifying_key_cache( unsafe_srs_seed: u64, ) -> FileMutex { FileMutex::for_shared_cache( - "ivc-recursive-verifying-key-v1", + "ivc-recursive-verifying-key", &[ certificate_verifying_key_bytes, production_recursive_verifying_key, @@ -260,7 +260,7 @@ fn signer_fixture_cache( genesis_next_protocol_parameters: u64, ) -> FileMutex { FileMutex::for_shared_cache( - "ivc-signer-fixture-v1", + "ivc-signer-fixture", &[ &signer_count.to_le_bytes(), &asset_seed.to_le_bytes(), From d53b1686324e5f69ff25890190730a15fe6d04d8 Mon Sep 17 00:00:00 2001 From: Hamza Jeljeli Date: Thu, 20 Aug 2026 12:45:01 +0900 Subject: [PATCH 8/9] refactor(stm): name the from-scratch test setup builders explicitly --- .../src/circuits/halo2_ivc/embedded_assets.rs | 3 +- .../common/generators/asset_generation.rs | 60 ++++++++++++------- .../halo2_ivc/tests/common/generators/mod.rs | 4 +- .../tests/common/generators/setup.rs | 8 +-- .../halo2_ivc/tests/golden/positive.rs | 10 ++-- 5 files changed, 52 insertions(+), 33 deletions(-) diff --git a/mithril-stm/src/circuits/halo2_ivc/embedded_assets.rs b/mithril-stm/src/circuits/halo2_ivc/embedded_assets.rs index f4131c47ee2..a8116a3c58f 100644 --- a/mithril-stm/src/circuits/halo2_ivc/embedded_assets.rs +++ b/mithril-stm/src/circuits/halo2_ivc/embedded_assets.rs @@ -175,7 +175,8 @@ pub(crate) struct FollowingCertificateInEpochAsset { /// 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. +/// `build_asset_generation_setup_from_scratch()` 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` 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 6899eda9f8c..cf02c57585b 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 @@ -10,7 +10,7 @@ use super::proofs::{ use super::setup::{ AssetGenerationSetup, AssetPaths, GENESIS_EPOCH, INITIAL_CHAIN_LENGTH, build_recursive_fixed_bases, build_recursive_global, build_recursive_proving_key, - build_shared_recursive_context, + build_shared_recursive_context_from_scratch, }; use super::transitions::{ build_genesis_base_case_next_state, build_genesis_base_case_witness, @@ -392,7 +392,7 @@ pub(crate) fn generate_recursive_chain_state_asset( ); let total_start = Instant::now(); - let context = build_shared_recursive_context(setup); + let context = build_shared_recursive_context_from_scratch(setup); let (_, recursive_fixed_bases, combined_fixed_bases) = build_recursive_fixed_bases( &context.certificate_verifying_key, &context.recursive_verifying_key, @@ -432,7 +432,7 @@ pub(crate) fn generate_verification_context_asset( paths.verification_context.display() ); let total_start = Instant::now(); - let context = build_shared_recursive_context(setup); + let context = build_shared_recursive_context_from_scratch(setup); println!("generate_verification_context: certificate and recursive verifying keys ready"); let (_, _, combined_fixed_bases) = build_recursive_fixed_bases( @@ -478,7 +478,7 @@ pub(crate) fn generate_recursive_step_output_asset( let recursive_chain_state = load_recursive_chain_state_asset(&paths.recursive_chain_state) .expect("failed to load recursive_chain_state asset"); - let context = build_shared_recursive_context(setup); + let context = build_shared_recursive_context_from_scratch(setup); let recursive_proving_key = build_recursive_proving_key(&context); println!("generate_recursive_step_output: certificate and recursive keys ready"); @@ -523,7 +523,7 @@ pub(crate) fn generate_genesis_step_output_asset(setup: &AssetGenerationSetup, p ); let total_start = Instant::now(); - let context = build_shared_recursive_context(setup); + let context = build_shared_recursive_context_from_scratch(setup); let (_, _, combined_fixed_bases) = build_recursive_fixed_bases( &context.certificate_verifying_key, &context.recursive_verifying_key, @@ -619,7 +619,7 @@ pub(crate) fn generate_same_epoch_step_output_asset( ); let total_start = Instant::now(); - let context = build_shared_recursive_context(setup); + let context = build_shared_recursive_context_from_scratch(setup); let (_, recursive_fixed_bases, combined_fixed_bases) = build_recursive_fixed_bases( &context.certificate_verifying_key, &context.recursive_verifying_key, @@ -771,7 +771,7 @@ pub(crate) fn generate_first_step_cert_asset(setup: &AssetGenerationSetup, paths paths.first_step_cert.display() ); let total_start = Instant::now(); - let context = build_shared_recursive_context(setup); + let context = build_shared_recursive_context_from_scratch(setup); println!("generate_first_step_cert: shared recursive context ready"); let mut rng = OsRng; @@ -871,51 +871,69 @@ pub(crate) fn generate_genesis_benchmark_fixture_asset( #[test] #[ignore] fn generate_verification_context_only() { - use super::setup::{AssetPaths, build_asset_generation_setup}; - generate_verification_context_asset(&build_asset_generation_setup(), &AssetPaths::default()); + use super::setup::{AssetPaths, build_asset_generation_setup_from_scratch}; + generate_verification_context_asset( + &build_asset_generation_setup_from_scratch(), + &AssetPaths::default(), + ); } #[test] #[ignore] fn generate_recursive_chain_state_only() { - use super::setup::{AssetPaths, build_asset_generation_setup}; - generate_recursive_chain_state_asset(&build_asset_generation_setup(), &AssetPaths::default()); + use super::setup::{AssetPaths, build_asset_generation_setup_from_scratch}; + generate_recursive_chain_state_asset( + &build_asset_generation_setup_from_scratch(), + &AssetPaths::default(), + ); } #[test] #[ignore] fn generate_recursive_step_output_only() { - use super::setup::{AssetPaths, build_asset_generation_setup}; - generate_recursive_step_output_asset(&build_asset_generation_setup(), &AssetPaths::default()); + use super::setup::{AssetPaths, build_asset_generation_setup_from_scratch}; + generate_recursive_step_output_asset( + &build_asset_generation_setup_from_scratch(), + &AssetPaths::default(), + ); } #[test] #[ignore] fn generate_genesis_step_output_only() { - use super::setup::{AssetPaths, build_asset_generation_setup}; - generate_genesis_step_output_asset(&build_asset_generation_setup(), &AssetPaths::default()); + use super::setup::{AssetPaths, build_asset_generation_setup_from_scratch}; + generate_genesis_step_output_asset( + &build_asset_generation_setup_from_scratch(), + &AssetPaths::default(), + ); } #[test] #[ignore] fn generate_same_epoch_step_output_only() { - use super::setup::{AssetPaths, build_asset_generation_setup}; - generate_same_epoch_step_output_asset(&build_asset_generation_setup(), &AssetPaths::default()); + use super::setup::{AssetPaths, build_asset_generation_setup_from_scratch}; + generate_same_epoch_step_output_asset( + &build_asset_generation_setup_from_scratch(), + &AssetPaths::default(), + ); } #[test] #[ignore] fn generate_first_step_cert_only() { - use super::setup::{AssetPaths, build_asset_generation_setup}; - generate_first_step_cert_asset(&build_asset_generation_setup(), &AssetPaths::default()); + use super::setup::{AssetPaths, build_asset_generation_setup_from_scratch}; + generate_first_step_cert_asset( + &build_asset_generation_setup_from_scratch(), + &AssetPaths::default(), + ); } #[test] #[ignore] fn generate_genesis_benchmark_fixture_only() { - use super::setup::{AssetPaths, build_asset_generation_setup}; + use super::setup::{AssetPaths, build_asset_generation_setup_from_scratch}; generate_genesis_benchmark_fixture_asset( - &build_asset_generation_setup(), + &build_asset_generation_setup_from_scratch(), &AssetPaths::default(), ); } diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/mod.rs b/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/mod.rs index 83abf4f11be..53d3eb74597 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/mod.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/mod.rs @@ -10,8 +10,8 @@ pub(crate) use proofs::{ try_verify_prepare_poseidon_ivc, verify_prepare_blake2b_ivc, verify_prepare_poseidon_ivc, }; pub(crate) use setup::{ - AssetGenerationSetup, GENESIS_EPOCH, build_asset_generation_setup, - build_asset_generation_setup_from_cache, build_recursive_fixed_bases, build_recursive_global, + AssetGenerationSetup, GENESIS_EPOCH, build_asset_generation_setup_from_cache, + build_asset_generation_setup_from_scratch, build_recursive_fixed_bases, build_recursive_global, build_shared_recursive_context_from_cache, }; pub(crate) use transitions::{ 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 53b208f692f..16ec9ed95b1 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 @@ -328,7 +328,7 @@ fn store_cache_file(cache_file: &Path, value: &T) { /// /// Asset generators must use this: a stale cached key would silently produce assets derived from /// it. Read-only behavior tests should call [`build_shared_recursive_context_from_cache`]. -pub(crate) fn build_shared_recursive_context( +pub(crate) fn build_shared_recursive_context_from_scratch( setup: &AssetGenerationSetup, ) -> SharedRecursiveContext { build_shared_recursive_context_with(setup, RecursiveVerifyingKeySource::Derived) @@ -337,7 +337,7 @@ pub(crate) fn build_shared_recursive_context( /// Builds the shared verifier-side recursive setup, taking the recursive verifying key from the /// content-keyed test cache when one is present. /// -/// **Never call this from an asset generator** — see [`build_shared_recursive_context`]. +/// **Never call this from an asset generator** — see [`build_shared_recursive_context_from_scratch`]. pub(crate) fn build_shared_recursive_context_from_cache( setup: &AssetGenerationSetup, ) -> SharedRecursiveContext { @@ -644,14 +644,14 @@ fn assemble_asset_generation_setup(fixture: CachedSignerFixture) -> AssetGenerat /// /// Asset writers and the drift guard must use this: a stale cached fixture would let them produce /// or check committed bytes from outdated signer data. -pub(crate) fn build_asset_generation_setup() -> AssetGenerationSetup { +pub(crate) fn build_asset_generation_setup_from_scratch() -> AssetGenerationSetup { assemble_asset_generation_setup(build_signer_fixture()) } /// Builds the deterministic asset-generation setup, taking the signer fixture from the content-keyed /// test cache when a valid one is present. /// -/// **Never call this from an asset writer** — see [`build_asset_generation_setup`]. +/// **Never call this from an asset writer** — see [`build_asset_generation_setup_from_scratch`]. pub(crate) fn build_asset_generation_setup_from_cache() -> AssetGenerationSetup { let fixture_cache = signer_fixture_cache( SIGNER_COUNT, 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 2f3b68ea721..4a40ef2d187 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/golden/positive.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/golden/positive.rs @@ -15,10 +15,10 @@ use crate::circuits::halo2_ivc::tests::common::{ load_embedded_recursive_chain_state_asset, load_embedded_verification_context_asset, }, generators::{ - GENESIS_EPOCH, build_asset_generation_setup, build_asset_generation_setup_from_cache, - build_genesis_base_case_next_state, build_genesis_base_case_witness, - build_genesis_protocol_message_preimage, next_message_and_preimage_for_step, - next_state_for_step, + GENESIS_EPOCH, build_asset_generation_setup_from_cache, + build_asset_generation_setup_from_scratch, build_genesis_base_case_next_state, + 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, @@ -105,7 +105,7 @@ fn genesis_benchmark_fixture_is_deterministic_and_valid() { // deterministic generator output, be internally consistent, and carry a valid genesis // signature. Fails loudly if the committed `.bin` drifts from the deterministic generator. // Builds fresh on purpose: a drift guard must not read the fixture cache it is guarding. - let setup = build_asset_generation_setup(); + let setup = build_asset_generation_setup_from_scratch(); let fixture = load_embedded_genesis_benchmark_fixture().expect("genesis benchmark fixture should load"); From 27ce251e61a6b44c4616dc197a93282eaa93f8b6 Mon Sep 17 00:00:00 2001 From: Hamza Jeljeli Date: Thu, 20 Aug 2026 13:06:57 +0900 Subject: [PATCH 9/9] chore(stm): updated changelog and crate versions --- Cargo.lock | 2 +- mithril-common/Cargo.toml | 2 +- mithril-stm/CHANGELOG.md | 11 +++++++++++ mithril-stm/Cargo.toml | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed31ba37f19..c94cf3ce239 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4820,7 +4820,7 @@ dependencies = [ [[package]] name = "mithril-stm" -version = "0.12.7" +version = "0.12.8" dependencies = [ "anyhow", "blake2 0.10.6", diff --git a/mithril-common/Cargo.toml b/mithril-common/Cargo.toml index bf9781bfbe7..e6f426b8435 100644 --- a/mithril-common/Cargo.toml +++ b/mithril-common/Cargo.toml @@ -51,7 +51,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.12.7", default-features = false } +mithril-stm = { path = "../mithril-stm", version = "0.12.8", 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 14abd8205d7..33bace77b11 100644 --- a/mithril-stm/CHANGELOG.md +++ b/mithril-stm/CHANGELOG.md @@ -5,6 +5,17 @@ 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.12.8 (08-20-2026) + +### Changed + +- Extended the on-disk test cache to the recursive circuit fixtures, so the recursive verifying key, the certificate golden circuit keys and the deterministic signer fixture are derived once and shared across test processes instead of being rebuilt by each one. +- Shared the unsafe SRS cache between the circuit test generators and the prover setup, so the SRS is generated once per degree across the whole test suite. + +### Removed + +- Removed the `StmCircuitError::CircuitKeysCacheLockPoisoned` variant, which became unreachable once the in-process circuit key cache was replaced by the on-disk cache. + ## 0.12.7 (08-19-2026) ### Changed diff --git a/mithril-stm/Cargo.toml b/mithril-stm/Cargo.toml index 33a2ccb1989..89ea6218823 100644 --- a/mithril-stm/Cargo.toml +++ b/mithril-stm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-stm" -version = "0.12.7" +version = "0.12.8" edition = { workspace = true } authors = { workspace = true } homepage = { workspace = true }