Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions circuits/bin/threshold/pk_aggregation/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -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<N>; L]; H],
pk1: [[Polynomial<N>; L]; H],
pk0_agg: [Polynomial<N>; L],
pk1_agg: [Polynomial<N>; L],
) -> pub Field {
let pk_aggregation: PkAggregation<N, H, L, PK_AGGREGATION_BIT_PK> = PkAggregation::new(
PK_AGGREGATION_CONFIGS,
CRP,
expected_threshold_pk_commitments,
pk0,
pk1,
pk0_agg,
pk1_agg,
);

pk_aggregation.execute()
Expand Down
79 changes: 32 additions & 47 deletions circuits/lib/src/core/threshold/pk_aggregation.nr
Original file line number Diff line number Diff line change
Expand Up @@ -22,64 +22,59 @@ impl<let L: u32> Configs<L> {

/// 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<let N: u32, let H: u32, let L: u32, let BIT_PK: u32> {
/// Circuit parameters including CRT moduli
configs: Configs<L>,

/// 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<N>; 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],

/// Individual threshold public key first components from H honest parties.
/// pk0[party_idx][basis_idx] for each party and CRT basis.
/// (committed witnesses)
pk0: [[Polynomial<N>; 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<N>; 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<N>; 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<N>; L],
}

impl<let N: u32, let H: u32, let L: u32, let BIT_PK: u32> PkAggregation<N, H, L, BIT_PK> {
pub fn new(
configs: Configs<L>,
crp: [Polynomial<N>; L],
expected_threshold_pk_commitments: [Field; H],
pk0: [[Polynomial<N>; L]; H],
pk1: [[Polynomial<N>; L]; H],
pk0_agg: [Polynomial<N>; L],
pk1_agg: [Polynomial<N>; L],
) -> Self {
PkAggregation { configs, expected_threshold_pk_commitments, pk0, pk1, pk0_agg, pk1_agg }
PkAggregation { configs, crp, expected_threshold_pk_commitments, pk0, pk0_agg }
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// 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::<N, L, BIT_PK>(self.pk0[i], self.pk1[i])
compute_threshold_pk_commitment::<N, L, BIT_PK>(self.pk0[i])
== self.expected_threshold_pk_commitments[i],
"PK commitment mismatch",
);
Expand All @@ -105,46 +100,36 @@ impl<let N: u32, let H: u32, let L: u32, let BIT_PK: u32> PkAggregation<N, H, L,

let half_qi: Field = (q_l - 1) / 2;
let h_half_qi: Field = H as Field * half_qi;
// Offset keeps (sum_shifted - expected_unred + offset) non-negative in the integer
// sense (not Field-wrapped). sum_shifted in [0, H*q_l), expected_unred in
// [(H-1)*q_l/2, (H+1)*q_l/2) -> 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;
for party_idx in 0..H {
sum_shifted =
sum_shifted + pk[party_idx][basis_idx].coefficients[coeff_idx] + half_qi;
}
let sum_reduced = mod_q_l.reduce_mod(sum_shifted);

let expected =
mod_q_l.reduce_mod(pk_agg[basis_idx].coefficients[coeff_idx] + h_half_qi);
assert(sum_reduced == expected, "pk0 aggregation mismatch");
}
}

/// pk1 is the same (a) for all parties; pk1_agg = a
fn verify_pk1(self, basis_idx: u32) {
for coeff_idx in 0..N {
for party_idx in 0..H {
assert(
self.pk1[party_idx][basis_idx].coefficients[coeff_idx]
== self.pk1_agg[basis_idx].coefficients[coeff_idx],
"pk1 mismatch",
);
}
// Single divisibility check replaces two reduce_mod calls + equality assert:
// saves ~37 gates per position (~48 gates -> ~12 gates for assert_zero_mod).
mod_q_l.assert_zero_mod(
sum_shifted + offset - pk_agg[basis_idx].coefficients[coeff_idx] - h_half_qi,
);
}
}

/// Returns `commit(pk_agg)` for P3 (`user_data_encryption_ct0` / `ct1`).
/// Returns `commit(pk0_agg, crp)` for P3 (`user_data_encryption_ct0` / `ct1`).
pub fn execute(self) -> Field {
// Step 1: Bind each party PK to its C1 commitment.
// Step 1: Bind each party pk0 to its C1 commitment.
self.verify_pk_commitments();

// Step 2: Check `pk0_agg` sums party limbs and `pk1_agg` matches CRS `a` (per CRT basis).
// Step 2: Check `pk0_agg` is the mod-q_l sum of party pk0 limbs (per CRT basis).
for basis_idx in 0..L {
self.verify_pk_for_basis(self.pk0, self.pk0_agg, basis_idx);
self.verify_pk1(basis_idx);
}

// Step 3: Commit to the aggregated threshold public key.
compute_pk_aggregation_commitment::<N, L, BIT_PK>(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::<N, L, BIT_PK>(self.pk0_agg, self.crp)
}
}
2 changes: 1 addition & 1 deletion circuits/lib/src/core/threshold/pk_generation.nr
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ impl<let N: u32, let L: u32, let BIT_EEK: u32, let BIT_SK: u32, let BIT_E_SM: u3
let sk_commitment = compute_share_computation_sk_commitment::<N, BIT_SK>(self.sk);
let e_sm_commitment =
compute_share_computation_e_sm_commitment::<N, L, BIT_E_SM>(self.e_sm);
let pk_commitment = compute_threshold_pk_commitment::<N, L, BIT_PK>(self.pk0, self.a);
let pk_commitment = compute_threshold_pk_commitment::<N, L, BIT_PK>(self.pk0);

// Step 3: Fiat-Shamir challenge and per-modulus evaluation checks.
let gamma = self.generate_challenge(sk_commitment, pk_commitment);
Expand Down
5 changes: 1 addition & 4 deletions circuits/lib/src/math/commitments.nr
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,8 @@ pub fn compute_dkg_pk_commitment<let N: u32, let L: u32, let BIT_PK: u32>(

pub fn compute_threshold_pk_commitment<let N: u32, let L: u32, let BIT_PK: u32>(
pk0: [Polynomial<N>; L],
pk1: [Polynomial<N>; L],
) -> Field {
let mut payload = multiple_polynomial_payload::<N, L, BIT_PK>(Vec::new(), pk0);
payload = multiple_polynomial_payload::<N, L, BIT_PK>(payload, pk1);

let payload = multiple_polynomial_payload::<N, L, BIT_PK>(Vec::new(), pk0);
compute_commitment(payload, DS_PK_GENERATION)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down
27 changes: 7 additions & 20 deletions crates/zk-helpers/src/circuits/commitments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down
12 changes: 0 additions & 12 deletions crates/zk-helpers/src/circuits/threshold/pk_aggregation/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,9 +76,8 @@ pub struct Bounds {
pub struct Inputs {
pub expected_threshold_pk_commitments: Vec<BigInt>,
pub pk0: Vec<CrtPolynomial>,
pub pk1: Vec<CrtPolynomial>,
pub pk0_agg: CrtPolynomial,
pub pk1_agg: CrtPolynomial,
pub crp: CrtPolynomial,
}

impl Computation for Configs {
Expand Down Expand Up @@ -158,40 +158,28 @@ impl Computation for Inputs {
let bit_pk = compute_modulus_bit(&threshold_params);

let mut pk0: Vec<CrtPolynomial> = data.pk0_shares.clone();
// pk1 is the same (common random polynomial a) for all parties
let mut pk1: Vec<CrtPolynomial> = (0..data.committee.h).map(|_| data.a.clone()).collect();
// Extract pk0_agg and pk1_agg from aggregated public key (c[1] = a)
let mut pk0_agg = CrtPolynomial::from_fhe_polynomial(&data.public_key.c[0]);
let mut pk1_agg = CrtPolynomial::from_fhe_polynomial(&data.public_key.c[1]);

// Compute expected_threshold_pk_commitments for each honest party
// Each commitment is computed from pk0[i] and pk1[i] for party i
let mut expected_threshold_pk_commitments = Vec::new();
let crp = deterministic_crp_crt_polynomial(&threshold_params)?;

pk0_agg.reverse();
pk0_agg.center(threshold_params.moduli())?;
pk1_agg.reverse();
pk1_agg.center(threshold_params.moduli())?;

let mut expected_threshold_pk_commitments = Vec::new();

for party_index in 0..data.committee.h {
pk0[party_index].reverse();
pk0[party_index].center(threshold_params.moduli())?;

pk1[party_index].reverse();
pk1[party_index].center(threshold_params.moduli())?;

let commitment =
compute_threshold_pk_commitment(&pk0[party_index], &pk1[party_index], bit_pk);
let commitment = compute_threshold_pk_commitment(&pk0[party_index], bit_pk);

expected_threshold_pk_commitments.push(commitment);
}

Ok(Inputs {
expected_threshold_pk_commitments,
pk0,
pk1,
pk0_agg,
pk1_agg,
crp,
})
}

Expand All @@ -201,22 +189,14 @@ impl Computation for Inputs {
.iter()
.map(|p| crt_polynomial_to_toml_json(p))
.collect();
let pk1: Vec<Vec<serde_json::Value>> = 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
1 change: 1 addition & 0 deletions crates/zk-prover/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading