From 0b1cd5cf32452250f0e6806730e1ba8e1a839061 Mon Sep 17 00:00:00 2001 From: Zara Date: Thu, 4 Jun 2026 10:00:01 -0700 Subject: [PATCH 01/12] perf(circuits): replace two reduce_mod calls with assert_zero_mod in C5 verify_pk_for_basis verify_pk_for_basis previously reduced both sides of the congruence check independently and compared them (2x ModU64::reduce_mod + assert == 0). It is equivalent and cheaper to check the difference is divisible by q_l using a single ModU64::assert_zero_mod call. A compile-time offset (H * q_l) is added before the subtraction to guarantee the delta is a small non-negative integer in the Field (not a field-wrapped negative), which is required by __reduce_witness_u64. Bound: delta + H*q_l in [(H-1)*q_l/2, (H+2)*q_l/2); max quotient <= H+2, safely fits in u64 for realistic committee sizes. ACIR opcode count (insecure-512, H=3): 43,830 -> 7,990 (-82%). Expected ~24% reduction in Barretenberg constraint count for secure config (N=8192, L=3) where 2x L*N = 49,152 reduce_mod calls are replaced by L*N = 24,576 assert_zero_mod calls. Co-Authored-By: Claude Sonnet 4.6 --- .../lib/src/core/threshold/pk_aggregation.nr | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/circuits/lib/src/core/threshold/pk_aggregation.nr b/circuits/lib/src/core/threshold/pk_aggregation.nr index 0b7ffe2a2..6691cdba8 100644 --- a/circuits/lib/src/core/threshold/pk_aggregation.nr +++ b/circuits/lib/src/core/threshold/pk_aggregation.nr @@ -105,6 +105,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,11 +116,13 @@ 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, + ); } } From d3f51a12d73ba061f9ec8c657c8422e3877b7e0d Mon Sep 17 00:00:00 2001 From: Zara Date: Thu, 4 Jun 2026 10:15:05 -0700 Subject: [PATCH 02/12] perf(circuits): remove per-party pk1 witness from C5; pin CRS directly pk1 = CRS a is a public constant shared by all parties. Including it as a per-party committed witness in C5 served only as a chain check back to C1's commitment, but that check is now unnecessary because: 1. C1 still verifies pk1 = CRS internally (circuit constraint unchanged). 2. C5 now passes the circuit-constant CRP directly as pk1_agg, giving the same binding guarantee without any per-party witness. Changes: - compute_threshold_pk_commitment: remove pk1 parameter; hash pk0 only. Domain separator DS_PK_GENERATION is unchanged; the io_pattern encodes input length so old (pk0+pk1) and new (pk0-only) hashes cannot collide. - C1 (pk_generation): update the one call site accordingly. - C5 lib (pk_aggregation): remove pk1/pk1_agg fields; add crp field; remove verify_pk1; pass crp to compute_pk_aggregation_commitment so the user-facing final commitment still covers both pk0_agg and pk1=CRP. - C5 bin (main.nr): remove pk1/pk1_agg circuit inputs; import and pass CRP. ACIR opcodes (insecure-512, H=3): 7,990 -> 4,002 (-50%). Combined with the assert_zero_mod commit: 43,830 -> 4,002 (-91% total). Witness reduction at H=10: removes (H+1)*L*N = 11*3*8192 = 270,336 field elements from the prover witness. Co-Authored-By: Claude Sonnet 4.6 --- .../bin/threshold/pk_aggregation/src/main.nr | 7 +- .../lib/src/core/threshold/pk_aggregation.nr | 65 +++++++------------ .../lib/src/core/threshold/pk_generation.nr | 2 +- circuits/lib/src/math/commitments.nr | 5 +- 4 files changed, 27 insertions(+), 52 deletions(-) diff --git a/circuits/bin/threshold/pk_aggregation/src/main.nr b/circuits/bin/threshold/pk_aggregation/src/main.nr index 0ef023adb..74ee28434 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 6691cdba8..9290fb963 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", ); @@ -126,31 +121,17 @@ impl PkAggregation 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 aa2b0de23..4a15e62a1 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 cb6f3fbcb..7448576bd 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) } From 536ff313dc0f58b29d25da99e3bbe84bae12c01d Mon Sep 17 00:00:00 2001 From: Zara Date: Thu, 4 Jun 2026 10:21:15 -0700 Subject: [PATCH 03/12] fmt --- circuits/lib/src/core/threshold/pk_aggregation.nr | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/circuits/lib/src/core/threshold/pk_aggregation.nr b/circuits/lib/src/core/threshold/pk_aggregation.nr index 9290fb963..f662a5698 100644 --- a/circuits/lib/src/core/threshold/pk_aggregation.nr +++ b/circuits/lib/src/core/threshold/pk_aggregation.nr @@ -114,9 +114,7 @@ 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, + sum_shifted + offset - pk_agg[basis_idx].coefficients[coeff_idx] - h_half_qi, ); } } From 9732ff71daea46c207c9b032f0abb76e71a84267 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Fri, 5 Jun 2026 13:18:46 +0200 Subject: [PATCH 04/12] update commitments and structs --- circuits/lib/src/configs/default/mod.nr | 6 ++-- crates/zk-helpers/src/circuits/commitments.rs | 27 ++++---------- .../threshold/pk_aggregation/computation.rs | 35 +++++-------------- .../threshold/pk_aggregation/sample.rs | 3 +- crates/zk-prover/tests/local_e2e_tests.rs | 6 +--- 5 files changed, 20 insertions(+), 57 deletions(-) diff --git a/circuits/lib/src/configs/default/mod.nr b/circuits/lib/src/configs/default/mod.nr index 76bfde233..91863d856 100644 --- a/circuits/lib/src/configs/default/mod.nr +++ b/circuits/lib/src/configs/default/mod.nr @@ -4,13 +4,13 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. // -// Auto-generated by build-circuits.ts for preset: insecure-512 +// Auto-generated by build-circuits.ts for preset: secure-8192 // Committee size (N_PARTIES / T / H) is routed through `committee::active`, // which `build-circuits.ts` regenerates atomically with this file. pub use super::committee::active::{H, N_PARTIES, T}; -pub use super::insecure::dkg; -pub use super::insecure::threshold; +pub use super::secure::dkg; +pub use super::secure::threshold; /// Max number of non-zero coefficients in the message polynomial. /// This is a conservative estimate that should be okay for most use cases. diff --git a/crates/zk-helpers/src/circuits/commitments.rs b/crates/zk-helpers/src/circuits/commitments.rs index 9383aec0a..2321224d8 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/computation.rs b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/computation.rs index df3ae3aed..ffd86df18 100644 --- a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/computation.rs +++ b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/computation.rs @@ -75,9 +75,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 +157,21 @@ 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 mut crp = data.a.clone(); pk0_agg.reverse(); pk0_agg.center(threshold_params.moduli())?; - pk1_agg.reverse(); - pk1_agg.center(threshold_params.moduli())?; + crp.reverse(); + crp.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 +179,8 @@ impl Computation for Inputs { Ok(Inputs { expected_threshold_pk_commitments, pk0, - pk1, pk0_agg, - pk1_agg, + crp, }) } @@ -201,22 +190,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 94dd7bb93..5b0f03955 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/tests/local_e2e_tests.rs b/crates/zk-prover/tests/local_e2e_tests.rs index 424ca593c..8f8b177ff 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); From 0c6bb7918218869fccde06d078c6d87a8f225790 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Fri, 5 Jun 2026 14:06:59 +0200 Subject: [PATCH 05/12] fix tests --- .../src/circuits/threshold/pk_aggregation/codegen.rs | 12 ------------ .../circuits/threshold/pk_aggregation/computation.rs | 5 ++--- 2 files changed, 2 insertions(+), 15 deletions(-) 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 faccf11d4..a7d65f00a 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 ffd86df18..1ac46c872 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; @@ -158,12 +159,10 @@ impl Computation for Inputs { let mut pk0: Vec = data.pk0_shares.clone(); let mut pk0_agg = CrtPolynomial::from_fhe_polynomial(&data.public_key.c[0]); - let mut crp = data.a.clone(); + let crp = deterministic_crp_crt_polynomial(&threshold_params)?; pk0_agg.reverse(); pk0_agg.center(threshold_params.moduli())?; - crp.reverse(); - crp.center(threshold_params.moduli())?; let mut expected_threshold_pk_commitments = Vec::new(); From 9d9cf650308368d0b6bd326c44173cfb4a3aa1be Mon Sep 17 00:00:00 2001 From: 0xjei Date: Fri, 5 Jun 2026 14:50:25 +0200 Subject: [PATCH 06/12] fix CI --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e520df8a..97eefaa57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -539,6 +539,19 @@ jobs: - name: 'Setup ZK prover' run: | enclave noir setup + VERSIONS_JSON="crates/zk-prover/versions.json" + INTEGRATION_NOIR="tests/integration/.enclave/noir" + REQUIRED_BB="$(jq -r '.required_bb_version' "$VERSIONS_JSON")" + REQUIRED_CIRCUITS="$(jq -r '.required_circuits_version' "$VERSIONS_JSON")" + rm -rf "${INTEGRATION_NOIR}/circuits" + mkdir -p "${INTEGRATION_NOIR}/circuits" + git fetch origin circuit-artifacts + git archive origin/circuit-artifacts | tar -x -C "${INTEGRATION_NOIR}/circuits" + jq -n \ + --arg bb "$REQUIRED_BB" \ + --arg circuits "$REQUIRED_CIRCUITS" \ + '{bb_version: $bb, circuits_version: $circuits}' \ + > "${INTEGRATION_NOIR}/version.json" - name: 'Run ${{ matrix.test-suite }} tests' run: 'pnpm test:integration ${{ matrix.test-suite }} --no-prebuild' - name: 'Add test summary' @@ -799,6 +812,19 @@ jobs: - name: Setup ZK prover run: | enclave noir setup + VERSIONS_JSON="crates/zk-prover/versions.json" + NOIR_DIR="$HOME/.enclave/noir" + REQUIRED_BB="$(jq -r '.required_bb_version' "$VERSIONS_JSON")" + REQUIRED_CIRCUITS="$(jq -r '.required_circuits_version' "$VERSIONS_JSON")" + rm -rf "${NOIR_DIR}/circuits" + mkdir -p "${NOIR_DIR}/circuits" + git fetch origin circuit-artifacts + git archive origin/circuit-artifacts | tar -x -C "${NOIR_DIR}/circuits" + jq -n \ + --arg bb "$REQUIRED_BB" \ + --arg circuits "$REQUIRED_CIRCUITS" \ + '{bb_version: $bb, circuits_version: $circuits}' \ + > "${NOIR_DIR}/version.json" - name: Run Playwright tests working-directory: ./examples/CRISP @@ -1230,6 +1256,19 @@ jobs: - name: Setup ZK prover run: | enclave noir setup + VERSIONS_JSON="crates/zk-prover/versions.json" + NOIR_DIR="$HOME/.enclave/noir" + REQUIRED_BB="$(jq -r '.required_bb_version' "$VERSIONS_JSON")" + REQUIRED_CIRCUITS="$(jq -r '.required_circuits_version' "$VERSIONS_JSON")" + rm -rf "${NOIR_DIR}/circuits" + mkdir -p "${NOIR_DIR}/circuits" + git fetch origin circuit-artifacts + git archive origin/circuit-artifacts | tar -x -C "${NOIR_DIR}/circuits" + jq -n \ + --arg bb "$REQUIRED_BB" \ + --arg circuits "$REQUIRED_CIRCUITS" \ + '{bb_version: $bb, circuits_version: $circuits}' \ + > "${NOIR_DIR}/version.json" - name: Verify downloaded artifacts run: | From 41b5a54d23b2fcbd71491a027b0fd4de7b4f9d2f Mon Sep 17 00:00:00 2001 From: 0xjei Date: Fri, 5 Jun 2026 15:18:17 +0200 Subject: [PATCH 07/12] switch to insecure & add parallelization for folds --- .github/workflows/ci.yml | 39 -------- Cargo.lock | 1 + circuits/lib/src/configs/default/mod.nr | 6 +- crates/zk-prover/Cargo.toml | 1 + .../src/circuits/aggregation/node_dkg_fold.rs | 93 +++++++++++++------ 5 files changed, 68 insertions(+), 72 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97eefaa57..6e520df8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -539,19 +539,6 @@ jobs: - name: 'Setup ZK prover' run: | enclave noir setup - VERSIONS_JSON="crates/zk-prover/versions.json" - INTEGRATION_NOIR="tests/integration/.enclave/noir" - REQUIRED_BB="$(jq -r '.required_bb_version' "$VERSIONS_JSON")" - REQUIRED_CIRCUITS="$(jq -r '.required_circuits_version' "$VERSIONS_JSON")" - rm -rf "${INTEGRATION_NOIR}/circuits" - mkdir -p "${INTEGRATION_NOIR}/circuits" - git fetch origin circuit-artifacts - git archive origin/circuit-artifacts | tar -x -C "${INTEGRATION_NOIR}/circuits" - jq -n \ - --arg bb "$REQUIRED_BB" \ - --arg circuits "$REQUIRED_CIRCUITS" \ - '{bb_version: $bb, circuits_version: $circuits}' \ - > "${INTEGRATION_NOIR}/version.json" - name: 'Run ${{ matrix.test-suite }} tests' run: 'pnpm test:integration ${{ matrix.test-suite }} --no-prebuild' - name: 'Add test summary' @@ -812,19 +799,6 @@ jobs: - name: Setup ZK prover run: | enclave noir setup - VERSIONS_JSON="crates/zk-prover/versions.json" - NOIR_DIR="$HOME/.enclave/noir" - REQUIRED_BB="$(jq -r '.required_bb_version' "$VERSIONS_JSON")" - REQUIRED_CIRCUITS="$(jq -r '.required_circuits_version' "$VERSIONS_JSON")" - rm -rf "${NOIR_DIR}/circuits" - mkdir -p "${NOIR_DIR}/circuits" - git fetch origin circuit-artifacts - git archive origin/circuit-artifacts | tar -x -C "${NOIR_DIR}/circuits" - jq -n \ - --arg bb "$REQUIRED_BB" \ - --arg circuits "$REQUIRED_CIRCUITS" \ - '{bb_version: $bb, circuits_version: $circuits}' \ - > "${NOIR_DIR}/version.json" - name: Run Playwright tests working-directory: ./examples/CRISP @@ -1256,19 +1230,6 @@ jobs: - name: Setup ZK prover run: | enclave noir setup - VERSIONS_JSON="crates/zk-prover/versions.json" - NOIR_DIR="$HOME/.enclave/noir" - REQUIRED_BB="$(jq -r '.required_bb_version' "$VERSIONS_JSON")" - REQUIRED_CIRCUITS="$(jq -r '.required_circuits_version' "$VERSIONS_JSON")" - rm -rf "${NOIR_DIR}/circuits" - mkdir -p "${NOIR_DIR}/circuits" - git fetch origin circuit-artifacts - git archive origin/circuit-artifacts | tar -x -C "${NOIR_DIR}/circuits" - jq -n \ - --arg bb "$REQUIRED_BB" \ - --arg circuits "$REQUIRED_CIRCUITS" \ - '{bb_version: $bb, circuits_version: $circuits}' \ - > "${NOIR_DIR}/version.json" - name: Verify downloaded artifacts run: | diff --git a/Cargo.lock b/Cargo.lock index 83031dba3..e01e57dd0 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/lib/src/configs/default/mod.nr b/circuits/lib/src/configs/default/mod.nr index 91863d856..76bfde233 100644 --- a/circuits/lib/src/configs/default/mod.nr +++ b/circuits/lib/src/configs/default/mod.nr @@ -4,13 +4,13 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. // -// Auto-generated by build-circuits.ts for preset: secure-8192 +// Auto-generated by build-circuits.ts for preset: insecure-512 // Committee size (N_PARTIES / T / H) is routed through `committee::active`, // which `build-circuits.ts` regenerates atomically with this file. pub use super::committee::active::{H, N_PARTIES, T}; -pub use super::secure::dkg; -pub use super::secure::threshold; +pub use super::insecure::dkg; +pub use super::insecure::threshold; /// Max number of non-zero coefficients in the message polynomial. /// This is a conservative estimate that should be okay for most use cases. diff --git a/crates/zk-prover/Cargo.toml b/crates/zk-prover/Cargo.toml index 670c24591..301341b77 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/circuits/aggregation/node_dkg_fold.rs b/crates/zk-prover/src/circuits/aggregation/node_dkg_fold.rs index c2d6081f5..9970415d8 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), From cdc8880ba86234b2282de696aed069eba32a5fc0 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Fri, 5 Jun 2026 15:40:05 +0200 Subject: [PATCH 08/12] update required circuits version --- crates/zk-prover/versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zk-prover/versions.json b/crates/zk-prover/versions.json index 029030f7d..d6d9d1540 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", From 35e88a2f2331ab8f306c8752d0b26668b24814e1 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Fri, 5 Jun 2026 15:53:57 +0200 Subject: [PATCH 09/12] update download scripts --- crates/zk-prover/src/backend/download.rs | 122 ++++++++++++++++++++++- scripts/build-circuits.ts | 12 ++- 2 files changed, 125 insertions(+), 9 deletions(-) diff --git a/crates/zk-prover/src/backend/download.rs b/crates/zk-prover/src/backend/download.rs index e465b7f83..f2652852f 100644 --- a/crates/zk-prover/src/backend/download.rs +++ b/crates/zk-prover/src/backend/download.rs @@ -19,6 +19,49 @@ 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"]; + +/// Resolve a manifest-relative path against on-disk circuit layouts. +/// +/// Older releases used `{preset}/{variant}/...`. v0.2.0+ archives use +/// `{preset}/{committee}/{variant}/...` while `checksums.json` may still list the +/// legacy flat paths — try committee-prefixed candidates before failing. +fn resolve_circuit_manifest_path(circuits_dir: &Path, rel_path: &str) -> Option { + let direct = circuits_dir.join(rel_path); + if direct.exists() { + return Some(direct); + } + + let mut parts = rel_path.split('/'); + let preset = parts.next()?; + let next = parts.next()?; + if COMMITTEE_SUBDIRS.contains(&next) { + return None; + } + if !CIRCUIT_VARIANT_DIRS.contains(&next) { + return None; + } + let suffix = parts.collect::>().join("/"); + let suffix = if suffix.is_empty() { + String::new() + } else { + format!("/{suffix}") + }; + + for committee in COMMITTEE_SUBDIRS { + let candidate = circuits_dir.join(format!("{preset}/{committee}/{next}{suffix}")); + if candidate.exists() { + return Some(candidate); + } + } + + None +} + impl ZkBackend { pub async fn download_bb(&self) -> Result<(), ZkError> { if self.using_custom_bb { @@ -154,10 +197,8 @@ 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 file_path = resolve_circuit_manifest_path(&self.circuits_dir, rel_path) + .ok_or_else(|| ZkError::CircuitNotFound(rel_path.clone()))?; let data = fs::read(&file_path).await?; verify_checksum(rel_path, &data, Some(expected_hash))?; @@ -244,3 +285,76 @@ 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 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(); + } + + #[test] + fn resolve_circuit_manifest_path_prefers_direct_layout() { + let temp = TempDir::new().unwrap(); + let circuits_dir = temp.path(); + write_file(circuits_dir, "insecure-512/default/dkg/pk/pk.json", b"flat"); + + let resolved = + resolve_circuit_manifest_path(circuits_dir, "insecure-512/default/dkg/pk/pk.json") + .unwrap(); + + assert_eq!( + resolved, + circuits_dir.join("insecure-512/default/dkg/pk/pk.json") + ); + } + + #[test] + fn resolve_circuit_manifest_path_falls_back_to_committee_layout() { + let temp = TempDir::new().unwrap(); + let circuits_dir = temp.path(); + write_file( + circuits_dir, + "insecure-512/micro/default/dkg/pk/pk.json", + b"micro", + ); + + let resolved = + resolve_circuit_manifest_path(circuits_dir, "insecure-512/default/dkg/pk/pk.json") + .unwrap(); + + assert_eq!( + resolved, + circuits_dir.join("insecure-512/micro/default/dkg/pk/pk.json") + ); + } + + #[test] + fn resolve_circuit_manifest_path_accepts_committee_scoped_layout() { + let temp = TempDir::new().unwrap(); + let circuits_dir = temp.path(); + write_file( + circuits_dir, + "insecure-512/micro/default/dkg/pk/pk.json", + b"micro", + ); + + let resolved = resolve_circuit_manifest_path( + circuits_dir, + "insecure-512/micro/default/dkg/pk/pk.json", + ) + .unwrap(); + + assert_eq!( + resolved, + circuits_dir.join("insecure-512/micro/default/dkg/pk/pk.json") + ); + } +} diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index 9c36829f5..1012ade01 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 From c2c33e96a838f4e3dacc95fb2a85632beb27706b Mon Sep 17 00:00:00 2001 From: 0xjei Date: Fri, 5 Jun 2026 16:06:09 +0200 Subject: [PATCH 10/12] update download scripts for checksums --- crates/zk-prover/src/backend/download.rs | 150 +++++++++++++++-------- 1 file changed, 97 insertions(+), 53 deletions(-) diff --git a/crates/zk-prover/src/backend/download.rs b/crates/zk-prover/src/backend/download.rs index f2652852f..4e0e8f398 100644 --- a/crates/zk-prover/src/backend/download.rs +++ b/crates/zk-prover/src/backend/download.rs @@ -25,25 +25,20 @@ const COMMITTEE_SUBDIRS: &[&str] = &["micro", "small", "medium", "large"]; /// Circuit artifact variant directories at `{preset}/{committee?}/{variant}/...`. const CIRCUIT_VARIANT_DIRS: &[&str] = &["default", "evm", "recursive"]; -/// Resolve a manifest-relative path against on-disk circuit layouts. -/// -/// Older releases used `{preset}/{variant}/...`. v0.2.0+ archives use -/// `{preset}/{committee}/{variant}/...` while `checksums.json` may still list the -/// legacy flat paths — try committee-prefixed candidates before failing. -fn resolve_circuit_manifest_path(circuits_dir: &Path, rel_path: &str) -> Option { +/// 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); - if direct.exists() { - return Some(direct); - } + let mut candidates = vec![direct.clone()]; let mut parts = rel_path.split('/'); - let preset = parts.next()?; - let next = parts.next()?; - if COMMITTEE_SUBDIRS.contains(&next) { - return None; - } - if !CIRCUIT_VARIANT_DIRS.contains(&next) { - return None; + 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() { @@ -53,13 +48,44 @@ fn resolve_circuit_manifest_path(circuits_dir: &Path, rel_path: &str) -> Option< }; for committee in COMMITTEE_SUBDIRS { - let candidate = circuits_dir.join(format!("{preset}/{committee}/{next}{suffix}")); - if candidate.exists() { - return Some(candidate); + candidates.push(circuits_dir.join(format!("{preset}/{committee}/{next}{suffix}"))); + } + + candidates +} + +/// Resolve a manifest path to file bytes, matching `expected_hash` when multiple committee +/// copies exist (v0.2.0 flat checksums vs per-committee tarball layout). +async fn read_manifest_file( + circuits_dir: &Path, + rel_path: &str, + expected_hash: &str, +) -> Result, ZkError> { + 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(data), + 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, + }); } - None + Err(ZkError::CircuitNotFound(rel_path.to_string())) } impl ZkBackend { @@ -197,11 +223,7 @@ impl ZkBackend { let mut circuit_infos = HashMap::new(); for (rel_path, expected_hash) in &manifest.files { - let file_path = resolve_circuit_manifest_path(&self.circuits_dir, rel_path) - .ok_or_else(|| 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(), @@ -289,6 +311,7 @@ async fn download_with_progress(url: &str, message: &str) -> Result, ZkE #[cfg(test)] mod tests { use super::*; + use sha2::{Digest, Sha256}; use std::fs; use tempfile::TempDir; @@ -300,61 +323,82 @@ mod tests { fs::write(path, contents).unwrap(); } - #[test] - fn resolve_circuit_manifest_path_prefers_direct_layout() { + 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(); - write_file(circuits_dir, "insecure-512/default/dkg/pk/pk.json", b"flat"); + let contents = b"flat"; + write_file( + circuits_dir, + "insecure-512/default/dkg/pk/pk.json", + contents, + ); + let hash = sha256_hex(contents); - let resolved = - resolve_circuit_manifest_path(circuits_dir, "insecure-512/default/dkg/pk/pk.json") - .unwrap(); + let data = read_manifest_file( + circuits_dir, + "insecure-512/default/dkg/pk/pk.json", + &hash, + ) + .await + .unwrap(); - assert_eq!( - resolved, - circuits_dir.join("insecure-512/default/dkg/pk/pk.json") - ); + assert_eq!(data, contents); } - #[test] - fn resolve_circuit_manifest_path_falls_back_to_committee_layout() { + #[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", - b"micro", + micro, + ); + write_file( + circuits_dir, + "insecure-512/large/default/dkg/pk/pk.json", + large, ); + let large_hash = sha256_hex(large); - let resolved = - resolve_circuit_manifest_path(circuits_dir, "insecure-512/default/dkg/pk/pk.json") - .unwrap(); + let data = read_manifest_file( + circuits_dir, + "insecure-512/default/dkg/pk/pk.json", + &large_hash, + ) + .await + .unwrap(); - assert_eq!( - resolved, - circuits_dir.join("insecure-512/micro/default/dkg/pk/pk.json") - ); + assert_eq!(data, large); } - #[test] - fn resolve_circuit_manifest_path_accepts_committee_scoped_layout() { + #[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", - b"micro", + contents, ); + let hash = sha256_hex(contents); - let resolved = resolve_circuit_manifest_path( + let data = read_manifest_file( circuits_dir, "insecure-512/micro/default/dkg/pk/pk.json", + &hash, ) + .await .unwrap(); - assert_eq!( - resolved, - circuits_dir.join("insecure-512/micro/default/dkg/pk/pk.json") - ); + assert_eq!(data, contents); } } From d888b3c13aafab0d72342565262a168a74221993 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Fri, 5 Jun 2026 16:07:27 +0200 Subject: [PATCH 11/12] cargo fmt --- crates/zk-prover/src/backend/download.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/zk-prover/src/backend/download.rs b/crates/zk-prover/src/backend/download.rs index 4e0e8f398..960b1ac94 100644 --- a/crates/zk-prover/src/backend/download.rs +++ b/crates/zk-prover/src/backend/download.rs @@ -339,13 +339,9 @@ mod tests { ); let hash = sha256_hex(contents); - let data = read_manifest_file( - circuits_dir, - "insecure-512/default/dkg/pk/pk.json", - &hash, - ) - .await - .unwrap(); + let data = read_manifest_file(circuits_dir, "insecure-512/default/dkg/pk/pk.json", &hash) + .await + .unwrap(); assert_eq!(data, contents); } From 2736b720a5d5b441322425f2c5064ac11c289ff9 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Fri, 5 Jun 2026 16:19:36 +0200 Subject: [PATCH 12/12] update layouts based on committee --- crates/zk-prover/src/backend/download.rs | 28 ++++++++++++++---- crates/zk-prover/tests/integration_tests.rs | 32 ++++++++++++++------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/crates/zk-prover/src/backend/download.rs b/crates/zk-prover/src/backend/download.rs index 960b1ac94..d4319e9bf 100644 --- a/crates/zk-prover/src/backend/download.rs +++ b/crates/zk-prover/src/backend/download.rs @@ -54,13 +54,13 @@ fn circuit_manifest_candidates(circuits_dir: &Path, rel_path: &str) -> Vec Result, ZkError> { +) -> Result { let mut last_mismatch: Option<(PathBuf, String)> = None; for candidate in circuit_manifest_candidates(circuits_dir, rel_path) { @@ -69,7 +69,7 @@ async fn read_manifest_file( } let data = fs::read(&candidate).await?; match verify_checksum(rel_path, &data, Some(expected_hash)) { - Ok(()) => return Ok(data), + Ok(()) => return Ok(candidate), Err(ZkError::ChecksumMismatch { actual, .. }) => { last_mismatch = Some((candidate, actual)); } @@ -77,7 +77,7 @@ async fn read_manifest_file( } } - if let Some((path, actual)) = last_mismatch { + if let Some((_path, actual)) = last_mismatch { return Err(ZkError::ChecksumMismatch { file: rel_path.to_string(), expected: expected_hash.to_string(), @@ -88,7 +88,25 @@ async fn read_manifest_file( 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"); diff --git a/crates/zk-prover/tests/integration_tests.rs b/crates/zk-prover/tests/integration_tests.rs index 2286473a3..9a2b62d2c 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();