From 6308d735e537b790d3de2419d667ff2e8ceecbbc Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 6 Feb 2026 11:45:33 +0100 Subject: [PATCH 1/2] refactor: add pk-aggregation circuit --- crates/polynomial/src/crt_polynomial.rs | 11 + .../zk-helpers/src/circuits/threshold/mod.rs | 1 + .../threshold/pk_aggregation/circuit.rs | 30 +++ .../threshold/pk_aggregation/codegen.rs | 144 ++++++++++ .../threshold/pk_aggregation/computation.rs | 254 ++++++++++++++++++ .../circuits/threshold/pk_aggregation/mod.rs | 19 ++ .../threshold/pk_aggregation/sample.rs | 96 +++++++ .../threshold/pk_generation/circuit.rs | 2 +- .../threshold/pk_generation/computation.rs | 62 ++--- .../circuits/threshold/pk_generation/mod.rs | 2 +- .../threshold/pk_generation/sample.rs | 15 +- 11 files changed, 598 insertions(+), 38 deletions(-) create mode 100644 crates/zk-helpers/src/circuits/threshold/pk_aggregation/circuit.rs create mode 100644 crates/zk-helpers/src/circuits/threshold/pk_aggregation/codegen.rs create mode 100644 crates/zk-helpers/src/circuits/threshold/pk_aggregation/computation.rs create mode 100644 crates/zk-helpers/src/circuits/threshold/pk_aggregation/mod.rs create mode 100644 crates/zk-helpers/src/circuits/threshold/pk_aggregation/sample.rs diff --git a/crates/polynomial/src/crt_polynomial.rs b/crates/polynomial/src/crt_polynomial.rs index ceee0a1247..2089689f93 100644 --- a/crates/polynomial/src/crt_polynomial.rs +++ b/crates/polynomial/src/crt_polynomial.rs @@ -167,6 +167,17 @@ impl CrtPolynomial { } } + /// Multiplies each limb's coefficients by a scalar. + /// + /// # Arguments + /// + /// * `scalar` - The scalar to multiply each coefficient by. + pub fn scalar_mul(&mut self, scalar: &BigInt) { + for limb in &mut self.limbs { + limb.scalar_mul(scalar); + } + } + /// Adds a limb to the CRT polynomial. /// /// # Arguments diff --git a/crates/zk-helpers/src/circuits/threshold/mod.rs b/crates/zk-helpers/src/circuits/threshold/mod.rs index 4d50273c2c..cdd8d3a606 100644 --- a/crates/zk-helpers/src/circuits/threshold/mod.rs +++ b/crates/zk-helpers/src/circuits/threshold/mod.rs @@ -4,5 +4,6 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +pub mod pk_aggregation; pub mod pk_generation; pub mod user_data_encryption; diff --git a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/circuit.rs b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/circuit.rs new file mode 100644 index 0000000000..0b5e5f6799 --- /dev/null +++ b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/circuit.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::computation::DkgInputType; +use crate::registry::Circuit; +use crate::CiphernodesCommittee; +use e3_fhe_params::ParameterType; +use e3_polynomial::CrtPolynomial; +use fhe::bfv::PublicKey; + +#[derive(Debug)] +pub struct PkAggregationCircuit; + +impl Circuit for PkAggregationCircuit { + const NAME: &'static str = "pk-aggregation"; + const PREFIX: &'static str = "PK_AGGREGATION"; + const SUPPORTED_PARAMETER: ParameterType = ParameterType::THRESHOLD; + const DKG_INPUT_TYPE: Option = None; +} + +#[derive(Debug, Clone)] +pub struct PkAggregationCircuitInput { + pub committee: CiphernodesCommittee, + pub public_key: PublicKey, + pub pk0_shares: Vec, + pub a: CrtPolynomial, +} diff --git a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/codegen.rs b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/codegen.rs new file mode 100644 index 0000000000..91c2eb29e7 --- /dev/null +++ b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/codegen.rs @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! Code generation for the public-key BFV circuit: Prover.toml and configs.nr. + +use e3_fhe_params::BfvPreset; + +use crate::circuits::computation::Computation; +use crate::threshold::pk_aggregation::circuit::PkAggregationCircuit; +use crate::threshold::pk_aggregation::computation::{Configs, Witness}; +use crate::threshold::pk_aggregation::PkAggregationCircuitInput; +use crate::utils::join_display; +use crate::CircuitCodegen; +use crate::CircuitsErrors; +use crate::{Artifacts, CodegenToml}; +use crate::{Circuit, CodegenConfigs}; + +/// Implementation of [`CircuitCodegen`] for [`PkAggregationCircuit`]. +impl CircuitCodegen for PkAggregationCircuit { + type Preset = BfvPreset; + type Input = PkAggregationCircuitInput; + type Error = CircuitsErrors; + + fn codegen(&self, preset: Self::Preset, input: &Self::Input) -> Result { + let witness = Witness::compute(preset, input)?; + let configs = Configs::compute(preset, &())?; + + let toml = generate_toml(witness)?; + let configs = generate_configs(preset, &configs); + + Ok(Artifacts { toml, configs }) + } +} + +pub fn generate_toml(witness: Witness) -> Result { + let json = witness + .to_json() + .map_err(|e| CircuitsErrors::SerdeJson(e))?; + + Ok(toml::to_string(&json)?) +} + +pub fn generate_configs(_preset: BfvPreset, configs: &Configs) -> CodegenConfigs { + let prefix = ::PREFIX; + + let qis_str = join_display(&configs.moduli, ", "); + + format!( + r#"use crate::core::threshold::pk_aggregation::Configs as PkAggregationConfigs; + +// Global configs for Public Key Aggregation TRBFV circuit +pub global N: u32 = {}; +pub global L: u32 = {}; +pub global QIS: [Field; L] = [{}]; + +/************************************ +------------------------------------- +pk_agg_trbfv (CIRCUIT 5 - PUBLIC KEY AGGREGATION TRBFV) +------------------------------------- +************************************/ + +// pk_agg_trbfv - bit parameters +pub global {}_BIT_PK: u32 = {}; + +// pk_agg_trbfv - configs +pub global {}_CONFIGS: PkAggregationConfigs = PkAggregationConfigs::new( + QIS, +); +"#, + configs.n, // N + configs.l, // L + qis_str, // QIS array + prefix, // _BIT_PK + configs.bits.pk_bit, // _BIT_PK + prefix, // _CONFIGS + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::CiphernodesCommitteeSize; + + #[test] + fn test_toml_generation_and_structure() { + let preset = BfvPreset::InsecureThreshold512; + let committee = CiphernodesCommitteeSize::Small.values(); + let prefix: &str = ::PREFIX; + + let sample = PkAggregationCircuitInput::generate_sample(preset, committee).unwrap(); + let witness = Witness::compute(preset, &sample).unwrap(); + let configs = Configs::compute(preset, &()).unwrap(); + + let qis_str = join_display(&configs.moduli, ", "); + + let parsed: serde_json::Value = witness.to_json().unwrap(); + let pk0 = parsed + .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(witness).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())); + assert!(codegen_configs + .contains(format!("{}_BIT_PK: u32 = {}", prefix, configs.bits.pk_bit).as_str())); + assert!(codegen_configs.contains( + format!( + "{}_CONFIGS: PkAggregationConfigs = PkAggregationConfigs::new(", + prefix + ) + .as_str() + )); + assert!(codegen_configs.contains("QIS,")); + assert!(codegen_configs.contains(format!("QIS: [Field; L] = [{}];", qis_str).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 new file mode 100644 index 0000000000..ee8be3c5b2 --- /dev/null +++ b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/computation.rs @@ -0,0 +1,254 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! Computation types for the public key aggregation circuit: constants, bounds, bit widths, and witness. +//! +//! [`Configs`], [`Bounds`], [`Bits`], and [`Witness`] are produced from BFV parameters +//! and (for witness) public key shares and aggregated public key. They implement [`Computation`] and are used by codegen. + +use crate::compute_pk_aggregation_commitment; +use crate::compute_pk_bit; +use crate::crt_polynomial_to_toml_json; +use crate::get_zkp_modulus; +use crate::threshold::pk_aggregation::circuit::PkAggregationCircuit; +use crate::threshold::pk_aggregation::circuit::PkAggregationCircuitInput; +use crate::CircuitsErrors; +use crate::{CircuitComputation, Computation}; +use e3_fhe_params::build_pair_for_preset; +use e3_fhe_params::BfvPreset; +use e3_polynomial::CrtPolynomial; +use num_bigint::BigInt; +use num_bigint::BigUint; +use serde::{Deserialize, Serialize}; + +/// Output of [`CircuitComputation::compute`] for [`PkAggregationCircuit`]: bounds, bit widths, and witness. +#[derive(Debug)] +pub struct PkAggregationComputationOutput { + pub bounds: Bounds, + pub bits: Bits, + pub witness: Witness, +} + +/// Implementation of [`CircuitComputation`] for [`PkAggregationCircuit`]. +impl CircuitComputation for PkAggregationCircuit { + type Preset = BfvPreset; + type Input = PkAggregationCircuitInput; + type Output = PkAggregationComputationOutput; + type Error = CircuitsErrors; + + fn compute(preset: Self::Preset, input: &Self::Input) -> Result { + let bounds = Bounds::compute(preset, &())?; + let bits = Bits::compute(preset, &())?; + let witness = Witness::compute(preset, &input)?; + + Ok(PkAggregationComputationOutput { + bounds, + bits, + witness, + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Configs { + pub n: usize, + pub l: usize, + pub moduli: Vec, + pub bits: Bits, + pub bounds: Bounds, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Bits { + pub pk_bit: u32, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Bounds { + pub pk_bound: BigUint, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Witness { + pub expected_pk_commitments: Vec, + pub pk0: Vec, + pub pk1: Vec, + pub pk0_agg: CrtPolynomial, + pub pk1_agg: CrtPolynomial, +} + +impl Computation for Configs { + type Preset = BfvPreset; + type Input = (); + type Error = CircuitsErrors; + + fn compute(preset: Self::Preset, _: &Self::Input) -> Result { + let (threshold_params, _) = + build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Other(e.to_string()))?; + + let moduli = threshold_params.moduli().to_vec(); + + let bounds = Bounds::compute(preset, &())?; + let bits = Bits::compute(preset, &())?; + + Ok(Configs { + n: threshold_params.degree(), + l: moduli.len(), + moduli, + bits, + bounds, + }) + } +} + +impl Computation for Bits { + type Preset = BfvPreset; + type Input = (); + type Error = CircuitsErrors; + + fn compute(preset: Self::Preset, _: &Self::Input) -> Result { + let (threshold_params, _) = + build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Other(e.to_string()))?; + + let pk_bit = compute_pk_bit(&threshold_params); + + Ok(Bits { pk_bit }) + } +} + +impl Computation for Bounds { + type Preset = BfvPreset; + type Input = (); + type Error = CircuitsErrors; + + fn compute(preset: Self::Preset, _: &Self::Input) -> Result { + let (threshold_params, _) = + build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Other(e.to_string()))?; + + let mut pk_bound_max = BigUint::from(0u32); + + for &qi in threshold_params.moduli() { + let qi_bound: BigUint = (&BigUint::from(qi) - 1u32) / 2u32; + + if qi_bound > pk_bound_max { + pk_bound_max = qi_bound; + } + } + + let bounds = Bounds { + pk_bound: pk_bound_max, + }; + + Ok(bounds) + } +} + +impl Computation for Witness { + type Preset = BfvPreset; + type Input = PkAggregationCircuitInput; + type Error = CircuitsErrors; + + fn compute(preset: Self::Preset, input: &Self::Input) -> Result { + let (threshold_params, _) = + build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Other(e.to_string()))?; + + let bit_pk = compute_pk_bit(&threshold_params); + let moduli = threshold_params.moduli(); + let zkp_modulus = &get_zkp_modulus(); + + // Coefficients must be in [0, q_i), not centered to (-q_i/2, q_i/2]. The circuit sums + // party coefficients then applies reduce_mod to get a value in [0, q_l); the aggregated + // key is also in [0, q_i). Centered representatives would make the sum before reduction + // inconsistent and could break the aggregation check. + + let mut pk0: Vec = input.pk0_shares.clone(); + // pk1 is the same (common random polynomial a) for all parties + let mut pk1: Vec = (0..input.committee.h).map(|_| input.a.clone()).collect(); + // Extract pk0_agg from aggregated public key + let mut pk0_agg = CrtPolynomial::from_fhe_polynomial(&input.public_key.c.c[0]); + let mut pk1_agg = input.a.clone(); + + // Compute expected_pk_commitments for each honest party + // Each commitment is computed from pk0[i] and pk1[i] for party i + let mut expected_pk_commitments = Vec::new(); + + pk0_agg.reverse(); + pk0_agg.reduce(moduli)?; + pk0_agg.reduce_uniform(zkp_modulus); + + pk1_agg.reverse(); + pk1_agg.scalar_mul(&BigInt::from(input.committee.h)); + pk1_agg.reduce(moduli)?; + pk1_agg.reduce_uniform(zkp_modulus); + + for party_index in 0..input.committee.h { + pk0[party_index].reverse(); + pk0[party_index].reduce(moduli)?; + pk0[party_index].reduce_uniform(zkp_modulus); + + pk1[party_index].reverse(); + pk1[party_index].reduce(moduli)?; + pk1[party_index].reduce_uniform(zkp_modulus); + + let commitment = + compute_pk_aggregation_commitment(&pk0[party_index], &pk1[party_index], bit_pk); + + expected_pk_commitments.push(commitment); + } + + Ok(Witness { + expected_pk_commitments, + pk0, + pk1: pk1.clone(), + pk0_agg, + pk1_agg, + }) + } + + fn to_json(&self) -> serde_json::Result { + let pk0: Vec> = self + .pk0 + .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 json = serde_json::json!({ + "expected_pk_commitments": self.expected_pk_commitments, + "pk0": pk0, + "pk1": pk1, + "pk0_agg": pk0_agg, + "pk1_agg": pk1_agg, + }); + + Ok(json) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bound_and_bits_computation_consistency() { + let preset = BfvPreset::InsecureThreshold512; + let (threshold_params, _) = build_pair_for_preset(preset).unwrap(); + + let bounds = Bounds::compute(preset, &()).unwrap(); + let bits = Bits::compute(preset, &()).unwrap(); + + let expected_bits = compute_pk_bit(&threshold_params); + + assert_eq!(bounds.pk_bound, BigUint::from(34359701504u128)); + assert_eq!(bits.pk_bit, expected_bits); + } +} diff --git a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/mod.rs b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/mod.rs new file mode 100644 index 0000000000..5840366d8b --- /dev/null +++ b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/mod.rs @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! Public key aggregation circuit. +//! +//! This circuit proves public key aggregation with a threshold BFV public key (pk0, pk1) and produces +//! Prover.toml and configs.nr for the Noir prover. See [`PkAggregationCircuit`] and +//! [`PkAggregationCircuitInput`]. + +pub mod circuit; +pub mod codegen; +pub mod computation; +pub mod sample; +pub use circuit::*; +pub use codegen::*; +pub use computation::*; diff --git a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/sample.rs b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/sample.rs new file mode 100644 index 0000000000..72d3c81390 --- /dev/null +++ b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/sample.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! Sample data generation for pk aggregation circuit. +//! +//! [`Sample`] produces a random BFV public key shares from H honest parties and the aggregated public key; +//! the public key shares and aggregated public key are used as input for codegen and tests. + +use crate::{ + threshold::pk_aggregation::PkAggregationCircuitInput, CiphernodesCommittee, CircuitsErrors, +}; +use e3_fhe_params::{build_pair_for_preset, BfvPreset}; +use e3_polynomial::CrtPolynomial; +use fhe::mbfv::{AggregateIter, PublicKeyShare}; +use fhe::{ + bfv::{PublicKey, SecretKey}, + mbfv::CommonRandomPoly, +}; +use rand::rngs::OsRng; +use rand::thread_rng; + +impl PkAggregationCircuitInput { + pub fn generate_sample( + preset: BfvPreset, + committee: CiphernodesCommittee, + ) -> Result { + let (threshold_params, _) = build_pair_for_preset(preset).map_err(|e| { + CircuitsErrors::Sample(format!("Failed to build pair for preset: {:?}", e)) + })?; + + let mut rng = OsRng; + let mut thread_rng = thread_rng(); + + let crp = CommonRandomPoly::new(&threshold_params, &mut rng) + .map_err(|e| CircuitsErrors::Sample(format!("Failed to create CRP: {:?}", e)))?; + + // Generate public key shares for each party + let mut pk_shares = Vec::new(); + let mut pk0_shares = Vec::new(); + + for _ in 0..committee.h { + let sk = SecretKey::random(&threshold_params, &mut rng); + // Create PublicKeyShare - this generates the p0_share with a specific error term + let pk_share = PublicKeyShare::new(&sk, crp.clone(), &mut thread_rng).map_err(|e| { + CircuitsErrors::Sample(format!("Failed to create public key share: {:?}", e)) + })?; + + // Extract the p0_share Poly from the PublicKeyShare + // This ensures we use the same error term for both aggregation and vector extraction + let pk0_share = CrtPolynomial::from_fhe_polynomial(&pk_share.p0_share()); + + pk_shares.push(pk_share); + pk0_shares.push(pk0_share); + } + + // Aggregate public key shares to get the full public key + let public_key: PublicKey = pk_shares.iter().cloned().aggregate().map_err(|e| { + CircuitsErrors::Sample(format!("Failed to aggregate public key: {:?}", e)) + })?; + + Ok(PkAggregationCircuitInput { + committee, + public_key, + pk0_shares, + a: CrtPolynomial::from_fhe_polynomial(&crp.poly()), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + computation::Computation, + threshold::pk_generation::{PkGenerationCircuitInput, Witness}, + CiphernodesCommitteeSize, + }; + + use e3_fhe_params::DEFAULT_BFV_PRESET; + + #[test] + fn test_generate_sample() { + let committee = CiphernodesCommitteeSize::Small.values(); + let sample = + PkGenerationCircuitInput::generate_sample(DEFAULT_BFV_PRESET, committee).unwrap(); + let witness = Witness::compute(DEFAULT_BFV_PRESET, &sample).unwrap(); + + assert_eq!(witness.pk0is.limbs.len(), 2); + assert_eq!(witness.a.limbs.len(), 2); + assert_eq!(witness.e_sm.limbs.len(), 2); + assert_eq!(witness.r1is.limbs.len(), 2); + assert_eq!(witness.r2is.limbs.len(), 2); + } +} diff --git a/crates/zk-helpers/src/circuits/threshold/pk_generation/circuit.rs b/crates/zk-helpers/src/circuits/threshold/pk_generation/circuit.rs index d68b553305..2b2477b889 100644 --- a/crates/zk-helpers/src/circuits/threshold/pk_generation/circuit.rs +++ b/crates/zk-helpers/src/circuits/threshold/pk_generation/circuit.rs @@ -23,7 +23,7 @@ impl Circuit for PkGenerationCircuit { #[derive(Debug, Clone)] pub struct PkGenerationCircuitInput { pub committee: CiphernodesCommittee, - pub pk_share: CrtPolynomial, + pub pk0_share: CrtPolynomial, pub a: CrtPolynomial, pub eek: CrtPolynomial, pub e_sm: CrtPolynomial, diff --git a/crates/zk-helpers/src/circuits/threshold/pk_generation/computation.rs b/crates/zk-helpers/src/circuits/threshold/pk_generation/computation.rs index 5df6020917..db09ab92fb 100644 --- a/crates/zk-helpers/src/circuits/threshold/pk_generation/computation.rs +++ b/crates/zk-helpers/src/circuits/threshold/pk_generation/computation.rs @@ -109,7 +109,7 @@ impl Computation for Configs { fn compute(preset: Self::Preset, input: &Self::Input) -> Result { let (threshold_params, _) = - build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Sample(e.to_string()))?; + build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Other(e.to_string()))?; let moduli = threshold_params.moduli().to_vec(); @@ -168,7 +168,7 @@ impl Computation for Bounds { fn compute(preset: Self::Preset, input: &Self::Input) -> Result { let (threshold_params, _) = - build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Sample(e.to_string()))?; + build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Other(e.to_string()))?; let n = BigInt::from(threshold_params.degree()); let ctx = threshold_params.ctx_at_level(0)?; @@ -180,7 +180,7 @@ impl Computation for Bounds { let defaults = preset .search_defaults() - .ok_or_else(|| CircuitsErrors::Sample("missing search defaults".to_string()))?; + .ok_or_else(|| CircuitsErrors::Other("missing search defaults".to_string()))?; let num_ciphertexts = defaults.z; let smudging_config = SmudgingBoundCalculatorConfig::new( @@ -245,7 +245,7 @@ impl Computation for Witness { fn compute(preset: Self::Preset, input: &Self::Input) -> Result { let (threshold_params, _) = - build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Sample(e.to_string()))?; + build_pair_for_preset(preset).map_err(|e| CircuitsErrors::Other(e.to_string()))?; let moduli: Vec = threshold_params .moduli() @@ -271,7 +271,7 @@ impl Computation for Witness { Polynomial, )> = izip!( moduli.clone(), - input.pk_share.limbs.clone(), + input.pk0_share.limbs.clone(), input.a.limbs.clone(), input.eek.limbs.clone(), input.e_sm.limbs.clone(), @@ -280,10 +280,10 @@ impl Computation for Witness { .enumerate() .par_bridge() .map( - |(i, (qi, mut pk_share, mut a, mut eek, mut e_sm, mut sk))| { - pk_share.reverse(); - pk_share.reduce(&qi); - pk_share.center(&qi); + |(i, (qi, mut pk0_share, mut a, mut eek, mut e_sm, mut sk))| { + pk0_share.reverse(); + pk0_share.reduce(&qi); + pk0_share.center(&qi); a.reverse(); a.center(&qi); @@ -297,8 +297,8 @@ impl Computation for Witness { sk.reverse(); sk.center(&qi); - // Calculate pk_share_hat = -a * sk + e - let pk_share_hat = { + // Calculate pk0_share_hat = -a * sk + eek + let pk0_share_hat = { let mut exp = a.neg(); exp = exp.mul(&sk); @@ -307,18 +307,18 @@ impl Computation for Witness { exp.add(&eek) }; - assert_eq!((pk_share_hat.coefficients().len() as u64) - 1, 2 * (n - 1)); + assert_eq!((pk0_share_hat.coefficients().len() as u64) - 1, 2 * (n - 1)); - // Check whether pk_share_hat mod R_qi (the ring) is equal to pk_share - let mut pk_share_hat_mod_rqi = pk_share_hat.reduce_by_cyclotomic(&cyclo).unwrap(); + // Check whether pk0_share_hat mod R_qi (the ring) is equal to pk0_share + let mut pk0_share_hat_mod_rqi = pk0_share_hat.reduce_by_cyclotomic(&cyclo).unwrap(); - pk_share_hat_mod_rqi.reduce(&qi); - pk_share_hat_mod_rqi.center(&qi); + pk0_share_hat_mod_rqi.reduce(&qi); + pk0_share_hat_mod_rqi.center(&qi); - assert_eq!(&pk_share, &pk_share_hat_mod_rqi); + assert_eq!(&pk0_share, &pk0_share_hat_mod_rqi); - // Compute r2_numerator = pk_share - pk_share_hat and reduce/center the polynomial - let r2_numerator = pk_share.sub(&pk_share_hat); + // Compute r2_numerator = pk0_share - pk0_share_hat and reduce/center the polynomial + let r2_numerator = pk0_share.sub(&pk0_share_hat); assert_eq!((r2_numerator.coefficients().len() as u64) - 1, 2 * (n - 1)); @@ -327,14 +327,14 @@ impl Computation for Witness { r2_numerator_centered.center(&qi); // Compute r2 as the quotient of numerator divided by the cyclotomic polynomial - // to produce: (pk_share - pk_share_hat) / (x^N + 1) mod Z_qi. Remainder should be empty. + // to produce: (pk0_share - pk0_share_hat) / (x^N + 1) mod Z_qi. Remainder should be empty. let cyclo_polynomial = Polynomial::new(cyclo.clone()); let (r2, r2_rem) = r2_numerator_centered.div(&cyclo_polynomial).unwrap(); assert!(r2_rem.is_zero()); assert_eq!((r2.coefficients().len() as u64) - 1, n - 2); // Order(r2i) = N - 2 - // Assert that (pk_share - pk_share_hat) = (r2 * cyclo) mod Z_qi + // Assert that (pk0_share - pk0_share_hat) = (r2 * cyclo) mod Z_qi let r2_cyclo_times = r2.mul(&cyclo_polynomial); let mut r2_cyclo_times_centered = r2_cyclo_times.clone(); @@ -347,7 +347,7 @@ impl Computation for Witness { 2 * (n - 1) ); - // Calculate r1 = (pk_share - pk_share_hat - r2 * cyclo) / qi mod Z_p. Remainder should be empty. + // Calculate r1 = (pk0_share - pk0_share_hat - r2 * cyclo) / qi mod Z_p. Remainder should be empty. let r1_numerator = r2_numerator.sub(&r2_cyclo_times); assert_eq!((r2_numerator.coefficients().len() as u64) - 1, 2 * (n - 1)); @@ -360,13 +360,13 @@ impl Computation for Witness { assert_eq!(&r1_numerator, &r1.mul(&qi_polynomial)); - // Assert that pk_share = (pk_share_hat + r1 * qi + r2 * cyclo) mod R_qi + // Assert that pk0_share = (pk0_share_hat + r1 * qi + r2 * cyclo) mod R_qi let r1_qi_times = r1.scalar_mul(&qi); - let pk_share_calculated = pk_share_hat.add(&r1_qi_times).add(&r2_cyclo_times); + let pk0_share_calculated = pk0_share_hat.add(&r1_qi_times).add(&r2_cyclo_times); - assert_eq!(&pk_share, &pk_share_calculated.trim_leading_zeros()); + assert_eq!(&pk0_share, &pk0_share_calculated.trim_leading_zeros()); - (i, r2, r1, pk_share.clone(), a.clone(), e_sm.clone()) + (i, r2, r1, pk0_share.clone(), a.clone(), e_sm.clone()) }, ) .collect(); @@ -375,7 +375,7 @@ impl Computation for Witness { let mut r2 = CrtPolynomial::new(vec![]); let mut r1 = CrtPolynomial::new(vec![]); - let mut pk_share = CrtPolynomial::new(vec![]); + let mut pk0_share = CrtPolynomial::new(vec![]); let mut a = CrtPolynomial::new(vec![]); let mut e_sm = CrtPolynomial::new(vec![]); @@ -387,17 +387,17 @@ impl Computation for Witness { eek.reverse(); eek.center(&moduli[0]); - for (_i, r2i, r1i, pk_sharei, ai, e_smi) in results { + for (_i, r2i, r1i, pk0_sharei, ai, e_smi) in results { r2.add_limb(r2i); r1.add_limb(r1i); - pk_share.add_limb(pk_sharei); + pk0_share.add_limb(pk0_sharei); a.add_limb(ai); e_sm.add_limb(e_smi); } let zkp_modulus = &get_zkp_modulus(); - pk_share.reduce_uniform(zkp_modulus); + pk0_share.reduce_uniform(zkp_modulus); a.reduce_uniform(zkp_modulus); r1.reduce_uniform(zkp_modulus); r2.reduce_uniform(zkp_modulus); @@ -412,7 +412,7 @@ impl Computation for Witness { e_sm, r1is: r1, r2is: r2, - pk0is: pk_share, + pk0is: pk0_share, pk1is: a, }) } diff --git a/crates/zk-helpers/src/circuits/threshold/pk_generation/mod.rs b/crates/zk-helpers/src/circuits/threshold/pk_generation/mod.rs index c288f074ad..65bbc34de3 100644 --- a/crates/zk-helpers/src/circuits/threshold/pk_generation/mod.rs +++ b/crates/zk-helpers/src/circuits/threshold/pk_generation/mod.rs @@ -6,7 +6,7 @@ //! Public key generation circuit. //! -//! This circuit proves public key generation with a BFV public key (pk0, pk1) and produces +//! This circuit proves public key generation with a threshold BFV public key (pk0, pk1) and produces //! Prover.toml and configs.nr for the Noir prover. See [`PkGenerationCircuit`] and //! [`PkGenerationCircuitInput`]. diff --git a/crates/zk-helpers/src/circuits/threshold/pk_generation/sample.rs b/crates/zk-helpers/src/circuits/threshold/pk_generation/sample.rs index dc4d9185c4..f7c53f8155 100644 --- a/crates/zk-helpers/src/circuits/threshold/pk_generation/sample.rs +++ b/crates/zk-helpers/src/circuits/threshold/pk_generation/sample.rs @@ -28,15 +28,20 @@ impl PkGenerationCircuitInput { preset: BfvPreset, committee: CiphernodesCommittee, ) -> Result { - let (threshold_params, _) = build_pair_for_preset(preset).unwrap(); + let (threshold_params, _) = build_pair_for_preset(preset).map_err(|e| { + CircuitsErrors::Sample(format!("Failed to build pair for preset: {:?}", e)) + })?; let mut rng = thread_rng(); let secret_key = SecretKey::random(&threshold_params, &mut rng); - let crp = CommonRandomPoly::new(&threshold_params, &mut rng).unwrap(); + let crp = CommonRandomPoly::new(&threshold_params, &mut rng) + .map_err(|e| CircuitsErrors::Sample(format!("Failed to create CRP: {:?}", e)))?; - let (public_key_share, a, sk, e) = - PublicKeyShare::new_extended(&secret_key, crp.clone(), &mut rng).unwrap(); + let (pk0_share, a, sk, e) = + PublicKeyShare::new_extended(&secret_key, crp.clone(), &mut rng).map_err(|e| { + CircuitsErrors::Sample(format!("Failed to create public key share: {:?}", e)) + })?; let num_parties = committee.n; let threshold = committee.threshold; @@ -65,7 +70,7 @@ impl PkGenerationCircuitInput { Ok(PkGenerationCircuitInput { committee, - pk_share: CrtPolynomial::from_fhe_polynomial(&public_key_share), + pk0_share: CrtPolynomial::from_fhe_polynomial(&pk0_share), a: CrtPolynomial::from_fhe_polynomial(&a), eek: CrtPolynomial::from_fhe_polynomial(&e), e_sm: CrtPolynomial::from_fhe_polynomial(&e_sm), From a8109c95c548af412ca71a2ed43af1c3240b3242 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 6 Feb 2026 13:19:53 +0100 Subject: [PATCH 2/2] fix: computation bugs in pk-aggregation circuit --- circuits/lib/src/configs/committee/small.nr | 2 +- .../lib/src/configs/insecure/threshold.nr | 4 +-- circuits/lib/src/configs/secure/threshold.nr | 4 +-- crates/polynomial/src/crt_polynomial.rs | 2 +- crates/zk-helpers/src/bin/zk_cli.rs | 11 ++++++ .../threshold/pk_aggregation/codegen.rs | 19 ++++------ .../threshold/pk_aggregation/computation.rs | 17 +++++---- .../threshold/pk_aggregation/sample.rs | 24 +++++++------ crates/zk-helpers/src/utils.rs | 36 +++++++++++++------ 9 files changed, 71 insertions(+), 48 deletions(-) diff --git a/circuits/lib/src/configs/committee/small.nr b/circuits/lib/src/configs/committee/small.nr index 9d037d65f7..93dc81fc3d 100644 --- a/circuits/lib/src/configs/committee/small.nr +++ b/circuits/lib/src/configs/committee/small.nr @@ -12,4 +12,4 @@ pub global N_PARTIES: u32 = 5; /// Threshold. pub global T: u32 = 2; /// Number of honest parties. -pub global H: u32 = 5; +pub global H: u32 = 3; diff --git a/circuits/lib/src/configs/insecure/threshold.nr b/circuits/lib/src/configs/insecure/threshold.nr index b83ac49930..cc21b0c727 100644 --- a/circuits/lib/src/configs/insecure/threshold.nr +++ b/circuits/lib/src/configs/insecure/threshold.nr @@ -53,10 +53,8 @@ pk_aggregation (CIRCUIT 5) ------------------------------------- ************************************/ -// pk_aggregation - bit parameters -pub global PK_AGGREGATION_BIT_PK: u32 = 36; +pub global PK_AGGREGATION_BIT_PK: u32 = 35; -// pk_aggregation - configs pub global PK_AGGREGATION_CONFIGS: PkAggregationConfigs = PkAggregationConfigs::new(QIS); /************************************ diff --git a/circuits/lib/src/configs/secure/threshold.nr b/circuits/lib/src/configs/secure/threshold.nr index 12ecede68e..cf42116d5e 100644 --- a/circuits/lib/src/configs/secure/threshold.nr +++ b/circuits/lib/src/configs/secure/threshold.nr @@ -58,10 +58,8 @@ pk_aggregation (CIRCUIT 5) ------------------------------------- ************************************/ -// pk_aggregation - bit parameters -pub global PK_AGGREGATION_BIT_PK: u32 = 53; +pub global PK_AGGREGATION_BIT_PK: u32 = 52; -// pk_aggregation - configs pub global PK_AGGREGATION_CONFIGS: PkAggregationConfigs = PkAggregationConfigs::new(QIS); /************************************ diff --git a/crates/polynomial/src/crt_polynomial.rs b/crates/polynomial/src/crt_polynomial.rs index 2089689f93..2da4f21f5f 100644 --- a/crates/polynomial/src/crt_polynomial.rs +++ b/crates/polynomial/src/crt_polynomial.rs @@ -174,7 +174,7 @@ impl CrtPolynomial { /// * `scalar` - The scalar to multiply each coefficient by. pub fn scalar_mul(&mut self, scalar: &BigInt) { for limb in &mut self.limbs { - limb.scalar_mul(scalar); + *limb = limb.scalar_mul(scalar); } } diff --git a/crates/zk-helpers/src/bin/zk_cli.rs b/crates/zk-helpers/src/bin/zk_cli.rs index 23d48043c0..0b1b6b9d5c 100644 --- a/crates/zk-helpers/src/bin/zk_cli.rs +++ b/crates/zk-helpers/src/bin/zk_cli.rs @@ -24,6 +24,8 @@ use e3_zk_helpers::dkg::share_encryption::{ ShareEncryptionCircuit, ShareEncryptionCircuitInput, ShareEncryptionSample, }; use e3_zk_helpers::registry::{Circuit, CircuitRegistry}; +use e3_zk_helpers::threshold::pk_aggregation::PkAggregationCircuit; +use e3_zk_helpers::threshold::pk_aggregation::PkAggregationCircuitInput; use e3_zk_helpers::threshold::pk_generation::{PkGenerationCircuit, PkGenerationCircuitInput}; use e3_zk_helpers::threshold::user_data_encryption::{ UserDataEncryptionCircuit, UserDataEncryptionCircuitInput, UserDataEncryptionSample, @@ -159,6 +161,7 @@ fn main() -> Result<()> { registry.register(Arc::new(UserDataEncryptionCircuit)); registry.register(Arc::new(ShareEncryptionCircuit)); registry.register(Arc::new(PkGenerationCircuit)); + registry.register(Arc::new(PkAggregationCircuit)); // Handle list circuits flag. if args.list_circuits { @@ -326,6 +329,14 @@ fn main() -> Result<()> { let circuit = PkGenerationCircuit; circuit.codegen(preset, &sample)? } + name if name == ::NAME => { + let sample = PkAggregationCircuitInput::generate_sample( + preset, + CiphernodesCommitteeSize::Small.values(), + )?; + let circuit = PkAggregationCircuit; + circuit.codegen(preset, &sample)? + } name => return Err(anyhow!("circuit {} not yet implemented", name)), }; 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 91c2eb29e7..1cc56023af 100644 --- a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/codegen.rs +++ b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/codegen.rs @@ -51,31 +51,27 @@ pub fn generate_configs(_preset: BfvPreset, configs: &Configs) -> CodegenConfigs format!( r#"use crate::core::threshold::pk_aggregation::Configs as PkAggregationConfigs; -// Global configs for Public Key Aggregation TRBFV circuit +// Global configs pub global N: u32 = {}; pub global L: u32 = {}; pub global QIS: [Field; L] = [{}]; /************************************ ------------------------------------- -pk_agg_trbfv (CIRCUIT 5 - PUBLIC KEY AGGREGATION TRBFV) +pk_aggregation (CIRCUIT 5) ------------------------------------- ************************************/ -// pk_agg_trbfv - bit parameters pub global {}_BIT_PK: u32 = {}; -// pk_agg_trbfv - configs -pub global {}_CONFIGS: PkAggregationConfigs = PkAggregationConfigs::new( - QIS, -); +pub global {}_CONFIGS: PkAggregationConfigs = PkAggregationConfigs::new(QIS,); "#, configs.n, // N configs.l, // L qis_str, // QIS array - prefix, // _BIT_PK - configs.bits.pk_bit, // _BIT_PK - prefix, // _CONFIGS + prefix, // BIT_PK + configs.bits.pk_bit, // BIT_PK + prefix, // CONFIGS ) } @@ -133,12 +129,11 @@ mod tests { .contains(format!("{}_BIT_PK: u32 = {}", prefix, configs.bits.pk_bit).as_str())); assert!(codegen_configs.contains( format!( - "{}_CONFIGS: PkAggregationConfigs = PkAggregationConfigs::new(", + "{}_CONFIGS: PkAggregationConfigs = PkAggregationConfigs::new(QIS,);", prefix ) .as_str() )); - assert!(codegen_configs.contains("QIS,")); assert!(codegen_configs.contains(format!("QIS: [Field; L] = [{}];", qis_str).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 ee8be3c5b2..6b61a553fd 100644 --- a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/computation.rs +++ b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/computation.rs @@ -9,6 +9,7 @@ //! [`Configs`], [`Bounds`], [`Bits`], and [`Witness`] are produced from BFV parameters //! and (for witness) public key shares and aggregated public key. They implement [`Computation`] and are used by codegen. +use crate::bigint_1d_to_json_values; use crate::compute_pk_aggregation_commitment; use crate::compute_pk_bit; use crate::crt_polynomial_to_toml_json; @@ -73,7 +74,7 @@ pub struct Bounds { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Witness { - pub expected_pk_commitments: Vec, + pub expected_threshold_pk_commitments: Vec, pub pk0: Vec, pub pk1: Vec, pub pk0_agg: CrtPolynomial, @@ -171,9 +172,9 @@ impl Computation for Witness { let mut pk0_agg = CrtPolynomial::from_fhe_polynomial(&input.public_key.c.c[0]); let mut pk1_agg = input.a.clone(); - // Compute expected_pk_commitments for each honest party + // 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_pk_commitments = Vec::new(); + let mut expected_threshold_pk_commitments = Vec::new(); pk0_agg.reverse(); pk0_agg.reduce(moduli)?; @@ -196,13 +197,13 @@ impl Computation for Witness { let commitment = compute_pk_aggregation_commitment(&pk0[party_index], &pk1[party_index], bit_pk); - expected_pk_commitments.push(commitment); + expected_threshold_pk_commitments.push(commitment); } Ok(Witness { - expected_pk_commitments, + expected_threshold_pk_commitments, pk0, - pk1: pk1.clone(), + pk1, pk0_agg, pk1_agg, }) @@ -221,9 +222,11 @@ impl Computation for Witness { .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_pk_commitments": self.expected_pk_commitments, + "expected_threshold_pk_commitments": expected_threshold_pk_commitments, "pk0": pk0, "pk1": pk1, "pk0_agg": pk0_agg, 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 72d3c81390..e0ea142094 100644 --- a/crates/zk-helpers/src/circuits/threshold/pk_aggregation/sample.rs +++ b/crates/zk-helpers/src/circuits/threshold/pk_aggregation/sample.rs @@ -74,23 +74,25 @@ impl PkAggregationCircuitInput { mod tests { use crate::{ computation::Computation, - threshold::pk_generation::{PkGenerationCircuitInput, Witness}, + threshold::pk_aggregation::computation::Configs, + threshold::pk_aggregation::{PkAggregationCircuitInput, Witness}, CiphernodesCommitteeSize, }; - use e3_fhe_params::DEFAULT_BFV_PRESET; + use e3_fhe_params::BfvPreset; #[test] fn test_generate_sample() { + let preset = BfvPreset::InsecureThreshold512; let committee = CiphernodesCommitteeSize::Small.values(); - let sample = - PkGenerationCircuitInput::generate_sample(DEFAULT_BFV_PRESET, committee).unwrap(); - let witness = Witness::compute(DEFAULT_BFV_PRESET, &sample).unwrap(); - - assert_eq!(witness.pk0is.limbs.len(), 2); - assert_eq!(witness.a.limbs.len(), 2); - assert_eq!(witness.e_sm.limbs.len(), 2); - assert_eq!(witness.r1is.limbs.len(), 2); - assert_eq!(witness.r2is.limbs.len(), 2); + let configs = Configs::compute(preset, &()).unwrap(); + + let sample = PkAggregationCircuitInput::generate_sample(preset, committee).unwrap(); + let witness = Witness::compute(preset, &sample).unwrap(); + + assert_eq!(witness.pk0.len(), sample.committee.h); + assert_eq!(witness.pk1.len(), sample.committee.h); + assert_eq!(witness.pk0_agg.limbs.len(), configs.l); + assert_eq!(witness.pk1_agg.limbs.len(), configs.l); } } diff --git a/crates/zk-helpers/src/utils.rs b/crates/zk-helpers/src/utils.rs index 3c27b4cf43..59e8510690 100644 --- a/crates/zk-helpers/src/utils.rs +++ b/crates/zk-helpers/src/utils.rs @@ -214,20 +214,36 @@ pub fn crt_polynomial_to_toml_json(crt_polynomial: &CrtPolynomial) -> Vec>]) -> Vec>> { - y.iter() +/// # Arguments +/// * `bigint_1d` - 1D vector of BigInt values +/// +/// # Returns +/// A vector of JSON values +pub fn bigint_1d_to_json_values(bigint_1d: &[BigInt]) -> Vec { + bigint_1d + .iter() + .map(|v| serde_json::Value::String(v.to_string())) + .collect() +} + +/// Convert a 3D vector of BigInt to a vector of vectors of vectors of JSON values. +/// +/// # Arguments +/// * `bigint_3d` - 3D vector of BigInt values +/// +/// # Returns +/// A vector of vectors of vectors of JSON values +pub fn bigint_3d_to_json_values( + bigint_3d: &[Vec>], +) -> Vec>> { + bigint_3d + .iter() .map(|coeff| { coeff .iter() - .map(|modulus| { - modulus - .iter() - .map(|v| serde_json::Value::String(v.to_string())) - .collect() - }) + .map(|modulus| bigint_1d_to_json_values(modulus)) .collect() }) .collect()