diff --git a/Cargo.lock b/Cargo.lock index 82e2b35a9ce..8444c07e3b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4816,7 +4816,7 @@ dependencies = [ [[package]] name = "mithril-stm" -version = "0.12.1" +version = "0.12.2" dependencies = [ "anyhow", "blake2 0.10.6", diff --git a/mithril-common/Cargo.toml b/mithril-common/Cargo.toml index 471db6622b3..b68c0daba5e 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.1", default-features = false } +mithril-stm = { path = "../mithril-stm", version = "0.12.2", 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 f763d9ac9d1..9a617c4c82c 100644 --- a/mithril-stm/CHANGELOG.md +++ b/mithril-stm/CHANGELOG.md @@ -5,6 +5,14 @@ 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.2 (08-03-2026) + +### Changed + +- Updated the test handling of the unsafe srs. It is now shared across more tests. +- Updated the cache of the test circuit keys so they can be shared more broadly. +- Remove most of the srs downsizing. + ## 0.12.1 (08-03-2026) ### Changed diff --git a/mithril-stm/Cargo.toml b/mithril-stm/Cargo.toml index e88635071d7..06f72194635 100644 --- a/mithril-stm/Cargo.toml +++ b/mithril-stm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-stm" -version = "0.12.1" +version = "0.12.2" edition = { workspace = true } authors = { workspace = true } homepage = { workspace = true } 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 1bdb4286f15..b0f56a28546 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 @@ -173,17 +173,20 @@ pub(crate) fn build_deterministic_params(circuit_degree: u32) -> ParamsKZG::unsafe_setup(circuit_degree, ChaCha20Rng::seed_from_u64(ASSET_SEED)) } -/// Derives circuit-specific commitment parameters from a shared universal SRS. +/// Returns commitment parameters for `circuit_degree`. Reuses `universal_kzg_parameters` +/// directly when it already matches, otherwise generates fresh parameters at `circuit_degree` +/// with the same deterministic seed. This is done because, most of the time, it is faster to +/// generate a new unsafe setup than to downsize an existing one. pub(super) fn derive_commitment_params( universal_kzg_parameters: &ParamsKZG, shared_srs_degree: u32, circuit_degree: u32, ) -> ParamsKZG { - let mut commitment_parameters = universal_kzg_parameters.clone(); - if circuit_degree < shared_srs_degree { - commitment_parameters.downsize(circuit_degree); + if circuit_degree == shared_srs_degree { + universal_kzg_parameters.clone() + } else { + build_deterministic_params(circuit_degree) } - commitment_parameters } /// Builds the shared verifier-side recursive setup from the deterministic SRS. diff --git a/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/verification_key.rs b/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/verification_key.rs index e1381ab9a0d..8af2852eddf 100644 --- a/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/verification_key.rs +++ b/mithril-stm/src/circuits/halo2_ivc/tests/common/generators/verification_key.rs @@ -21,8 +21,6 @@ use crate::{ use super::setup::build_deterministic_params; pub(crate) fn golden_recursive_circuit_verification_key_bytes() -> Vec { - let srs_for_recursive_circuit = build_deterministic_params(RECURSIVE_CIRCUIT_DEGREE); - let small_parameters = Parameters { m: 10, k: 1, @@ -32,8 +30,8 @@ pub(crate) fn golden_recursive_circuit_verification_key_bytes() -> Vec { let circuit = StmCertificateCircuit::try_new(&small_parameters, merkle_tree_depth).unwrap(); let circuit_degree = MidnightCircuit::from_relation(&circuit, None).k(); - let mut srs_for_non_recursive_circuit = srs_for_recursive_circuit.clone(); - srs_for_non_recursive_circuit.downsize(circuit_degree); + let srs_for_non_recursive_circuit = build_deterministic_params(circuit_degree); + let srs_for_recursive_circuit = build_deterministic_params(RECURSIVE_CIRCUIT_DEGREE); let circuit_verification_key = NonRecursiveCircuitVerifyingKey::new( midnight_zk_stdlib::setup_vk(&srs_for_non_recursive_circuit, &circuit), diff --git a/mithril-stm/src/circuits/test_utils/file_mutex.rs b/mithril-stm/src/circuits/test_utils/file_mutex.rs index 0edf1b656fd..e5333ce0f3d 100644 --- a/mithril-stm/src/circuits/test_utils/file_mutex.rs +++ b/mithril-stm/src/circuits/test_utils/file_mutex.rs @@ -166,21 +166,16 @@ mod tests { assert_ne!(baseline.directory(), other_depth.directory()); assert_ne!(baseline.directory(), other_seed.directory()); - // The IVC setup cache additionally folds in the SRS degree and both production verifying keys - // as a circuit-version salt; a change in either must also resolve to a different directory. + // The IVC setup cache folds in both production verifying keys as a circuit-version salt, like + // the certificate-key cache and it omits the SRS degree for the same reason. let ivc_baseline = FileMutex::for_shared_cache( "ivc-setup", - &[b"parameters-a", b"depth-4", b"degree-19", b"circuit-vk-1"], - ); - let other_degree = FileMutex::for_shared_cache( - "ivc-setup", - &[b"parameters-a", b"depth-4", b"degree-20", b"circuit-vk-1"], + &[b"parameters-a", b"depth-4", b"circuit-vk-1"], ); let other_circuit_version = FileMutex::for_shared_cache( "ivc-setup", - &[b"parameters-a", b"depth-4", b"degree-19", b"circuit-vk-2"], + &[b"parameters-a", b"depth-4", b"circuit-vk-2"], ); - assert_ne!(ivc_baseline.directory(), other_degree.directory()); assert_ne!(ivc_baseline.directory(), other_circuit_version.directory()); } } diff --git a/mithril-stm/src/circuits/trusted_setup.rs b/mithril-stm/src/circuits/trusted_setup.rs index bbb482cb908..d129a93b618 100644 --- a/mithril-stm/src/circuits/trusted_setup.rs +++ b/mithril-stm/src/circuits/trusted_setup.rs @@ -192,30 +192,52 @@ impl Default for TrustedSetupProvider { } /// Seed for the deterministic unsafe SRS used by the tests; it pins the SRS's tau. Test key caches -/// fold in this seed so they stay correct if it ever changes. The IVC setup cache also folds in the -/// SRS degree; the certificate-key cache omits it, since keygen downsizes the seed-pinned SRS to the -/// certificate circuit's own degree, so the oversized degree never affects the certificate key. +/// fold in this seed so they stay correct if it ever changes. Both the certificate-key and IVC setup +/// caches omit the SRS degree: keygen always downsizes the seed-pinned SRS to the target circuit +/// degree before deriving keys, so the oversized starting degree never affects the derived keys — +/// only `TrustedSetupProvider::with_unsafe_srs`'s own file layout, which nests by degree. #[cfg(any(test, feature = "benchmark-internals"))] pub(crate) const UNSAFE_SRS_SEED: u64 = 42; #[cfg(any(test, feature = "benchmark-internals"))] impl TrustedSetupProvider { /// Builds a `TrustedSetupProvider` backed by a freshly generated unsafe SRS of degree `k`, written - /// to `base_dir/srs/srs-parameters` with a matching SHA256 hash so the provider's hash check passes. + /// to `base_dir/{k}/srs/srs-parameters` with a matching SHA256 hash so the provider's hash check passes. /// For tests and benchmarks only. pub(crate) fn with_unsafe_srs(base_dir: &std::path::Path, k: u32) -> Self { + let base_dir = base_dir.join(k.to_string()); + let srs_file = base_dir + .join(MITHRIL_CIRCUIT_SRS_FOLDER) + .join(MITHRIL_CIRCUIT_SRS_FILENAME); + + if srs_file.exists() { + return Self::new(base_dir, "", "", Duration::from_secs(600)); + } + let srs = ParamsKZG::::unsafe_setup(k, ChaCha20Rng::seed_from_u64(UNSAFE_SRS_SEED)); let mut srs_bytes = Vec::new(); - srs.write_custom(&mut srs_bytes, SerdeFormat::RawBytes).unwrap(); + srs.write_custom(&mut srs_bytes, SerdeFormat::RawBytesUnchecked) + .unwrap(); let srs_dir = base_dir.join(MITHRIL_CIRCUIT_SRS_FOLDER); create_dir_all(&srs_dir).unwrap(); - File::create(srs_dir.join(MITHRIL_CIRCUIT_SRS_FILENAME)) - .unwrap() - .write_all(&srs_bytes) + + let temp_path = srs_dir.join(MITHRIL_CIRCUIT_SRS_FILENAME).with_extension("temp"); + let final_path = srs_dir.join(MITHRIL_CIRCUIT_SRS_FILENAME); + let mut temporary_file = File::create(&temp_path) + .with_context(|| format!("Failed to create temporary SRS file at {temp_path:?}.")) .unwrap(); + temporary_file.write_all(&srs_bytes).unwrap(); + temporary_file + .sync_all() + .with_context(|| "Failed to fsync temporary SRS file before rename.") + .unwrap(); + drop(temporary_file); + + std::fs::rename(temp_path, final_path).unwrap(); let expected_hash = hex::encode(Sha256::digest(&srs_bytes)); + Self::new(base_dir, expected_hash, "", Duration::from_secs(600)) } } @@ -466,6 +488,102 @@ mod tests { assert!(result.is_err()); } + mod with_unsafe_srs { + use super::*; + + #[test] + fn creates_srs_file_nested_under_degree_subdirectory_and_loads_successfully() { + let temp_dir = tempfile::tempdir_in("/tmp").unwrap(); + let k = 1; + + let provider = TrustedSetupProvider::with_unsafe_srs(temp_dir.path(), k); + + let expected_srs_path = temp_dir + .path() + .join(k.to_string()) + .join(MITHRIL_CIRCUIT_SRS_FOLDER) + .join(MITHRIL_CIRCUIT_SRS_FILENAME); + assert!(expected_srs_path.exists()); + assert!(provider.get_trusted_setup_parameters().is_ok()); + } + + #[test] + fn uses_separate_subdirectory_and_produces_distinct_files_per_degree() { + let temp_dir = tempfile::tempdir_in("/tmp").unwrap(); + + TrustedSetupProvider::with_unsafe_srs(temp_dir.path(), 1); + TrustedSetupProvider::with_unsafe_srs(temp_dir.path(), 2); + + let srs_path_k1 = temp_dir + .path() + .join("1") + .join(MITHRIL_CIRCUIT_SRS_FOLDER) + .join(MITHRIL_CIRCUIT_SRS_FILENAME); + let srs_path_k2 = temp_dir + .path() + .join("2") + .join(MITHRIL_CIRCUIT_SRS_FOLDER) + .join(MITHRIL_CIRCUIT_SRS_FILENAME); + + assert!(srs_path_k1.exists()); + assert!(srs_path_k2.exists()); + assert_ne!( + std::fs::read(srs_path_k1).unwrap(), + std::fs::read(srs_path_k2).unwrap() + ); + } + + #[test] + fn is_deterministic_for_the_same_degree_across_different_base_dirs() { + let temp_dir_a = tempfile::tempdir_in("/tmp").unwrap(); + let temp_dir_b = tempfile::tempdir_in("/tmp").unwrap(); + let k = 1; + + let provider_a = TrustedSetupProvider::with_unsafe_srs(temp_dir_a.path(), k); + let provider_b = TrustedSetupProvider::with_unsafe_srs(temp_dir_b.path(), k); + + let srs_subpath = std::path::Path::new(&k.to_string()) + .join(MITHRIL_CIRCUIT_SRS_FOLDER) + .join(MITHRIL_CIRCUIT_SRS_FILENAME); + let bytes_a = std::fs::read(temp_dir_a.path().join(&srs_subpath)).unwrap(); + let bytes_b = std::fs::read(temp_dir_b.path().join(&srs_subpath)).unwrap(); + + assert_eq!(bytes_a, bytes_b); + assert!(provider_a.get_trusted_setup_parameters().is_ok()); + assert!(provider_b.get_trusted_setup_parameters().is_ok()); + } + + #[test] + fn does_not_regenerate_or_overwrite_an_existing_srs_file() { + let temp_dir = tempfile::tempdir_in("/tmp").unwrap(); + let k = 1; + let srs_dir = temp_dir.path().join(k.to_string()).join(MITHRIL_CIRCUIT_SRS_FOLDER); + std::fs::create_dir_all(&srs_dir).unwrap(); + let srs_path = srs_dir.join(MITHRIL_CIRCUIT_SRS_FILENAME); + std::fs::write(&srs_path, b"sentinel-content-not-a-real-srs").unwrap(); + + TrustedSetupProvider::with_unsafe_srs(temp_dir.path(), k); + + let bytes_after = std::fs::read(&srs_path).unwrap(); + assert_eq!(bytes_after, b"sentinel-content-not-a-real-srs"); + } + + #[test] + fn leaves_no_temporary_file_behind_after_generation() { + let temp_dir = tempfile::tempdir_in("/tmp").unwrap(); + let k = 1; + + TrustedSetupProvider::with_unsafe_srs(temp_dir.path(), k); + + let temp_path = temp_dir + .path() + .join(k.to_string()) + .join(MITHRIL_CIRCUIT_SRS_FOLDER) + .join(MITHRIL_CIRCUIT_SRS_FILENAME) + .with_extension("temp"); + assert!(!temp_path.exists()); + } + } mod golden { use super::*; diff --git a/mithril-stm/src/proof_system/halo2_snark/setup.rs b/mithril-stm/src/proof_system/halo2_snark/setup.rs index d6a67efdb97..f7b72648b28 100644 --- a/mithril-stm/src/proof_system/halo2_snark/setup.rs +++ b/mithril-stm/src/proof_system/halo2_snark/setup.rs @@ -89,6 +89,22 @@ impl SnarkProverSetup { pub(crate) fn build_for_test( parameters: &Parameters, merkle_tree_depth: u32, + ) -> StmResult { + Self::build_for_test_with_unsafe_srs_degree( + parameters, + merkle_tree_depth, + RECURSIVE_CIRCUIT_DEGREE, + ) + } + + /// As [`Self::build_for_test`], but generates the unsafe SRS at `unsafe_srs_degree` instead of + /// exactly [`RECURSIVE_CIRCUIT_DEGREE`]. This removes the need to downsize in [`Self::load`] if + /// the input degree matches the circuit one. + #[cfg(test)] + pub(crate) fn build_for_test_with_unsafe_srs_degree( + parameters: &crate::Parameters, + merkle_tree_depth: u32, + unsafe_srs_degree: u32, ) -> StmResult { let parameters_bytes = parameters.to_bytes()?; let depth_bytes = merkle_tree_depth.to_le_bytes(); @@ -107,7 +123,7 @@ impl SnarkProverSetup { let _key_cache_lock = cache.lock()?; let trusted_setup_provider = - TrustedSetupProvider::with_unsafe_srs(&cache_directory, RECURSIVE_CIRCUIT_DEGREE); + TrustedSetupProvider::with_unsafe_srs(&cache_directory, unsafe_srs_degree); let circuit = StmCertificateCircuit::try_new(parameters, merkle_tree_depth)?; let provider = KeyProvider::new(cache_directory, "non-recursive", &[], circuit); Self::load(&trusted_setup_provider, &provider) diff --git a/mithril-stm/src/proof_system/ivc_halo2_snark/prover_setup.rs b/mithril-stm/src/proof_system/ivc_halo2_snark/prover_setup.rs index 539d14abf01..7bf5fd83e35 100644 --- a/mithril-stm/src/proof_system/ivc_halo2_snark/prover_setup.rs +++ b/mithril-stm/src/proof_system/ivc_halo2_snark/prover_setup.rs @@ -156,35 +156,59 @@ impl IvcSnarkProverSetup { /// Builds an [`IvcSnarkProverSetup`] from a deterministic, oversized unsafe SRS, exercising the /// real `load` path without the production SRS. Shared by the slow IVC tests through a - /// content-keyed cache keyed by the protocol parameters, Merkle-tree depth, the unsafe SRS identity - /// (degree and seed), and the production verifying keys as a circuit-version salt, so the recursive - /// keys — the dominant cost — are computed once and reused across tests and runs. + /// content-keyed cache keyed by the protocol parameters, Merkle-tree depth, the unsafe SRS seed, + /// and the production verifying keys as a circuit-version salt — not the SRS degree, since `load` + /// always downsizes to `RECURSIVE_CIRCUIT_DEGREE` before deriving keys, so any `unsafe_srs_degree` + /// reproduces the same recursive keys — the dominant cost — letting calls at different degrees + /// share one already-computed key cache instead of each paying for keygen separately. #[cfg(test)] pub(crate) fn build_for_test( parameters: &crate::Parameters, merkle_tree_depth: u32, + ) -> StmResult { + Self::build_for_test_with_unsafe_srs_degree( + parameters, + merkle_tree_depth, + RECURSIVE_CIRCUIT_DEGREE, + ) + } + + /// As [`Self::build_for_test`], but generates the unsafe SRS at `unsafe_srs_degree` instead of + /// exactly [`RECURSIVE_CIRCUIT_DEGREE`]. This removes the need to downsize in [`Self::load`] if + /// the input degree matches the circuit one. + #[cfg(test)] + pub(crate) fn build_for_test_with_unsafe_srs_degree( + parameters: &crate::Parameters, + merkle_tree_depth: u32, + unsafe_srs_degree: u32, ) -> StmResult { let parameters_bytes = parameters.to_bytes()?; let depth_bytes = merkle_tree_depth.to_le_bytes(); - let degree_bytes = (RECURSIVE_CIRCUIT_DEGREE + 1).to_le_bytes(); let seed_bytes = UNSAFE_SRS_SEED.to_le_bytes(); - let cache = FileMutex::for_shared_cache( + + // The unsafe SRS depends only on its degree and seed, never on the circuit parameters, so it + // is cached independently of them and shared by every parameter set that needs the same degree + // (`TrustedSetupProvider::with_unsafe_srs` itself nests its file by degree under this directory). + let srs_cache = FileMutex::for_shared_cache("unsafe-srs", &[&seed_bytes]); + let srs_directory = srs_cache.directory().to_path_buf(); + let _srs_cache_lock = srs_cache.lock()?; + let trusted_setup_provider = + TrustedSetupProvider::with_unsafe_srs(&srs_directory, unsafe_srs_degree); + + let key_cache = FileMutex::for_shared_cache( "ivc-setup", &[ NON_RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, RECURSIVE_CIRCUIT_VERIFICATION_KEY_FOR_PRODUCTION, ¶meters_bytes, &depth_bytes, - °ree_bytes, &seed_bytes, ], ); - let cache_directory = cache.directory().to_path_buf(); + let cache_directory = key_cache.directory().to_path_buf(); // Serialize cold-start keygen across the parallel slow-test processes. - let _key_cache_lock = cache.lock()?; + let _key_cache_lock = key_cache.lock()?; - let trusted_setup_provider = - TrustedSetupProvider::with_unsafe_srs(&cache_directory, RECURSIVE_CIRCUIT_DEGREE + 1); let certificate_provider = KeyProvider::new( cache_directory.join("certificate"), "non-recursive", @@ -264,8 +288,12 @@ mod tests { phi_f: 0.2, }; let merkle_tree_depth = SIGNER_COUNT.next_power_of_two().trailing_zeros(); - let ivc_setup = IvcSnarkProverSetup::build_for_test(¶meters, merkle_tree_depth) - .expect("IvcSnarkProverSetup::load should succeed"); + let ivc_setup = IvcSnarkProverSetup::build_for_test_with_unsafe_srs_degree( + ¶meters, + merkle_tree_depth, + RECURSIVE_CIRCUIT_DEGREE + 1, + ) + .expect("IvcSnarkProverSetup::load should succeed"); let verification_context = load_embedded_verification_context_asset() .expect("verification context asset should load"); diff --git a/mithril-stm/src/protocol/aggregate_signature/clerk.rs b/mithril-stm/src/protocol/aggregate_signature/clerk.rs index f117467f16f..0d7e7ecb3e5 100644 --- a/mithril-stm/src/protocol/aggregate_signature/clerk.rs +++ b/mithril-stm/src/protocol/aggregate_signature/clerk.rs @@ -361,16 +361,12 @@ mod tests { params: Parameters, depth: u32, ) -> (Arc, NonRecursiveCircuitVerifyingKey) { - let srs: Arc> = Arc::new(ParamsKZG::::unsafe_setup( - 12, - ChaCha20Rng::from_seed([42u8; 32]), - )); let circuit = StmCertificateCircuit::try_new(¶ms, depth) .expect("certificate circuit should build"); let circuit_degree = MidnightCircuit::from_relation(&circuit, None).k(); - let mut cert_srs = (*srs).clone(); - cert_srs.downsize(circuit_degree); + let cert_srs = + ParamsKZG::::unsafe_setup(circuit_degree, ChaCha20Rng::from_seed([42u8; 32])); let midnight_vk = zk::setup_vk(&cert_srs, &circuit); let midnight_pk = zk::setup_pk(&circuit, &midnight_vk);