From 4f59223882039125c50773f29ba3af3b31dda50d Mon Sep 17 00:00:00 2001 From: 0xjei Date: Sat, 6 Jun 2026 12:27:14 +0200 Subject: [PATCH 1/3] fix committee threshold with correct value --- crates/aggregator/src/actors/publickey_aggregator.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/aggregator/src/actors/publickey_aggregator.rs b/crates/aggregator/src/actors/publickey_aggregator.rs index b38331ff0..ab6aa7cd9 100644 --- a/crates/aggregator/src/actors/publickey_aggregator.rs +++ b/crates/aggregator/src/actors/publickey_aggregator.rs @@ -300,12 +300,10 @@ impl PublicKeyAggregator { keyshare_bytes: keyshare_bytes.clone(), aggregated_pk_bytes: pubkey.clone(), params_preset: self.params_preset, - // C5 aggregates the H honest keyshares only; the circuit witness and prover - // path use `committee_h` (see pk_aggregation/computation.rs). N is not needed - // here — set both fields to H so downstream validation stays consistent. - committee_n: committee_h, + // C5 witness uses `committee_h` keyshares; artifact lookup needs canonical (N, H, T). + committee_n: threshold_n, committee_h, - committee_threshold: 0, + committee_threshold: threshold_m, }, public_key: pubkey.clone(), nodes: honest_nodes_set.clone(), From ee054d0e3febad3f559bd24d1ad53d0803a68b42 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Sat, 6 Jun 2026 12:40:11 +0200 Subject: [PATCH 2/3] add regression test --- Cargo.lock | 3 + crates/aggregator/Cargo.toml | 3 + .../src/actors/publickey_aggregator.rs | 139 +++++++++++++++++- 3 files changed, 143 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e01e57dd0..905db1711 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3076,9 +3076,12 @@ dependencies = [ "e3-utils", "e3-zk-helpers", "e3-zk-prover", + "fhe", "fhe-math", + "fhe-traits", "futures", "num-bigint", + "rand 0.9.2", "serde", "tracing", ] diff --git a/crates/aggregator/Cargo.toml b/crates/aggregator/Cargo.toml index 3c4159641..7b14bc287 100644 --- a/crates/aggregator/Cargo.toml +++ b/crates/aggregator/Cargo.toml @@ -36,3 +36,6 @@ tracing = { workspace = true } [dev-dependencies] e3-test-helpers = { workspace = true } +fhe = { workspace = true } +fhe-traits = { workspace = true } +rand = { workspace = true } diff --git a/crates/aggregator/src/actors/publickey_aggregator.rs b/crates/aggregator/src/actors/publickey_aggregator.rs index ab6aa7cd9..f3cbb8b2e 100644 --- a/crates/aggregator/src/actors/publickey_aggregator.rs +++ b/crates/aggregator/src/actors/publickey_aggregator.rs @@ -1593,7 +1593,8 @@ mod tests { use super::*; use e3_data::{AutoPersist, DataStore, InMemStore, Repository}; use e3_events::{ - CircuitName, ComputeRequestErrorKind, HistoryCollector, TakeEvents, Unsequenced, ZkError, + CircuitName, ComputeRequestErrorKind, HistoryCollector, ProofPayload, ProofType, + TakeEvents, Unsequenced, ZkError, }; use e3_test_helpers::get_common_setup; use std::collections::BTreeSet; @@ -1647,6 +1648,18 @@ mod tests { PublicKeyAggregator, Addr>, E3id, + )> { + build_public_key_aggregator_with_committee(initial_state, CiphernodesCommitteeSize::Micro) + .await + } + + async fn build_public_key_aggregator_with_committee( + initial_state: PublicKeyAggregatorState, + committee_size: CiphernodesCommitteeSize, + ) -> Result<( + PublicKeyAggregator, + Addr>, + E3id, )> { let (bus, rng, _seed, params, crp, _errors, history) = get_common_setup(Some(BfvPreset::InsecureThreshold512.into()))?; @@ -1658,7 +1671,7 @@ mod tests { bus, e3_id: e3_id.clone(), params_preset: BfvPreset::InsecureThreshold512, - committee_size: CiphernodesCommitteeSize::Micro, + committee_size, }, test_state(initial_state), ); @@ -1666,6 +1679,80 @@ mod tests { Ok((aggregator, history, e3_id)) } + fn c1_proof_with_pk_commitment(e3_id: &E3id, pk_commitment: [u8; 32]) -> SignedProofPayload { + let mut signals = vec![0u8; 96]; + signals[32..64].copy_from_slice(&pk_commitment); + SignedProofPayload { + payload: ProofPayload { + e3_id: e3_id.clone(), + proof_type: ProofType::C1PkGeneration, + proof: Proof::new( + CircuitName::PkGeneration, + ArcBytes::from_bytes(&[1]), + ArcBytes::from_bytes(&signals), + ), + }, + signature: ArcBytes::from_bytes(&[0u8; 65]), + } + } + + fn verifying_c1_non_square_state( + fhe: &Fhe, + e3_id: &E3id, + ) -> Result<(PublicKeyAggregatorState, usize, usize, usize)> { + use fhe::bfv::SecretKey; + use fhe::mbfv::PublicKeyShare; + use fhe_traits::Serialize; + + let committee = CiphernodesCommitteeSize::Medium.values(); + let threshold_n = committee.n; + let threshold_m = committee.threshold; + let circuit_h = committee.h; + assert_ne!( + threshold_n, circuit_h, + "test requires a non-square committee (N != H)" + ); + + let mut submission_order = Vec::with_capacity(threshold_n); + let mut c1_proofs = Vec::with_capacity(threshold_n); + let mut rng = rand::rng(); + + for party_id in 0..threshold_n as u64 { + let node = format!("0x{:040x}", party_id + 1); + if party_id < circuit_h as u64 { + let sk = SecretKey::random(&fhe.params, &mut rng); + let pk_share = PublicKeyShare::new(&sk, fhe.crp.clone(), &mut rng)?; + let ks_bytes = ArcBytes::from_bytes(&pk_share.to_bytes()); + let commitment = e3_zk_helpers::compute_pk_commitment_from_keyshare_bytes( + &ks_bytes, + &fhe.params, + &fhe.crp, + )?; + submission_order.push((party_id, node, ks_bytes)); + c1_proofs.push(Some(c1_proof_with_pk_commitment( + e3_id, + commitment, + ))); + } else { + submission_order.push((party_id, node, ArcBytes::from_bytes(&[party_id as u8]))); + c1_proofs.push(None); + } + } + + Ok(( + PublicKeyAggregatorState::VerifyingC1 { + submission_order, + threshold_m, + threshold_n, + c1_proofs, + no_proof_parties: vec![], + }, + threshold_n, + threshold_m, + circuit_h, + )) + } + async fn next_event(history: &Addr>) -> Result { let mut result = history.send(TakeEvents::::new(1)).await?; assert!(!result.timed_out, "timed out waiting for an event"); @@ -1822,4 +1909,52 @@ mod tests { Ok(()) } + + #[actix::test] + async fn pk_aggregation_proof_pending_carries_canonical_committee_dims() -> Result<()> { + let (bus, rng, _seed, params, crp, _errors, history) = + get_common_setup(Some(BfvPreset::InsecureThreshold512.into()))?; + let e3_id = E3id::new("42", 1); + let fhe = Arc::new(Fhe::new(params, crp, rng)); + let (initial_state, threshold_n, threshold_m, circuit_h) = + verifying_c1_non_square_state(&fhe, &e3_id)?; + + let mut aggregator = PublicKeyAggregator::new( + PublicKeyAggregatorParams { + fhe, + bus, + e3_id: e3_id.clone(), + params_preset: BfvPreset::InsecureThreshold512, + committee_size: CiphernodesCommitteeSize::Medium, + }, + test_state(initial_state), + ); + + let dishonest: BTreeSet = (circuit_h as u64..threshold_n as u64).collect(); + aggregator.handle_c1_verification_complete(TypedEvent::new( + ShareVerificationComplete { + e3_id: e3_id.clone(), + kind: VerificationKind::PkGenerationProofs, + dishonest_parties: dishonest, + }, + test_ctx(ShareVerificationComplete { + e3_id: e3_id.clone(), + kind: VerificationKind::PkGenerationProofs, + dishonest_parties: BTreeSet::new(), + }), + ))?; + + let event = next_event(&history).await?; + assert!(matches!( + event.into_data(), + EnclaveEventData::PkAggregationProofPending(data) + if data.e3_id == e3_id + && data.proof_request.committee_n == threshold_n + && data.proof_request.committee_h == circuit_h + && data.proof_request.committee_threshold == threshold_m + && data.proof_request.keyshare_bytes.len() == circuit_h + )); + + Ok(()) + } } From 5e658a618ac4a79ed1d40fe8875e574152672e02 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Sat, 6 Jun 2026 12:40:23 +0200 Subject: [PATCH 3/3] fmt --- crates/aggregator/src/actors/publickey_aggregator.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/aggregator/src/actors/publickey_aggregator.rs b/crates/aggregator/src/actors/publickey_aggregator.rs index f3cbb8b2e..d59d7683d 100644 --- a/crates/aggregator/src/actors/publickey_aggregator.rs +++ b/crates/aggregator/src/actors/publickey_aggregator.rs @@ -1729,10 +1729,7 @@ mod tests { &fhe.crp, )?; submission_order.push((party_id, node, ks_bytes)); - c1_proofs.push(Some(c1_proof_with_pk_commitment( - e3_id, - commitment, - ))); + c1_proofs.push(Some(c1_proof_with_pk_commitment(e3_id, commitment))); } else { submission_order.push((party_id, node, ArcBytes::from_bytes(&[party_id as u8]))); c1_proofs.push(None);