diff --git a/Cargo.lock b/Cargo.lock index 83031dba3b..e01e57dd04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4009,6 +4009,7 @@ dependencies = [ "num-traits", "paste", "rand 0.9.2", + "rayon", "reqwest", "serde", "serde_json", diff --git a/circuits/bin/threshold/pk_aggregation/src/main.nr b/circuits/bin/threshold/pk_aggregation/src/main.nr index 0ef023adb8..74ee284342 100644 --- a/circuits/bin/threshold/pk_aggregation/src/main.nr +++ b/circuits/bin/threshold/pk_aggregation/src/main.nr @@ -5,24 +5,21 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use lib::configs::default::H; -use lib::configs::default::threshold::{L, N, PK_AGGREGATION_BIT_PK, PK_AGGREGATION_CONFIGS}; +use lib::configs::default::threshold::{CRP, L, N, PK_AGGREGATION_BIT_PK, PK_AGGREGATION_CONFIGS}; use lib::core::threshold::pk_aggregation::PkAggregation; use lib::math::polynomial::Polynomial; fn main( expected_threshold_pk_commitments: pub [Field; H], pk0: [[Polynomial; L]; H], - pk1: [[Polynomial; L]; H], pk0_agg: [Polynomial; L], - pk1_agg: [Polynomial; L], ) -> pub Field { let pk_aggregation: PkAggregation = PkAggregation::new( PK_AGGREGATION_CONFIGS, + CRP, expected_threshold_pk_commitments, pk0, - pk1, pk0_agg, - pk1_agg, ); pk_aggregation.execute() diff --git a/circuits/lib/src/core/threshold/pk_aggregation.nr b/circuits/lib/src/core/threshold/pk_aggregation.nr index 0b7ffe2a20..f662a5698a 100644 --- a/circuits/lib/src/core/threshold/pk_aggregation.nr +++ b/circuits/lib/src/core/threshold/pk_aggregation.nr @@ -22,21 +22,26 @@ impl Configs { /// Threshold public key aggregation (C5). /// -/// **Role:** Sum honest parties' threshold PK limbs into `pk0_agg`, check `pk1_agg` matches CRS `a`, -/// and commit the aggregated key for P3. +/// **Role:** Sum honest parties' threshold PK limbs into `pk0_agg`, pin `pk1_agg` to the +/// circuit-constant CRS `a`, and commit the aggregated key for P3. /// -/// **Consumes:** `commit(pk_trbfv[h])` from C1 per honest party `h`. +/// **Consumes:** `commit(pk0[h])` from C1 per honest party `h`. /// -/// **Produces:** `commit(pk_agg)` for user-data encryption (P3). +/// **Produces:** `commit(pk0_agg, crp)` for user-data encryption (P3). /// -/// **Verifies:** For each limb and coefficient, `pk0_agg` matches the mod-`q_l` sum of party `pk0` -/// (centered coefficients, `ModU128::reduce_mod` with half-`q` shift); `pk1_agg` equals shared CRS `a`. +/// **Verifies:** For each limb and coefficient, `pk0_agg` matches the mod-q_l sum of party +/// pk0 (centered coefficients). pk1_agg is not a witness; the CRS `crp` is used directly, +/// eliminating H*L*N per-party pk1 witness elements and the pk1 consistency loop. pub struct PkAggregation { /// Circuit parameters including CRT moduli configs: Configs, + /// Common Reference String polynomials (circuit constant, one per CRT basis). + /// Used directly as pk1_agg in the final commitment; no per-party pk1 witness needed. + crp: [Polynomial; L], + /// Expected commitments to each honest party's threshold public key share. - /// commit(pk_trbfv[h]) produced by C1 for each h in H. + /// commit(pk0[h]) produced by C1 for each h in H. /// (public witnesses) expected_threshold_pk_commitments: [Field; H], @@ -44,42 +49,32 @@ pub struct PkAggregation { /// pk0[party_idx][basis_idx] for each party and CRT basis. /// (committed witnesses) pk0: [[Polynomial; L]; H], - /// Individual threshold public key second components from H honest parties. - /// pk1[party_idx][basis_idx] for each party and CRT basis. - /// (committed witnesses) - pk1: [[Polynomial; L]; H], /// Claimed aggregated public key first component for each CRT basis. /// Must equal sum(pk0[h][l]) mod q_l for each basis l. /// (committed witness) pk0_agg: [Polynomial; L], - /// Claimed aggregated public key second component for each CRT basis. - /// Must equal sum(pk1[h][l]) mod q_l for each basis l. - /// (committed witness) - pk1_agg: [Polynomial; L], } impl PkAggregation { pub fn new( configs: Configs, + crp: [Polynomial; L], expected_threshold_pk_commitments: [Field; H], pk0: [[Polynomial; L]; H], - pk1: [[Polynomial; L]; H], pk0_agg: [Polynomial; L], - pk1_agg: [Polynomial; L], ) -> Self { - PkAggregation { configs, expected_threshold_pk_commitments, pk0, pk1, pk0_agg, pk1_agg } + PkAggregation { configs, crp, expected_threshold_pk_commitments, pk0, pk0_agg } } - /// Verifies that each honest party's public key hashes to its expected commitment from C1. + /// Verifies that each honest party's pk0 hashes to its expected commitment from C1. /// - /// Ensures that only correctly generated public key shares, as verified during - /// key generation in C1, are included in the aggregation. Any substitution of - /// a different key will cause this assertion to fail. + /// C1 commits only to pk0 (not pk1) because pk1 = CRS is a public constant. + /// Binding pk0 per-party is sufficient: pk1 is pinned globally via the crp field. fn verify_pk_commitments(self) { for i in 0..H { assert( - compute_threshold_pk_commitment::(self.pk0[i], self.pk1[i]) + compute_threshold_pk_commitment::(self.pk0[i]) == self.expected_threshold_pk_commitments[i], "PK commitment mismatch", ); @@ -105,6 +100,10 @@ impl PkAggregation delta + H*q_l >= (H-1)*q_l/2 >= 0; max quotient <= H+2. + let offset: Field = H as Field * q_l; for coeff_idx in 0..N { let mut sum_shifted: Field = 0; @@ -112,39 +111,25 @@ impl PkAggregation ~12 gates for assert_zero_mod). + mod_q_l.assert_zero_mod( + sum_shifted + offset - pk_agg[basis_idx].coefficients[coeff_idx] - h_half_qi, + ); } } - /// Returns `commit(pk_agg)` for P3 (`user_data_encryption_ct0` / `ct1`). + /// Returns `commit(pk0_agg, crp)` for P3 (`user_data_encryption_ct0` / `ct1`). pub fn execute(self) -> Field { - // Step 1: Bind each party PK to its C1 commitment. + // Step 1: Bind each party pk0 to its C1 commitment. self.verify_pk_commitments(); - // Step 2: Check `pk0_agg` sums party limbs and `pk1_agg` matches CRS `a` (per CRT basis). + // Step 2: Check `pk0_agg` is the mod-q_l sum of party pk0 limbs (per CRT basis). for basis_idx in 0..L { self.verify_pk_for_basis(self.pk0, self.pk0_agg, basis_idx); - self.verify_pk1(basis_idx); } - // Step 3: Commit to the aggregated threshold public key. - compute_pk_aggregation_commitment::(self.pk0_agg, self.pk1_agg) + // Step 3: Commit to (pk0_agg, crp). crp is the circuit-constant CRS a used as pk1_agg. + compute_pk_aggregation_commitment::(self.pk0_agg, self.crp) } } diff --git a/circuits/lib/src/core/threshold/pk_generation.nr b/circuits/lib/src/core/threshold/pk_generation.nr index aa2b0de233..4a15e62a15 100644 --- a/circuits/lib/src/core/threshold/pk_generation.nr +++ b/circuits/lib/src/core/threshold/pk_generation.nr @@ -130,7 +130,7 @@ impl(self.sk); let e_sm_commitment = compute_share_computation_e_sm_commitment::(self.e_sm); - let pk_commitment = compute_threshold_pk_commitment::(self.pk0, self.a); + let pk_commitment = compute_threshold_pk_commitment::(self.pk0); // Step 3: Fiat-Shamir challenge and per-modulus evaluation checks. let gamma = self.generate_challenge(sk_commitment, pk_commitment); diff --git a/circuits/lib/src/math/commitments.nr b/circuits/lib/src/math/commitments.nr index cb6f3fbcbe..7448576bd4 100644 --- a/circuits/lib/src/math/commitments.nr +++ b/circuits/lib/src/math/commitments.nr @@ -171,11 +171,8 @@ pub fn compute_dkg_pk_commitment( pub fn compute_threshold_pk_commitment( pk0: [Polynomial; L], - pk1: [Polynomial; L], ) -> Field { - let mut payload = multiple_polynomial_payload::(Vec::new(), pk0); - payload = multiple_polynomial_payload::(payload, pk1); - + let payload = multiple_polynomial_payload::(Vec::new(), pk0); compute_commitment(payload, DS_PK_GENERATION) } diff --git a/crates/zk-helpers/src/circuits/commitments.rs b/crates/zk-helpers/src/circuits/commitments.rs index 9383aec0ac..2321224d8d 100644 --- a/crates/zk-helpers/src/circuits/commitments.rs +++ b/crates/zk-helpers/src/circuits/commitments.rs @@ -197,25 +197,20 @@ pub fn compute_dkg_pk_commitment(pk0: &CrtPolynomial, pk1: &CrtPolynomial, bit_p BigInt::from_bytes_le(num_bigint::Sign::Plus, &commitment_bytes) } -/// Compute a commitment to the threshold public key polynomials by flattening them and hashing. +/// Compute a commitment to the threshold public key by flattening pk0 and hashing. /// -/// This matches the Noir `compute_threshold_pk_commitment` function exactly. +/// This matches the Noir `compute_threshold_pk_commitment` function exactly, +/// which hashes only pk0 with domain separator DS_PK_GENERATION. /// /// # Arguments -/// * `pk0` - First component of the thershold public key (CRT limbs) -/// * `pk1` - Second component of the thershold public key (CRT limbs) +/// * `pk0` - First component of the threshold public key (CRT limbs) /// * `bit_pk` - The bit width for public key coefficient bounds /// /// # Returns /// A `BigInt` representing the commitment hash value -pub fn compute_threshold_pk_commitment( - pk0: &CrtPolynomial, - pk1: &CrtPolynomial, - bit_pk: u32, -) -> BigInt { +pub fn compute_threshold_pk_commitment(pk0: &CrtPolynomial, bit_pk: u32) -> BigInt { let mut payload = Vec::new(); payload = flatten(payload, &pk0.limbs, bit_pk); - payload = flatten(payload, &pk1.limbs, bit_pk); let input_size = payload.len() as u32; let io_pattern = [0x80000000 | input_size, 1]; @@ -252,12 +247,7 @@ pub fn compute_pk_commitment_from_keyshare_bytes( pk0.center(moduli) .map_err(|e| crate::CircuitsErrors::Other(format!("pk0 center: {}", e)))?; - let mut pk1 = CrtPolynomial::from_fhe_polynomial(&crp.poly()); - pk1.reverse(); - pk1.center(moduli) - .map_err(|e| crate::CircuitsErrors::Other(format!("pk1 center: {}", e)))?; - - let commitment = compute_threshold_pk_commitment(&pk0, &pk1, bit_pk); + let commitment = compute_threshold_pk_commitment(&pk0, bit_pk); let (_, be_bytes) = commitment.to_bytes_be(); let mut padded = [0u8; 32]; let start = 32usize.saturating_sub(be_bytes.len()); @@ -711,10 +701,7 @@ mod tests { let mut pk0 = CrtPolynomial::from_fhe_polynomial(&pk_share.p0_share()); pk0.reverse(); pk0.center(params.moduli()).unwrap(); - let mut pk1 = CrtPolynomial::from_fhe_polynomial(&crp.poly()); - pk1.reverse(); - pk1.center(params.moduli()).unwrap(); - let expected = compute_threshold_pk_commitment(&pk0, &pk1, bit_pk); + let expected = compute_threshold_pk_commitment(&pk0, bit_pk); let (_, be_bytes) = expected.to_bytes_be(); let mut expected_padded = [0u8; 32]; let start = 32usize.saturating_sub(be_bytes.len()); diff --git a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/codegen.rs b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/codegen.rs index faccf11d46..a7d65f00a1 100644 --- a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/codegen.rs +++ b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/codegen.rs @@ -96,30 +96,18 @@ mod tests { .get("pk0") .and_then(|value| value.as_array()) .unwrap(); - let pk1 = parsed - .get("pk1") - .and_then(|value| value.as_array()) - .unwrap(); let pk0_agg = parsed .get("pk0_agg") .and_then(|value| value.as_array()) .unwrap(); - let pk1_agg = parsed - .get("pk1_agg") - .and_then(|value| value.as_array()) - .unwrap(); assert!(!pk0.is_empty()); - assert!(!pk1.is_empty()); assert!(!pk0_agg.is_empty()); - assert!(!pk1_agg.is_empty()); let codegen_toml = generate_toml(inputs).unwrap(); let codegen_configs = generate_configs(preset, &configs); assert!(codegen_toml.contains("pk0")); - assert!(codegen_toml.contains("pk1")); assert!(codegen_toml.contains("[[pk0_agg]]")); - assert!(codegen_toml.contains("[[pk1_agg]]")); assert!(codegen_configs.contains(format!("N: u32 = {}", configs.n).as_str())); assert!(codegen_configs.contains(format!("L: u32 = {}", configs.l).as_str())); diff --git a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/computation.rs b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/computation.rs index df3ae3aed4..1ac46c8720 100644 --- a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/computation.rs +++ b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/computation.rs @@ -15,6 +15,7 @@ use crate::compute_threshold_pk_commitment; use crate::crt_polynomial_to_toml_json; use crate::threshold::pk_aggregation::circuit::PkAggregationCircuit; use crate::threshold::pk_aggregation::circuit::PkAggregationCircuitData; +use crate::threshold::pk_generation::utils::deterministic_crp_crt_polynomial; use crate::CircuitsErrors; use crate::{CircuitComputation, Computation}; use e3_fhe_params::build_pair_for_preset; @@ -75,9 +76,8 @@ pub struct Bounds { pub struct Inputs { pub expected_threshold_pk_commitments: Vec, pub pk0: Vec, - pub pk1: Vec, pub pk0_agg: CrtPolynomial, - pub pk1_agg: CrtPolynomial, + pub crp: CrtPolynomial, } impl Computation for Configs { @@ -158,30 +158,19 @@ impl Computation for Inputs { let bit_pk = compute_modulus_bit(&threshold_params); let mut pk0: Vec = data.pk0_shares.clone(); - // pk1 is the same (common random polynomial a) for all parties - let mut pk1: Vec = (0..data.committee.h).map(|_| data.a.clone()).collect(); - // Extract pk0_agg and pk1_agg from aggregated public key (c[1] = a) let mut pk0_agg = CrtPolynomial::from_fhe_polynomial(&data.public_key.c[0]); - let mut pk1_agg = CrtPolynomial::from_fhe_polynomial(&data.public_key.c[1]); - - // Compute expected_threshold_pk_commitments for each honest party - // Each commitment is computed from pk0[i] and pk1[i] for party i - let mut expected_threshold_pk_commitments = Vec::new(); + let crp = deterministic_crp_crt_polynomial(&threshold_params)?; pk0_agg.reverse(); pk0_agg.center(threshold_params.moduli())?; - pk1_agg.reverse(); - pk1_agg.center(threshold_params.moduli())?; + + let mut expected_threshold_pk_commitments = Vec::new(); for party_index in 0..data.committee.h { pk0[party_index].reverse(); pk0[party_index].center(threshold_params.moduli())?; - pk1[party_index].reverse(); - pk1[party_index].center(threshold_params.moduli())?; - - let commitment = - compute_threshold_pk_commitment(&pk0[party_index], &pk1[party_index], bit_pk); + let commitment = compute_threshold_pk_commitment(&pk0[party_index], bit_pk); expected_threshold_pk_commitments.push(commitment); } @@ -189,9 +178,8 @@ impl Computation for Inputs { Ok(Inputs { expected_threshold_pk_commitments, pk0, - pk1, pk0_agg, - pk1_agg, + crp, }) } @@ -201,22 +189,14 @@ impl Computation for Inputs { .iter() .map(|p| crt_polynomial_to_toml_json(p)) .collect(); - let pk1: Vec> = self - .pk1 - .iter() - .map(|p| crt_polynomial_to_toml_json(p)) - .collect(); let pk0_agg = crt_polynomial_to_toml_json(&self.pk0_agg); - let pk1_agg = crt_polynomial_to_toml_json(&self.pk1_agg); let expected_threshold_pk_commitments = bigint_1d_to_json_values(&self.expected_threshold_pk_commitments); let json = serde_json::json!({ "expected_threshold_pk_commitments": expected_threshold_pk_commitments, "pk0": pk0, - "pk1": pk1, "pk0_agg": pk0_agg, - "pk1_agg": pk1_agg, }); Ok(json) diff --git a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/sample.rs b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/sample.rs index 94dd7bb930..5b0f039556 100644 --- a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/sample.rs +++ b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/sample.rs @@ -84,8 +84,7 @@ mod tests { let inputs = Inputs::compute(preset, &sample).unwrap(); assert_eq!(inputs.pk0.len(), sample.committee.h); - assert_eq!(inputs.pk1.len(), sample.committee.h); assert_eq!(inputs.pk0_agg.limbs.len(), configs.l); - assert_eq!(inputs.pk1_agg.limbs.len(), configs.l); + assert_eq!(inputs.crp.limbs.len(), configs.l); } } diff --git a/crates/zk-prover/Cargo.toml b/crates/zk-prover/Cargo.toml index 670c245918..301341b77c 100644 --- a/crates/zk-prover/Cargo.toml +++ b/crates/zk-prover/Cargo.toml @@ -36,6 +36,7 @@ indicatif = "0.17" nargo = { git = "https://github.com/noir-lang/noir", tag = "v1.0.0-beta.16" } noirc_abi = { git = "https://github.com/noir-lang/noir", tag = "v1.0.0-beta.16" } num-bigint.workspace = true +rayon.workspace = true reqwest = { workspace = true, features = ["json", "stream"] } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true diff --git a/crates/zk-prover/src/backend/download.rs b/crates/zk-prover/src/backend/download.rs index e465b7f833..d4319e9bf3 100644 --- a/crates/zk-prover/src/backend/download.rs +++ b/crates/zk-prover/src/backend/download.rs @@ -19,7 +19,94 @@ use walkdir::WalkDir; use super::ZkBackend; +/// Known committee subdirectories in per-committee circuit release layouts (v0.2.0+). +const COMMITTEE_SUBDIRS: &[&str] = &["micro", "small", "medium", "large"]; + +/// Circuit artifact variant directories at `{preset}/{committee?}/{variant}/...`. +const CIRCUIT_VARIANT_DIRS: &[&str] = &["default", "evm", "recursive"]; + +/// Collect candidate on-disk paths for a manifest entry (legacy flat + per-committee layouts). +fn circuit_manifest_candidates(circuits_dir: &Path, rel_path: &str) -> Vec { + let direct = circuits_dir.join(rel_path); + let mut candidates = vec![direct.clone()]; + + let mut parts = rel_path.split('/'); + let Some(preset) = parts.next() else { + return candidates; + }; + let Some(next) = parts.next() else { + return candidates; + }; + if COMMITTEE_SUBDIRS.contains(&next) || !CIRCUIT_VARIANT_DIRS.contains(&next) { + return candidates; + } + let suffix = parts.collect::>().join("/"); + let suffix = if suffix.is_empty() { + String::new() + } else { + format!("/{suffix}") + }; + + for committee in COMMITTEE_SUBDIRS { + candidates.push(circuits_dir.join(format!("{preset}/{committee}/{next}{suffix}"))); + } + + candidates +} + +/// Resolve a manifest path to an on-disk file, matching `expected_hash` when multiple committee +/// copies exist (v0.2.0 flat checksums vs per-committee tarball layout). +async fn locate_manifest_artifact( + circuits_dir: &Path, + rel_path: &str, + expected_hash: &str, +) -> Result { + let mut last_mismatch: Option<(PathBuf, String)> = None; + + for candidate in circuit_manifest_candidates(circuits_dir, rel_path) { + if !candidate.exists() { + continue; + } + let data = fs::read(&candidate).await?; + match verify_checksum(rel_path, &data, Some(expected_hash)) { + Ok(()) => return Ok(candidate), + Err(ZkError::ChecksumMismatch { actual, .. }) => { + last_mismatch = Some((candidate, actual)); + } + Err(e) => return Err(e), + } + } + + if let Some((_path, actual)) = last_mismatch { + return Err(ZkError::ChecksumMismatch { + file: rel_path.to_string(), + expected: expected_hash.to_string(), + actual, + }); + } + + Err(ZkError::CircuitNotFound(rel_path.to_string())) +} + +async fn read_manifest_file( + circuits_dir: &Path, + rel_path: &str, + expected_hash: &str, +) -> Result, ZkError> { + let path = locate_manifest_artifact(circuits_dir, rel_path, expected_hash).await?; + fs::read(&path).await.map_err(ZkError::from) +} + impl ZkBackend { + /// Resolve a `checksums.json` entry to an on-disk path (legacy flat or per-committee layout). + pub async fn locate_manifest_artifact( + &self, + rel_path: &str, + expected_hash: &str, + ) -> Result { + locate_manifest_artifact(&self.circuits_dir, rel_path, expected_hash).await + } + pub async fn download_bb(&self) -> Result<(), ZkError> { if self.using_custom_bb { println!("IGNORING DOWNLOAD BECAUSE WE ARE USING A CUSTOM BB"); @@ -154,13 +241,7 @@ impl ZkBackend { let mut circuit_infos = HashMap::new(); for (rel_path, expected_hash) in &manifest.files { - let file_path = self.circuits_dir.join(rel_path); - if !file_path.exists() { - return Err(ZkError::CircuitNotFound(rel_path.clone())); - } - - let data = fs::read(&file_path).await?; - verify_checksum(rel_path, &data, Some(expected_hash))?; + read_manifest_file(&self.circuits_dir, rel_path, expected_hash).await?; circuit_infos.insert( rel_path.clone(), @@ -244,3 +325,94 @@ async fn download_with_progress(url: &str, message: &str) -> Result, ZkE pb.finish_with_message("download complete"); Ok(bytes) } + +#[cfg(test)] +mod tests { + use super::*; + use sha2::{Digest, Sha256}; + use std::fs; + use tempfile::TempDir; + + fn write_file(root: &Path, rel: &str, contents: &[u8]) { + let path = root.join(rel); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(path, contents).unwrap(); + } + + fn sha256_hex(data: &[u8]) -> String { + hex::encode(Sha256::digest(data)) + } + + #[tokio::test] + async fn read_manifest_file_prefers_direct_layout() { + let temp = TempDir::new().unwrap(); + let circuits_dir = temp.path(); + let contents = b"flat"; + write_file( + circuits_dir, + "insecure-512/default/dkg/pk/pk.json", + contents, + ); + let hash = sha256_hex(contents); + + let data = read_manifest_file(circuits_dir, "insecure-512/default/dkg/pk/pk.json", &hash) + .await + .unwrap(); + + assert_eq!(data, contents); + } + + #[tokio::test] + async fn read_manifest_file_picks_committee_matching_checksum() { + let temp = TempDir::new().unwrap(); + let circuits_dir = temp.path(); + let micro = b"micro"; + let large = b"large"; + write_file( + circuits_dir, + "insecure-512/micro/default/dkg/pk/pk.json", + micro, + ); + write_file( + circuits_dir, + "insecure-512/large/default/dkg/pk/pk.json", + large, + ); + let large_hash = sha256_hex(large); + + let data = read_manifest_file( + circuits_dir, + "insecure-512/default/dkg/pk/pk.json", + &large_hash, + ) + .await + .unwrap(); + + assert_eq!(data, large); + } + + #[tokio::test] + async fn read_manifest_file_accepts_committee_scoped_manifest_path() { + let temp = TempDir::new().unwrap(); + let circuits_dir = temp.path(); + let contents = b"micro"; + write_file( + circuits_dir, + "insecure-512/micro/default/dkg/pk/pk.json", + contents, + ); + let hash = sha256_hex(contents); + + let data = read_manifest_file( + circuits_dir, + "insecure-512/micro/default/dkg/pk/pk.json", + &hash, + ) + .await + .unwrap(); + + assert_eq!(data, contents); + } +} diff --git a/crates/zk-prover/src/circuits/aggregation/node_dkg_fold.rs b/crates/zk-prover/src/circuits/aggregation/node_dkg_fold.rs index c2d6081f5e..9970415d83 100644 --- a/crates/zk-prover/src/circuits/aggregation/node_dkg_fold.rs +++ b/crates/zk-prover/src/circuits/aggregation/node_dkg_fold.rs @@ -163,7 +163,10 @@ fn push_step(timings: &mut Vec, step: &str, started: Instan }); } -/// Run C2abFold → C3 folds → C3abFold → C4abFold → NodeFold; returns a [`CircuitName::NodeFold`] proof. +/// Run C2abFold || (C3a fold || C3b fold) → C3abFold → C4abFold → NodeFold; returns a [`CircuitName::NodeFold`] proof. +/// +/// C2abFold and the two C3 fold chains are mutually independent and run concurrently via +/// `rayon::join`. C3a and C3b are also independent of each other and run as a nested join. pub fn prove_node_dkg_fold( prover: &ZkProver, input: &NodeDkgFoldInput, @@ -190,37 +193,67 @@ pub fn prove_node_dkg_fold( c2a_key_hash: c2a_vk.key_hash.clone(), c2b_key_hash: c2b_vk.key_hash.clone(), }; - let t = Instant::now(); - let c2ab_proof = build_and_prove_recursive_bin( - prover, - CircuitName::C2abFold, - &c2ab, - &format!("{e3_id}-c2ab"), - artifacts_dir, - )?; - push_step(&mut step_timings, "c2ab_fold", t); - let t = Instant::now(); - let c3a_folded = generate_sequential_c3_fold( - prover, - input.c3a_inner_proofs, - input.c3_slot_indices_a, - input.c3_total_slots, - &format!("{e3_id}-c3a"), - artifacts_dir, - )?; - push_step(&mut step_timings, "c3a_fold", t); + // c2ab_fold is independent of the c3 chains; c3a and c3b are independent of each other. + // Run all three concurrently: c2ab || (c3a || c3b). + let ((c2ab_result, c2ab_elapsed), ((c3a_result, c3a_elapsed), (c3b_result, c3b_elapsed))) = + rayon::join( + || { + let t = Instant::now(); + let r = build_and_prove_recursive_bin( + prover, + CircuitName::C2abFold, + &c2ab, + &format!("{e3_id}-c2ab"), + artifacts_dir, + ); + (r, t.elapsed()) + }, + || { + rayon::join( + || { + let t = Instant::now(); + let r = generate_sequential_c3_fold( + prover, + input.c3a_inner_proofs, + input.c3_slot_indices_a, + input.c3_total_slots, + &format!("{e3_id}-c3a"), + artifacts_dir, + ); + (r, t.elapsed()) + }, + || { + let t = Instant::now(); + let r = generate_sequential_c3_fold( + prover, + input.c3b_inner_proofs, + input.c3_slot_indices_b, + input.c3_total_slots, + &format!("{e3_id}-c3b"), + artifacts_dir, + ); + (r, t.elapsed()) + }, + ) + }, + ); - let t = Instant::now(); - let c3b_folded = generate_sequential_c3_fold( - prover, - input.c3b_inner_proofs, - input.c3_slot_indices_b, - input.c3_total_slots, - &format!("{e3_id}-c3b"), - artifacts_dir, - )?; - push_step(&mut step_timings, "c3b_fold", t); + let c2ab_proof = c2ab_result?; + step_timings.push(FoldProveStepTiming { + step: "c2ab_fold".to_string(), + seconds: c2ab_elapsed.as_secs_f64(), + }); + let c3a_folded = c3a_result?; + step_timings.push(FoldProveStepTiming { + step: "c3a_fold".to_string(), + seconds: c3a_elapsed.as_secs_f64(), + }); + let c3b_folded = c3b_result?; + step_timings.push(FoldProveStepTiming { + step: "c3b_fold".to_string(), + seconds: c3b_elapsed.as_secs_f64(), + }); let c3_fold_vk = vk::load_vk_artifacts( &prover.circuits_dir(CircuitVariant::Default, artifacts_dir), diff --git a/crates/zk-prover/tests/integration_tests.rs b/crates/zk-prover/tests/integration_tests.rs index 2286473a30..9a2b62d2c9 100644 --- a/crates/zk-prover/tests/integration_tests.rs +++ b/crates/zk-prover/tests/integration_tests.rs @@ -14,9 +14,18 @@ mod common; use common::test_backend; use e3_fhe_params::BfvPreset; use e3_zk_helpers::circuits::dkg::pk::circuit::{PkCircuit, PkCircuitData}; +use e3_zk_helpers::CiphernodesCommitteeSize; use e3_zk_prover::{test_utils::get_tempdir, BbTarget, Provable, SetupStatus, ZkConfig, ZkProver}; use sha2::{Digest, Sha256}; use std::env; +use std::path::Path; + +/// Default committee for downloaded release artifacts (micro matches dev/CI convention). +const RELEASE_COMMITTEE: CiphernodesCommitteeSize = CiphernodesCommitteeSize::Micro; + +fn preset_committee_dir(circuits_dir: &Path, preset: &BfvPreset) -> std::path::PathBuf { + circuits_dir.join(preset.artifacts_dir_for_committee(RELEASE_COMMITTEE.as_str())) +} #[tokio::test] async fn test_full_flow_download_circuits_prove_and_verify() { @@ -79,8 +88,9 @@ async fn test_full_flow_download_circuits_prove_and_verify() { let result = backend.download_circuits().await; assert!(result.is_ok(), "download_circuits failed: {:?}", result); - // Circuit artifacts are nested under the preset directory (e.g. insecure-512/) - let preset_dir = backend.circuits_dir.join("insecure-512"); + // Circuit artifacts live under `{preset}/{committee}/{variant}/...` in v0.2.0+ releases. + let preset = BfvPreset::InsecureThreshold512; + let preset_dir = preset_committee_dir(&backend.circuits_dir, &preset); assert!( preset_dir .join("default") @@ -121,15 +131,15 @@ async fn test_full_flow_download_circuits_prove_and_verify() { assert!(backend.work_dir.exists()); assert!(backend.base_dir.join("version.json").exists()); - let preset = BfvPreset::InsecureThreshold512; let prover = ZkProver::new(&backend); + let artifacts_dir = preset.artifacts_dir_for_committee(RELEASE_COMMITTEE.as_str()); let sample = PkCircuitData::generate_sample(preset).expect("sample data generation should succeed"); let e3_id = "integration-test-full-flow"; let proof = PkCircuit - .prove(&prover, &preset, &sample, e3_id, &preset.artifacts_dir()) + .prove(&prover, &preset, &sample, e3_id, &artifacts_dir) .expect("proof generation should succeed"); assert!(!proof.data.is_empty(), "proof data should not be empty"); @@ -140,7 +150,7 @@ async fn test_full_flow_download_circuits_prove_and_verify() { let party_id = 0; let verified = PkCircuit - .verify(&prover, &proof, e3_id, party_id, &preset.artifacts_dir()) + .verify(&prover, &proof, e3_id, party_id, &artifacts_dir) .expect("verification call should not error"); assert!(verified, "proof should verify successfully"); @@ -204,12 +214,12 @@ async fn test_download_circuits_verifies_checksums() { ); // Re-read the file from disk and verify the stored checksum matches. - let file_path = backend.circuits_dir.join(rel_path); - assert!( - file_path.exists(), - "circuit file should exist on disk: {}", - file_path.display() - ); + let file_path = backend + .locate_manifest_artifact(rel_path, &circuit_info.checksum) + .await + .unwrap_or_else(|e| { + panic!("circuit file should exist on disk for {}: {e:?}", rel_path) + }); let data = tokio::fs::read(&file_path).await.unwrap(); let mut hasher = Sha256::new(); diff --git a/crates/zk-prover/tests/local_e2e_tests.rs b/crates/zk-prover/tests/local_e2e_tests.rs index 424ca593c1..8f8b177ffd 100644 --- a/crates/zk-prover/tests/local_e2e_tests.rs +++ b/crates/zk-prover/tests/local_e2e_tests.rs @@ -29,7 +29,6 @@ use e3_fhe_params::{build_pair_for_preset, BfvPreset}; use e3_polynomial::{CrtPolynomial, Polynomial}; use e3_zk_helpers::circuits::dkg::pk::circuit::PkCircuit; use e3_zk_helpers::circuits::dkg::pk::circuit::PkCircuitData; -use e3_zk_helpers::circuits::threshold::pk_generation::utils::deterministic_crp_crt_polynomial; use e3_zk_helpers::circuits::{ commitments::{ compute_aggregated_shares_commitment, compute_dkg_pk_commitment, @@ -540,11 +539,8 @@ async fn test_pk_generation_commitment_consistency() { &computation_output.inputs.sk, computation_output.bits.sk_bit, ); - let (threshold_params, _) = build_pair_for_preset(preset).expect("preset pair"); - let a = deterministic_crp_crt_polynomial(&threshold_params).expect("crp polynomial"); let pk_commitment_expected = compute_threshold_pk_commitment( &computation_output.inputs.pk0is, - &a, computation_output.bits.pk_bit, ); @@ -711,7 +707,7 @@ async fn test_pk_aggregation_commitment_consistency() { let expected_final_commitment = compute_pk_aggregation_commitment( &computation_output.inputs.pk0_agg, - &computation_output.inputs.pk1_agg, + &computation_output.inputs.crp, computation_output.bits.pk_bit, ); let final_commitment_from_proof = extract_field_from_end(&proof.public_signals, 0); diff --git a/crates/zk-prover/versions.json b/crates/zk-prover/versions.json index 029030f7d1..d6d9d15405 100644 --- a/crates/zk-prover/versions.json +++ b/crates/zk-prover/versions.json @@ -1,6 +1,6 @@ { "required_bb_version": "3.0.0-nightly.20260102", - "required_circuits_version": "0.1.15", + "required_circuits_version": "0.2.0", "bb_download_url": "https://github.com/AztecProtocol/aztec-packages/releases/download/v{version}/barretenberg-{arch}-{os}.tar.gz", "bb_checksums": { "amd64-linux": "0a4a331a40ef1a4f0e2fb745c38a02e2cbd32a9a5e71188d6f0d93ebee52a31f", diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index 9c36829f5c..1012ade012 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -34,6 +34,7 @@ interface CompiledCircuit { name: string group: CircuitGroup preset: string + committee: CircuitCommittee artifacts: { json?: string vk?: string @@ -524,7 +525,7 @@ class NoirCircuitBuilder { for (const circuit of circuits) { try { - result.compiled.push(this.buildCircuit(circuit, preset)) + result.compiled.push(this.buildCircuit(circuit, preset, committee)) } catch (error: any) { result.errors.push(`${preset}/${committee}/${circuit.name}: ${error.message}`) result.success = false @@ -637,12 +638,13 @@ class NoirCircuitBuilder { return dirs } - private buildCircuit(circuit: CircuitInfo, preset: string): CompiledCircuit { + private buildCircuit(circuit: CircuitInfo, preset: string, committee: CircuitCommittee): CompiledCircuit { const packageName = this.getPackageName(circuit.path) const result: CompiledCircuit = { name: circuit.name, group: circuit.group, preset, + committee, artifacts: {}, checksums: {}, } @@ -848,7 +850,7 @@ class NoirCircuitBuilder { // evm/ variant checksums (only for circuits that have an evm VK) if (c.checksums.vk && c.artifacts.vk) { - const evmPrefix = `${c.preset}/${CIRCUIT_VARIANTS.EVM}/${c.group}/${c.name}` + const evmPrefix = `${c.preset}/${c.committee}/${CIRCUIT_VARIANTS.EVM}/${c.group}/${c.name}` if (c.checksums.json && c.artifacts.json) { const f = `${evmPrefix}/${basename(c.artifacts.json)}` checksums[f] = c.checksums.json @@ -865,7 +867,7 @@ class NoirCircuitBuilder { } // default/ variant checksums - const defaultPrefix = `${c.preset}/${CIRCUIT_VARIANTS.DEFAULT}/${c.group}/${c.name}` + const defaultPrefix = `${c.preset}/${c.committee}/${CIRCUIT_VARIANTS.DEFAULT}/${c.group}/${c.name}` if (c.checksums.json && c.artifacts.json) { const f = `${defaultPrefix}/${basename(c.artifacts.json)}` checksums[f] = c.checksums.json @@ -885,7 +887,7 @@ class NoirCircuitBuilder { } // recursive/ variant checksums (noir-recursive VKs for inner proofs) if (c.checksums.vkNoir && c.artifacts.vkNoir) { - const recursivePrefix = `${c.preset}/${CIRCUIT_VARIANTS.RECURSIVE}/${c.group}/${c.name}` + const recursivePrefix = `${c.preset}/${c.committee}/${CIRCUIT_VARIANTS.RECURSIVE}/${c.group}/${c.name}` if (c.checksums.json && c.artifacts.json) { const f = `${recursivePrefix}/${basename(c.artifacts.json)}` checksums[f] = c.checksums.json