diff --git a/crates/events/src/enclave_event/compute_request/mod.rs b/crates/events/src/enclave_event/compute_request/mod.rs index 85fa352f7c..fdb8254d44 100644 --- a/crates/events/src/enclave_event/compute_request/mod.rs +++ b/crates/events/src/enclave_event/compute_request/mod.rs @@ -85,6 +85,8 @@ impl ToString for ComputeRequest { ZkRequest::PkGeneration(_) => "ZkPkGeneration", ZkRequest::ShareComputation(_) => "ZkShareComputation", ZkRequest::ShareEncryption(_) => "ZkShareEncryption", + ZkRequest::DkgShareDecryption(_) => "ZkDkgShareDecryption", + ZkRequest::VerifyShareProofs(_) => "ZkVerifyShareProofs", }, } .to_string() diff --git a/crates/events/src/enclave_event/compute_request/zk.rs b/crates/events/src/enclave_event/compute_request/zk.rs index a3ea9a2483..73105e70e4 100644 --- a/crates/events/src/enclave_event/compute_request/zk.rs +++ b/crates/events/src/enclave_event/compute_request/zk.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::Proof; +use crate::{Proof, ProofType, SignedProofPayload}; use derivative::Derivative; use e3_crypto::SensitiveBytes; use e3_fhe_params::BfvPreset; @@ -23,6 +23,10 @@ pub enum ZkRequest { ShareComputation(ShareComputationProofRequest), /// Generate proof for share encryption (C3a/C3b). ShareEncryption(ShareEncryptionProofRequest), + /// Generate proof for DKG share decryption (C4a/C4b). + DkgShareDecryption(DkgShareDecryptionProofRequest), + /// Batch-verify C2/C3 proofs from other parties. + VerifyShareProofs(VerifyShareProofsRequest), } /// Request to generate a proof for share computation (C2a or C2b). @@ -73,6 +77,28 @@ pub struct ShareEncryptionProofRequest { pub esi_index: usize, } +/// Request to generate a proof for DKG share decryption (C4a or C4b). +/// +/// Proves that a node correctly decrypted H honest parties' BFV-encrypted +/// Shamir shares using its own BFV secret key. +#[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derivative(Debug)] +pub struct DkgShareDecryptionProofRequest { + /// BFV secret key used for decryption (witness — encrypted at rest). + pub sk_bfv: SensitiveBytes, + /// Serialized BFV Ciphertext bytes from H honest parties, flattened as [H * L]. + /// Layout: party 0 mod 0, party 0 mod 1, ..., party 1 mod 0, ... + pub honest_ciphertexts_raw: Vec, + /// Number of honest parties (H). + pub num_honest_parties: usize, + /// Number of CRT moduli (L). + pub num_moduli: usize, + /// SecretKey or SmudgingNoise. + pub dkg_input_type: DkgInputType, + /// BFV preset for parameter resolution. + pub params_preset: BfvPreset, +} + /// Request to generate a proof for BFV public key generation (C0). #[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derivative(Debug)] @@ -142,6 +168,10 @@ pub enum ZkResponse { ShareComputation(ShareComputationProofResponse), /// Proof for share encryption (C3a/C3b). ShareEncryption(ShareEncryptionProofResponse), + /// Proof for DKG share decryption (C4a/C4b). + DkgShareDecryption(DkgShareDecryptionProofResponse), + /// Batch verification results for C2/C3 proofs. + VerifyShareProofs(VerifyShareProofsResponse), } /// Response containing a generated share computation proof. @@ -174,6 +204,22 @@ pub struct PkGenerationProofResponse { pub proof: Proof, } +/// Response containing a generated DKG share decryption proof. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DkgShareDecryptionProofResponse { + pub proof: Proof, + pub dkg_input_type: DkgInputType, +} + +impl DkgShareDecryptionProofResponse { + pub fn new(proof: Proof, dkg_input_type: DkgInputType) -> Self { + Self { + proof, + dkg_input_type, + } + } +} + impl ShareComputationProofResponse { pub fn new(proof: Proof, dkg_input_type: DkgInputType) -> Self { Self { @@ -195,6 +241,44 @@ impl PkGenerationProofResponse { } } +/// Request to batch-verify C2/C3 proofs received from other parties. +/// +/// Grouped by sender so the verifier can report honest/dishonest per party. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VerifyShareProofsRequest { + /// Proofs grouped by sender party_id. + pub party_proofs: Vec, +} + +/// All signed proofs from a single sender to verify. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PartyProofsToVerify { + /// The party that generated these proofs. + pub sender_party_id: u64, + /// Signed proofs to verify (C2a, C2b, C3a×L, C3b×L). + pub signed_proofs: Vec, +} + +/// Batch verification results for C2/C3 proofs. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VerifyShareProofsResponse { + /// Per-party verification results. + pub party_results: Vec, +} + +/// Verification result for all proofs from a single sender. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PartyVerificationResult { + /// The party whose proofs were verified. + pub sender_party_id: u64, + /// Whether ALL proofs from this party verified successfully. + pub all_verified: bool, + /// If any proof failed: the proof type that failed. + pub failed_proof_type: Option, + /// If any proof failed: the signed payload for fault attribution. + pub failed_signed_payload: Option, +} + /// ZK-specific error variants. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ZkError { diff --git a/crates/events/src/enclave_event/decryption_key_shared.rs b/crates/events/src/enclave_event/decryption_key_shared.rs new file mode 100644 index 0000000000..61c308fa47 --- /dev/null +++ b/crates/events/src/enclave_event/decryption_key_shared.rs @@ -0,0 +1,46 @@ +// 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::{E3id, Proof}; +use actix::Message; +use derivative::Derivative; +use e3_utils::utility_types::ArcBytes; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +/// Exchange #3: Each honest node shares its aggregated trBFV partial key shares +/// with all other honest nodes, together with C4 proofs of correct BFV decryption. +#[derive(Message, Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +#[derivative(Debug)] +pub struct DecryptionKeyShared { + pub e3_id: E3id, + /// The sender's party_id. + pub party_id: u64, + /// The sender's node address. + pub node: String, + /// Lagrange-interpolated aggregated SK polynomial (serialized). + #[derivative(Debug(format_with = "e3_utils::formatters::hexf"))] + pub sk_poly_sum: ArcBytes, + /// Lagrange-interpolated aggregated E_SM polynomials (serialized), one per smudging noise. + pub es_poly_sum: Vec, + /// C4a proof (SecretKey decryption). + pub c4a_proof: Proof, + /// C4b proofs (SmudgingNoise decryption), one per smudging noise index. + pub c4b_proofs: Vec, + /// Whether this was received from the network. + pub external: bool, +} + +impl Display for DecryptionKeyShared { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "DecryptionKeyShared {{ e3_id: {}, party_id: {} }}", + self.e3_id, self.party_id + ) + } +} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 30ecdde43f..5276e37409 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -14,6 +14,7 @@ mod committee_published; mod committee_requested; mod compute_request; mod configuration_updated; +mod decryption_key_shared; mod decryptionshare_created; mod die; mod e3_failed; @@ -61,6 +62,7 @@ pub use committee_published::*; pub use committee_requested::*; pub use compute_request::*; pub use configuration_updated::*; +pub use decryption_key_shared::*; pub use decryptionshare_created::*; pub use die::*; pub use e3_failed::*; @@ -200,6 +202,7 @@ pub enum EnclaveEventData { E3Requested(E3Requested), PublicKeyAggregated(PublicKeyAggregated), CiphertextOutputPublished(CiphertextOutputPublished), + DecryptionKeyShared(DecryptionKeyShared), DecryptionshareCreated(DecryptionshareCreated), PlaintextAggregated(PlaintextAggregated), PublishDocumentRequested(PublishDocumentRequested), @@ -468,6 +471,7 @@ impl EnclaveEventData { EnclaveEventData::E3Requested(ref data) => Some(data.e3_id.clone()), EnclaveEventData::PublicKeyAggregated(ref data) => Some(data.e3_id.clone()), EnclaveEventData::CiphertextOutputPublished(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::DecryptionKeyShared(ref data) => Some(data.e3_id.clone()), EnclaveEventData::DecryptionshareCreated(ref data) => Some(data.e3_id.clone()), EnclaveEventData::PlaintextAggregated(ref data) => Some(data.e3_id.clone()), EnclaveEventData::PkGenerationProofSigned(ref data) => Some(data.e3_id.clone()), @@ -519,6 +523,7 @@ impl_event_types!( E3Requested, PublicKeyAggregated, CiphertextOutputPublished, + DecryptionKeyShared, DecryptionshareCreated, PlaintextAggregated, PublishDocumentRequested, diff --git a/crates/events/src/enclave_event/threshold_share_created.rs b/crates/events/src/enclave_event/threshold_share_created.rs index e9794a0397..4de064bf26 100644 --- a/crates/events/src/enclave_event/threshold_share_created.rs +++ b/crates/events/src/enclave_event/threshold_share_created.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::E3id; +use crate::{E3id, SignedProofPayload}; use actix::Message; use derivative::Derivative; use e3_trbfv::shares::BfvEncryptedShares; @@ -64,6 +64,18 @@ pub struct ThresholdShareCreated { pub share: Arc, pub target_party_id: u64, pub external: bool, + /// Signed C2a proof (sk share computation) from the sender. + #[serde(default)] + pub signed_c2a_proof: Option, + /// Signed C2b proof (e_sm share computation) from the sender. + #[serde(default)] + pub signed_c2b_proof: Option, + /// Signed C3a proofs (sk share encryption per modulus row) for this recipient. + #[serde(default)] + pub signed_c3a_proofs: Vec, + /// Signed C3b proofs (e_sm share encryption per modulus row) for this recipient. + #[serde(default)] + pub signed_c3b_proofs: Vec, } impl Display for ThresholdShareCreated { diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 426be036d7..4a47abf2ea 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -10,13 +10,16 @@ use e3_crypto::{Cipher, SensitiveBytes}; use e3_data::Persistable; use e3_events::{ prelude::*, trap, BusHandle, CiphernodeSelected, CiphertextOutputPublished, ComputeRequest, - ComputeResponse, ComputeResponseKind, CorrelationId, DecryptionshareCreated, Die, - DkgProofSigned, E3RequestComplete, E3id, EType, EnclaveEvent, EnclaveEventData, EncryptionKey, - EncryptionKeyCollectionFailed, EncryptionKeyCreated, EncryptionKeyPending, EventContext, - KeyshareCreated, PartyId, PkGenerationProofRequest, PkGenerationProofSigned, ProofType, - Sequenced, ShareComputationProofRequest, ShareEncryptionProofRequest, SignedProofPayload, - ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated, ThresholdSharePending, - TypedEvent, + ComputeResponse, ComputeResponseKind, CorrelationId, DecryptionKeyShared, + DecryptionshareCreated, Die, DkgProofSigned, DkgShareDecryptionProofRequest, + DkgShareDecryptionProofResponse, E3Failed, E3RequestComplete, E3Stage, E3id, EType, + EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCollectionFailed, + EncryptionKeyCreated, EncryptionKeyPending, EventContext, FailureReason, KeyshareCreated, + PartyId, PartyProofsToVerify, PkGenerationProofRequest, PkGenerationProofSigned, Proof, + ProofType, Sequenced, ShareComputationProofRequest, ShareEncryptionProofRequest, + SignedProofPayload, ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated, + ThresholdSharePending, TypedEvent, VerifyShareProofsRequest, VerifyShareProofsResponse, + ZkRequest, ZkResponse, }; use e3_fhe_params::create_deterministic_crp_from_default_seed; use e3_fhe_params::{build_pair_for_preset, BfvParamSet, BfvPreset}; @@ -31,7 +34,7 @@ use e3_trbfv::{ shares::{BfvEncryptedShares, EncryptableVec, Encrypted, ShamirShare, SharedSecret}, TrBFVConfig, TrBFVRequest, TrBFVResponse, }; -use e3_utils::{to_ordered_vec, utility_types::ArcBytes}; +use e3_utils::utility_types::ArcBytes; use e3_utils::{NotifySync, MAILBOX_LIMIT}; use e3_zk_helpers::computation::DkgInputType; use e3_zk_helpers::CiphernodesCommitteeSize; @@ -40,14 +43,14 @@ use fhe_traits::{DeserializeParametrized, Serialize}; use rand::{rngs::OsRng, SeedableRng}; use rand_chacha::ChaCha20Rng; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, mem, sync::{Arc, Mutex}, }; -use tracing::{info, trace, warn}; +use tracing::{error, info, trace, warn}; use crate::encryption_key_collector::{AllEncryptionKeysCollected, EncryptionKeyCollector}; -use crate::threshold_share_collector::ThresholdShareCollector; +use crate::threshold_share_collector::{ReceivedShareProofs, ThresholdShareCollector}; #[derive(Message, Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[rtype(result = "()")] @@ -64,12 +67,32 @@ pub struct GenEsiSss { #[rtype(result = "()")] pub struct AllThresholdSharesCollected { shares: Vec>, + /// Proofs from each sender, ordered by party_id (parallel to shares). + share_proofs: Vec, } -impl From>> for AllThresholdSharesCollected { - fn from(value: HashMap>) -> Self { - AllThresholdSharesCollected { - shares: to_ordered_vec(value), +impl AllThresholdSharesCollected { + pub fn new( + shares: HashMap>, + proofs: HashMap, + ) -> Self { + let mut entries: Vec<_> = shares.into_iter().collect(); + entries.sort_by_key(|(k, _)| *k); + let (party_ids, shares): (Vec<_>, Vec<_>) = entries.into_iter().unzip(); + let share_proofs = party_ids + .iter() + .map(|pid| { + proofs.get(pid).cloned().unwrap_or(ReceivedShareProofs { + signed_c2a_proof: None, + signed_c2b_proof: None, + signed_c3a_proofs: Vec::new(), + signed_c3b_proofs: Vec::new(), + }) + }) + .collect(); + Self { + shares, + share_proofs, } } } @@ -335,6 +358,20 @@ pub struct ThresholdKeyshare { encryption_key_collector: Option>, state: Persistable, share_enc_preset: BfvPreset, + /// Temporarily holds shares + proofs while C2/C3 proof verification is in flight. + pending_verification_shares: Option>>, + /// C4a proof (SecretKey decryption) — stored after generation, used in Exchange #3. + c4a_proof: Option, + /// C4b proofs (SmudgingNoise decryption) — keyed by esi_idx for deterministic ordering. + c4b_proofs: HashMap, + /// Expected number of C4b proofs (one per smudging noise index). + expected_c4b_count: usize, + /// Maps correlation IDs to esi_idx for C4b proof ordering. + c4b_correlation_map: HashMap, + /// Parties that provided no C2/C3 proofs (treated as dishonest when others did provide proofs). + no_proof_dishonest_parties: Option>, + /// Party IDs sent for C2/C3 verification — used to detect missing results in the response. + expected_verification_parties: Option>, } impl ThresholdKeyshare { @@ -346,6 +383,13 @@ impl ThresholdKeyshare { encryption_key_collector: None, state: params.state, share_enc_preset: params.share_enc_preset, + pending_verification_shares: None, + c4a_proof: None, + c4b_proofs: HashMap::new(), + expected_c4b_count: 0, + c4b_correlation_map: HashMap::new(), + no_proof_dishonest_parties: None, + expected_verification_parties: None, } } } @@ -534,6 +578,8 @@ impl ThresholdKeyshare { _ => Ok(()), }, ComputeResponseKind::Zk(zk) => match zk { + ZkResponse::VerifyShareProofs(_) => self.handle_verify_share_proofs_response(msg), + ZkResponse::DkgShareDecryption(_) => self.handle_c4_proof_response(msg), _ => Ok(()), }, } @@ -1001,13 +1047,264 @@ impl ThresholdKeyshare { Ok(()) } - /// 5. AllThresholdSharesCollected - Decrypt received shares using BFV and aggregate + /// 5. AllThresholdSharesCollected - Verify C2/C3 proofs, then decrypt and aggregate pub fn handle_all_threshold_shares_collected( - &self, + &mut self, msg: TypedEvent, ) -> Result<()> { let (msg, ec) = msg.into_components(); info!("AllThresholdSharesCollected"); + let state = self.state.try_get()?; + let e3_id = state.get_e3_id(); + let own_party_id = state.party_id; + + // Derive expected proof counts from our own share (trusted source). + // All parties use the same BFV params, so moduli counts are identical. + // Using the sender's share would let a malicious party manipulate expected counts. + let own_share = msg + .shares + .iter() + .find(|s| s.party_id == own_party_id) + .ok_or_else(|| anyhow!("Own share not found in AllThresholdSharesCollected"))?; + let expected_c3a = own_share + .sk_sss + .get_share(0) + .map(|s| s.num_moduli()) + .unwrap_or(0); + let expected_c3b: usize = own_share + .esi_sss + .iter() + .map(|esi| esi.get_share(0).map(|s| s.num_moduli()).unwrap_or(0)) + .sum(); + let expected_num_esi = own_share.esi_sss.len(); + + // Build verification requests for other parties' proofs + let mut party_proofs_to_verify: Vec = Vec::new(); + let mut no_proof_parties: HashSet = HashSet::new(); + let mut incomplete_proof_parties: HashSet = HashSet::new(); + for (share, proofs) in msg.shares.iter().zip(msg.share_proofs.iter()) { + if share.party_id == own_party_id { + continue; + } + + let has_any_proof = proofs.signed_c2a_proof.is_some() + || proofs.signed_c2b_proof.is_some() + || !proofs.signed_c3a_proofs.is_empty() + || !proofs.signed_c3b_proofs.is_empty(); + + if !has_any_proof { + no_proof_parties.insert(share.party_id); + continue; + } + + // Validate proof set completeness against trusted expected counts. + // A malicious sender could omit proofs that would fail verification, + // so we must check that all expected proofs are present. + let is_complete = proofs.signed_c2a_proof.is_some() + && proofs.signed_c2b_proof.is_some() + && proofs.signed_c3a_proofs.len() == expected_c3a + && proofs.signed_c3b_proofs.len() == expected_c3b + && share.esi_sss.len() == expected_num_esi; + + if !is_complete { + warn!( + "Party {} has incomplete proof set (c2a={}, c2b={}, c3a={}/{}, c3b={}/{}, esi={}/{}), treating as dishonest", + share.party_id, + proofs.signed_c2a_proof.is_some(), + proofs.signed_c2b_proof.is_some(), + proofs.signed_c3a_proofs.len(), expected_c3a, + proofs.signed_c3b_proofs.len(), expected_c3b, + share.esi_sss.len(), expected_num_esi, + ); + incomplete_proof_parties.insert(share.party_id); + continue; + } + + // Complete proof set — collect for verification + let mut signed_proofs = Vec::new(); + // SAFETY: is_complete guarantees c2a and c2b are Some + signed_proofs.push(proofs.signed_c2a_proof.clone().unwrap()); + signed_proofs.push(proofs.signed_c2b_proof.clone().unwrap()); + signed_proofs.extend(proofs.signed_c3a_proofs.iter().cloned()); + signed_proofs.extend(proofs.signed_c3b_proofs.iter().cloned()); + + party_proofs_to_verify.push(PartyProofsToVerify { + sender_party_id: share.party_id, + signed_proofs, + }); + } + + // Store shares for use after verification completes + self.pending_verification_shares = Some(msg.shares); + + // Backward compat: only when ALL non-self parties have zero proofs + // AND none have incomplete proofs (incomplete proofs are always dishonest) + if party_proofs_to_verify.is_empty() && incomplete_proof_parties.is_empty() { + if no_proof_parties.is_empty() { + info!( + "No C2/C3 proofs to verify for E3 {} — proceeding with all parties", + e3_id + ); + return self.proceed_with_decryption_key_calculation(None, ec); + } + info!( + "No C2/C3 proofs from any party for E3 {} — proceeding with all parties (backward compat)", + e3_id + ); + return self.proceed_with_decryption_key_calculation(None, ec); + } + + // Merge no-proof and incomplete-proof parties — both are dishonest + let mut unverified_dishonest: HashSet = incomplete_proof_parties; + unverified_dishonest.extend(no_proof_parties); + if !unverified_dishonest.is_empty() { + warn!( + "{} parties have missing/incomplete C2/C3 proofs for E3 {} — marking as dishonest: {:?}", + unverified_dishonest.len(), + e3_id, + unverified_dishonest + ); + } + + if party_proofs_to_verify.is_empty() { + // All non-self parties are dishonest (missing or incomplete proofs), none to verify + return self.proceed_with_decryption_key_calculation(Some(unverified_dishonest), ec); + } + + // Store dishonest parties so we can merge them with verification failures later + self.no_proof_dishonest_parties = Some(unverified_dishonest); + + // Track which party IDs we're sending for verification so we can detect missing results + self.expected_verification_parties = Some( + party_proofs_to_verify + .iter() + .map(|p| p.sender_party_id) + .collect(), + ); + + info!( + "Dispatching C2/C3 proof verification for E3 {} ({} parties)", + e3_id, + party_proofs_to_verify.len() + ); + + let event = ComputeRequest::zk( + ZkRequest::VerifyShareProofs(VerifyShareProofsRequest { + party_proofs: party_proofs_to_verify, + }), + CorrelationId::new(), + e3_id.clone(), + ); + self.bus.publish(event, ec)?; + Ok(()) + } + + /// Handle C2/C3 proof verification results — define honest set H and proceed. + pub fn handle_verify_share_proofs_response( + &mut self, + msg: TypedEvent, + ) -> Result<()> { + let (msg, ec) = msg.into_components(); + let resp: VerifyShareProofsResponse = match msg.response { + ComputeResponseKind::Zk(ZkResponse::VerifyShareProofs(r)) => r, + _ => bail!("Expected VerifyShareProofs response"), + }; + + let state = self.state.try_get()?; + let e3_id = state.get_e3_id(); + + // Partition into honest and dishonest based on proof verification results + let mut dishonest_parties: HashSet = HashSet::new(); + + // Merge in parties that provided no proofs (already identified as dishonest) + if let Some(no_proof) = self.no_proof_dishonest_parties.take() { + dishonest_parties.extend(no_proof); + } + + let expected_parties = self.expected_verification_parties.take(); + let mut seen_parties: HashSet = HashSet::new(); + + for result in &resp.party_results { + seen_parties.insert(result.sender_party_id); + if result.all_verified { + info!( + "Party {} passed C2/C3 verification for E3 {}", + result.sender_party_id, e3_id + ); + } else { + warn!( + "Party {} FAILED C2/C3 verification for E3 {} (proof type: {:?})", + result.sender_party_id, e3_id, result.failed_proof_type + ); + dishonest_parties.insert(result.sender_party_id); + } + } + + // Any party we sent for verification but got no result back is treated as dishonest + if let Some(expected) = expected_parties { + for party_id in &expected { + if !seen_parties.contains(party_id) { + warn!( + "Party {} missing from C2/C3 verification results for E3 {} — treating as dishonest", + party_id, e3_id + ); + dishonest_parties.insert(*party_id); + } + } + } + + if dishonest_parties.is_empty() { + info!( + "All parties passed C2/C3 verification for E3 {} — proceeding", + e3_id + ); + self.proceed_with_decryption_key_calculation(None, ec) + } else { + let threshold = state.threshold_m; + let total = state.threshold_n; + let honest_count = total - dishonest_parties.len() as u64; + + if honest_count < threshold { + warn!( + "Too few honest parties for E3 {} ({} honest < {} threshold) — cannot proceed", + e3_id, honest_count, threshold + ); + if let Err(err) = self.bus.publish( + E3Failed { + e3_id: msg.e3_id, + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }, + ec, + ) { + error!("Failed to publish E3Failed: {err}"); + } + self.pending_verification_shares = None; + return Ok(()); + } + + info!( + "Proceeding with {} honest parties for E3 {} ({} dishonest excluded)", + honest_count, + e3_id, + dishonest_parties.len() + ); + self.proceed_with_decryption_key_calculation(Some(dishonest_parties), ec) + } + } + + /// After verification, decrypt shares from honest parties, compute decryption key, + /// and dispatch C4 proof generation. + fn proceed_with_decryption_key_calculation( + &mut self, + dishonest_parties: Option>, + ec: EventContext, + ) -> Result<()> { + let shares = self + .pending_verification_shares + .take() + .ok_or_else(|| anyhow!("No pending verification shares"))?; + let cipher = self.cipher.clone(); let state = self.state.try_get()?; let e3_id = state.get_e3_id(); @@ -1021,10 +1318,61 @@ impl ThresholdKeyshare { let sk_bfv = deserialize_secret_key(&sk_bytes, ¶ms)?; let degree = params.degree(); - // Decrypt our share from each sender using BFV - // Local share (from self) has all parties' shares, network shares are pre-extracted - let sk_sss_collected: Vec = msg - .shares + // Filter to honest parties only + let honest_shares: Vec<_> = shares + .iter() + .filter(|ts| { + dishonest_parties + .as_ref() + .map_or(true, |dp| !dp.contains(&ts.party_id)) + }) + .collect(); + + let num_honest = honest_shares.len(); + info!( + "Decrypting shares from {} honest parties for E3 {}", + num_honest, e3_id + ); + + // Collect ciphertext bytes for C4 proof generation BEFORE decrypting + // C4a: sk_sss ciphertexts from honest parties [H * L] + let mut sk_ciphertexts_raw = Vec::new(); + let mut num_moduli_sk = 0; + for ts in &honest_shares { + let idx = if ts.sk_sss.len() == 1 { 0 } else { party_id }; + let share = ts + .sk_sss + .clone_share(idx) + .ok_or(anyhow!("No sk_sss share at index {}", idx))?; + num_moduli_sk = share.num_moduli(); + for ct_bytes in share.ciphertext_bytes() { + sk_ciphertexts_raw.push(ct_bytes.clone()); + } + } + + // C4b: esi_sss ciphertexts from honest parties — one set per smudging noise + // Layout per esi index: [H * L] ciphertexts + let num_esi = honest_shares + .first() + .map(|ts| ts.esi_sss.len()) + .unwrap_or(0); + let mut esi_ciphertexts_raw: Vec> = vec![Vec::new(); num_esi]; + let mut num_moduli_esi = 0; + for ts in &honest_shares { + for (esi_idx, esi_shares) in ts.esi_sss.iter().enumerate() { + let idx = if esi_shares.len() == 1 { 0 } else { party_id }; + let share = esi_shares + .clone_share(idx) + .ok_or(anyhow!("No esi_sss share at index {}", idx))?; + num_moduli_esi = share.num_moduli(); + for ct_bytes in share.ciphertext_bytes() { + esi_ciphertexts_raw[esi_idx].push(ct_bytes.clone()); + } + } + } + + // Decrypt our share from each honest sender using BFV + let sk_sss_collected: Vec = honest_shares .iter() .map(|ts| { let idx = if ts.sk_sss.len() == 1 { 0 } else { party_id }; @@ -1037,8 +1385,7 @@ impl ThresholdKeyshare { .collect::>()?; // Similarly decrypt esi_sss for each ciphertext - let esi_sss_collected: Vec> = msg - .shares + let esi_sss_collected: Vec> = honest_shares .iter() .map(|ts| { ts.esi_sss @@ -1054,6 +1401,7 @@ impl ThresholdKeyshare { }) .collect::>()?; + // Publish CalculateDecryptionKey request let request = CalculateDecryptionKeyRequest { trbfv_config, esi_sss_collected: esi_sss_collected @@ -1068,8 +1416,181 @@ impl ThresholdKeyshare { CorrelationId::new(), e3_id.clone(), ); + self.bus.publish(event, ec.clone())?; + + // Reset C4 proof storage and set expected count + self.c4a_proof = None; + self.c4b_proofs.clear(); + self.c4b_correlation_map.clear(); + self.expected_c4b_count = num_esi; + + // Dispatch C4a proof generation (SecretKey decryption) + info!( + "Dispatching C4a DkgShareDecryption proof (SecretKey) for E3 {} ({} honest, {} moduli)", + e3_id, num_honest, num_moduli_sk + ); + let c4a_request = ComputeRequest::zk( + ZkRequest::DkgShareDecryption(DkgShareDecryptionProofRequest { + sk_bfv: current.sk_bfv.clone(), + honest_ciphertexts_raw: sk_ciphertexts_raw, + num_honest_parties: num_honest, + num_moduli: num_moduli_sk, + dkg_input_type: DkgInputType::SecretKey, + params_preset: self.share_enc_preset.clone(), + }), + CorrelationId::new(), + e3_id.clone(), + ); + self.bus.publish(c4a_request, ec.clone())?; + + // Dispatch C4b proof generation for each smudging noise index + for (esi_idx, esi_cts) in esi_ciphertexts_raw.into_iter().enumerate() { + info!( + "Dispatching C4b DkgShareDecryption proof (SmudgingNoise[{}]) for E3 {} ({} honest, {} moduli)", + esi_idx, e3_id, num_honest, num_moduli_esi + ); + let correlation_id = CorrelationId::new(); + self.c4b_correlation_map.insert(correlation_id, esi_idx); + let c4b_request = ComputeRequest::zk( + ZkRequest::DkgShareDecryption(DkgShareDecryptionProofRequest { + sk_bfv: current.sk_bfv.clone(), + honest_ciphertexts_raw: esi_cts, + num_honest_parties: num_honest, + num_moduli: num_moduli_esi, + dkg_input_type: DkgInputType::SmudgingNoise, + params_preset: self.share_enc_preset.clone(), + }), + correlation_id, + e3_id.clone(), + ); + self.bus.publish(c4b_request, ec.clone())?; + } + + Ok(()) + } + + /// Handle C4 (DkgShareDecryption) proof responses — store for Exchange #3. + fn handle_c4_proof_response(&mut self, msg: TypedEvent) -> Result<()> { + let (msg, _ec) = msg.into_components(); + let correlation_id = msg.correlation_id; + let resp: DkgShareDecryptionProofResponse = match msg.response { + ComputeResponseKind::Zk(ZkResponse::DkgShareDecryption(r)) => r, + _ => bail!("Expected DkgShareDecryption response"), + }; + + let state = self.state.try_get()?; + let e3_id = state.get_e3_id(); + + match resp.dkg_input_type { + DkgInputType::SecretKey => { + info!("Received C4a proof (SecretKey decryption) for E3 {}", e3_id); + self.c4a_proof = Some(resp.proof); + } + DkgInputType::SmudgingNoise => { + let esi_idx = self + .c4b_correlation_map + .remove(&correlation_id) + .ok_or_else(|| { + anyhow!( + "Unknown correlation ID {} for C4b proof in E3 {}", + correlation_id, + e3_id + ) + })?; + info!( + "Received C4b proof (SmudgingNoise[{}] decryption) for E3 {} ({}/{})", + esi_idx, + e3_id, + self.c4b_proofs.len() + 1, + self.expected_c4b_count + ); + self.c4b_proofs.insert(esi_idx, resp.proof); + } + } + + if self.c4a_proof.is_some() && self.c4b_proofs.len() == self.expected_c4b_count { + info!( + "All C4 proofs received for E3 {} (1 C4a + {} C4b)", + e3_id, self.expected_c4b_count + ); + self.try_publish_decryption_key_shared(_ec)?; + } + + Ok(()) + } + + /// Publish Exchange #3 (DecryptionKeyShared) when both the decryption key + /// and all C4 proofs are ready. + fn try_publish_decryption_key_shared(&mut self, ec: EventContext) -> Result<()> { + let state = self.state.try_get()?; + let e3_id = state.get_e3_id(); + + // Need to be in ReadyForDecryption state (decryption key computed) + let ready: ReadyForDecryption = match state.clone().try_into() { + Ok(r) => r, + Err(_) => { + trace!("Not yet in ReadyForDecryption state — deferring Exchange #3"); + return Ok(()); + } + }; + + // Need all C4 proofs + let c4a = match &self.c4a_proof { + Some(p) => p.clone(), + None => { + trace!("C4a proof not yet received — deferring Exchange #3"); + return Ok(()); + } + }; + if self.c4b_proofs.len() != self.expected_c4b_count { + trace!("Not all C4b proofs received — deferring Exchange #3"); + return Ok(()); + } + + let party_id = state.party_id; + let node = state.address.clone(); + + // Decrypt sk_poly_sum and es_poly_sum from SensitiveBytes → ArcBytes for network transmission + let sk_poly_sum_bytes = ready.sk_poly_sum.access(&self.cipher)?; + let es_poly_sum_bytes: Vec = ready + .es_poly_sum + .iter() + .map(|s| { + let bytes = s.access(&self.cipher)?; + Ok(ArcBytes::from_bytes(&bytes)) + }) + .collect::>()?; + + info!( + "Publishing Exchange #3 (DecryptionKeyShared) for E3 {} party {}", + e3_id, party_id + ); + + // Assemble C4b proofs in esi_idx order to align with es_poly_sum + let mut c4b_ordered: Vec = Vec::with_capacity(self.expected_c4b_count); + for idx in 0..self.expected_c4b_count { + let proof = self + .c4b_proofs + .get(&idx) + .ok_or_else(|| anyhow!("Missing C4b proof for esi_idx {}", idx))? + .clone(); + c4b_ordered.push(proof); + } + + self.bus.publish( + DecryptionKeyShared { + e3_id: e3_id.clone(), + party_id, + node, + sk_poly_sum: ArcBytes::from_bytes(&sk_poly_sum_bytes), + es_poly_sum: es_poly_sum_bytes, + c4a_proof: c4a, + c4b_proofs: c4b_ordered, + external: false, + }, + ec, + )?; - self.bus.publish(event, ec)?; Ok(()) } @@ -1122,6 +1643,9 @@ impl ThresholdKeyshare { ec.clone(), )?; + // Check if C4 proofs are already ready — if so, publish Exchange #3 now + self.try_publish_decryption_key_shared(ec)?; + Ok(()) } @@ -1259,6 +1783,15 @@ impl Handler for ThresholdKeyshare { } } } + EnclaveEventData::DecryptionKeyShared(data) => { + if data.external { + info!( + "Received DecryptionKeyShared from party {} for E3 {}", + data.party_id, data.e3_id + ); + // TODO: Verify C4 proofs and store for threshold decryption + } + } EnclaveEventData::ComputeResponse(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } diff --git a/crates/keyshare/src/threshold_share_collector.rs b/crates/keyshare/src/threshold_share_collector.rs index f83240bba1..31682fc21e 100644 --- a/crates/keyshare/src/threshold_share_collector.rs +++ b/crates/keyshare/src/threshold_share_collector.rs @@ -12,7 +12,8 @@ use std::{ use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Message, SpawnHandle}; use e3_events::{ - E3id, ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated, TypedEvent, + E3id, SignedProofPayload, ThresholdShare, ThresholdShareCollectionFailed, + ThresholdShareCreated, TypedEvent, }; use e3_trbfv::PartyId; use e3_utils::MAILBOX_LIMIT; @@ -20,6 +21,19 @@ use tracing::{info, warn}; use crate::{AllThresholdSharesCollected, ThresholdKeyshare}; +/// Proofs received alongside a threshold share from a sender. +#[derive(Clone, Debug)] +pub struct ReceivedShareProofs { + /// Signed C2a proof (sk share computation) from the sender. + pub signed_c2a_proof: Option, + /// Signed C2b proof (e_sm share computation) from the sender. + pub signed_c2b_proof: Option, + /// Signed C3a proofs (sk share encryption per modulus row). + pub signed_c3a_proofs: Vec, + /// Signed C3b proofs (e_sm share encryption per modulus row). + pub signed_c3b_proofs: Vec, +} + const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(600); pub(crate) enum CollectorState { @@ -44,6 +58,8 @@ pub struct ThresholdShareCollector { state: CollectorState, /// The shares collected shares: HashMap>, + /// Proofs received alongside each party's shares + share_proofs: HashMap, /// A timeout handle for when this collector will report failure timeout_handle: Option, } @@ -56,6 +72,7 @@ impl ThresholdShareCollector { parent, state: CollectorState::Collecting, shares: HashMap::new(), + share_proofs: HashMap::new(), timeout_handle: None, }; collector.start() @@ -112,6 +129,15 @@ impl Handler> for ThresholdShareCollector { return; }; info!("Inserting... waiting on: {}", self.todo.len()); + self.share_proofs.insert( + pid, + ReceivedShareProofs { + signed_c2a_proof: msg.signed_c2a_proof, + signed_c2b_proof: msg.signed_c2b_proof, + signed_c3a_proofs: msg.signed_c3a_proofs, + signed_c3b_proofs: msg.signed_c3b_proofs, + }, + ); self.shares.insert(pid, msg.share); if self.todo.is_empty() { @@ -123,8 +149,11 @@ impl Handler> for ThresholdShareCollector { ctx.cancel_future(handle); } - let event: TypedEvent = - TypedEvent::new(self.shares.clone().into(), ec); + let proofs = std::mem::take(&mut self.share_proofs); + let event: TypedEvent = TypedEvent::new( + AllThresholdSharesCollected::new(self.shares.clone(), proofs), + ec, + ); self.parent.do_send(event); } info!( diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index bd3b822c7c..9078ef7d4f 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -23,10 +23,12 @@ use e3_events::EType; use e3_events::EffectsEnabled; use e3_events::{ BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeRequestKind, - ComputeResponse, EnclaveEvent, EnclaveEventData, EventPublisher, EventSubscriber, EventType, + ComputeResponse, DkgShareDecryptionProofRequest, DkgShareDecryptionProofResponse, EnclaveEvent, + EnclaveEventData, EventPublisher, EventSubscriber, EventType, PartyVerificationResult, PkBfvProofRequest, PkBfvProofResponse, PkGenerationProofRequest, PkGenerationProofResponse, ShareComputationProofRequest, ShareComputationProofResponse, ShareEncryptionProofRequest, - ShareEncryptionProofResponse, TypedEvent, ZkError as ZkEventError, ZkRequest, ZkResponse, + ShareEncryptionProofResponse, TypedEvent, VerifyShareProofsRequest, VerifyShareProofsResponse, + ZkError as ZkEventError, ZkRequest, ZkResponse, }; use e3_fhe_params::build_pair_for_preset; use e3_fhe_params::{BfvParamSet, BfvPreset}; @@ -36,6 +38,7 @@ use e3_trbfv::calculate_decryption_share::calculate_decryption_share; use e3_trbfv::calculate_threshold_decryption::calculate_threshold_decryption; use e3_trbfv::gen_esi_sss::gen_esi_sss; use e3_trbfv::gen_pk_share_and_sk_sss::gen_pk_share_and_sk_sss; +use e3_trbfv::helpers::deserialize_secret_key; use e3_trbfv::helpers::try_poly_from_bytes; use e3_trbfv::shares::SharedSecret; use e3_trbfv::{TrBFVError, TrBFVRequest, TrBFVResponse}; @@ -48,6 +51,7 @@ use e3_zk_helpers::circuits::threshold::pk_generation::circuit::{ }; use e3_zk_helpers::computation::DkgInputType; use e3_zk_helpers::dkg::share_computation::{ShareComputationCircuit, ShareComputationCircuitData}; +use e3_zk_helpers::dkg::share_decryption::{ShareDecryptionCircuit, ShareDecryptionCircuitData}; use e3_zk_helpers::dkg::share_encryption::{ShareEncryptionCircuit, ShareEncryptionCircuitData}; use e3_zk_prover::{Provable, ZkBackend, ZkProver}; use fhe::bfv::{Ciphertext, Encoding, Plaintext, PublicKey, SecretKey}; @@ -414,6 +418,12 @@ fn handle_zk_request( ZkRequest::ShareEncryption(req) => timefunc("zk_share_encryption", id, || { handle_share_encryption_proof(&prover, &cipher, req, request.clone()) }), + ZkRequest::DkgShareDecryption(req) => timefunc("zk_dkg_share_decryption", id, || { + handle_dkg_share_decryption_proof(&prover, &cipher, req, request.clone()) + }), + ZkRequest::VerifyShareProofs(req) => timefunc("zk_verify_share_proofs", id, || { + handle_verify_share_proofs(&prover, req, request.clone()) + }), } } @@ -707,3 +717,145 @@ fn handle_share_encryption_proof( request.e3_id, )) } + +fn handle_dkg_share_decryption_proof( + prover: &ZkProver, + cipher: &Cipher, + req: DkgShareDecryptionProofRequest, + request: ComputeRequest, +) -> Result { + // 1. Build DKG params from preset + let (_threshold_params, dkg_params) = build_pair_for_preset(req.params_preset) + .map_err(|e| make_zk_error(&request, format!("build_pair_for_preset: {}", e)))?; + + // 2. Decrypt BFV secret key from SensitiveBytes + let sk_bytes = req + .sk_bfv + .access_raw(cipher) + .map_err(|e| make_zk_error(&request, format!("sk_bfv decrypt: {}", e)))?; + let secret_key = deserialize_secret_key(&sk_bytes, &dkg_params) + .map_err(|e| make_zk_error(&request, format!("sk_bfv deserialize: {}", e)))?; + + // 3. Deserialize ciphertexts from raw bytes [H * L] → Vec> [H][L] + let h = req.num_honest_parties; + let l = req.num_moduli; + if req.honest_ciphertexts_raw.len() != h * l { + return Err(make_zk_error( + &request, + format!( + "Expected {} ciphertexts (H={} * L={}), got {}", + h * l, + h, + l, + req.honest_ciphertexts_raw.len() + ), + )); + } + + let mut honest_ciphertexts: Vec> = Vec::with_capacity(h); + for party_idx in 0..h { + let mut party_cts = Vec::with_capacity(l); + for mod_idx in 0..l { + let raw = &req.honest_ciphertexts_raw[party_idx * l + mod_idx]; + let ct = Ciphertext::from_bytes(raw, &dkg_params).map_err(|e| { + make_zk_error( + &request, + format!( + "ciphertext[{}][{}] deserialize: {:?}", + party_idx, mod_idx, e + ), + ) + })?; + party_cts.push(ct); + } + honest_ciphertexts.push(party_cts); + } + + // 4. Build circuit data + let circuit_data = ShareDecryptionCircuitData { + secret_key, + honest_ciphertexts, + dkg_input_type: req.dkg_input_type, + }; + + // 5. Generate proof + let circuit = ShareDecryptionCircuit; + let e3_id_str = request.e3_id.to_string(); + let proof = circuit + .prove(prover, &req.params_preset, &circuit_data, &e3_id_str) + .map_err(|e| { + ComputeRequestError::new( + ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(e.to_string())), + request.clone(), + ) + })?; + + // 6. Return response + Ok(ComputeResponse::zk( + ZkResponse::DkgShareDecryption(DkgShareDecryptionProofResponse { + proof, + dkg_input_type: req.dkg_input_type, + }), + request.correlation_id, + request.e3_id, + )) +} + +fn handle_verify_share_proofs( + prover: &ZkProver, + req: VerifyShareProofsRequest, + request: ComputeRequest, +) -> Result { + let e3_id_str = request.e3_id.to_string(); + + let party_results: Vec = req + .party_proofs + .into_iter() + .map(|party| { + let sender = party.sender_party_id; + for signed_proof in &party.signed_proofs { + let proof = &signed_proof.payload.proof; + let result = prover.verify(proof, &e3_id_str, sender); + match result { + Ok(true) => continue, + Ok(false) => { + info!( + "Proof verification failed for party {} ({:?})", + sender, signed_proof.payload.proof_type + ); + return PartyVerificationResult { + sender_party_id: sender, + all_verified: false, + failed_proof_type: Some(signed_proof.payload.proof_type), + failed_signed_payload: Some(signed_proof.clone()), + }; + } + Err(e) => { + info!( + "Proof verification error for party {} ({:?}): {}", + sender, signed_proof.payload.proof_type, e + ); + return PartyVerificationResult { + sender_party_id: sender, + all_verified: false, + failed_proof_type: Some(signed_proof.payload.proof_type), + failed_signed_payload: Some(signed_proof.clone()), + }; + } + } + } + PartyVerificationResult { + sender_party_id: sender, + all_verified: true, + failed_proof_type: None, + failed_signed_payload: None, + } + }) + .collect(); + + Ok(ComputeResponse::zk( + ZkResponse::VerifyShareProofs(VerifyShareProofsResponse { party_results }), + request.correlation_id, + request.e3_id, + )) +} diff --git a/crates/net/src/document_publisher.rs b/crates/net/src/document_publisher.rs index 63d98a6dc2..b2ce0d5451 100644 --- a/crates/net/src/document_publisher.rs +++ b/crates/net/src/document_publisher.rs @@ -15,10 +15,11 @@ use anyhow::Context; use anyhow::Result; use chrono::{DateTime, Utc}; use e3_events::{ - prelude::*, trap, trap_fut, BusHandle, CiphernodeSelected, CorrelationId, DocumentKind, - DocumentMeta, DocumentReceived, E3RequestComplete, E3id, EType, EnclaveEvent, EnclaveEventData, - EncryptionKeyCreated, EncryptionKeyReceived, Event, EventContext, EventSource, EventType, - Filter, PartyId, PublishDocumentRequested, Sequenced, ThresholdShareCreated, TypedEvent, + prelude::*, trap, trap_fut, BusHandle, CiphernodeSelected, CorrelationId, DecryptionKeyShared, + DocumentKind, DocumentMeta, DocumentReceived, E3RequestComplete, E3id, EType, EnclaveEvent, + EnclaveEventData, EncryptionKeyCreated, EncryptionKeyReceived, Event, EventContext, + EventSource, EventType, Filter, PartyId, PublishDocumentRequested, Sequenced, + ThresholdShareCreated, TypedEvent, }; use e3_utils::ArcBytes; use e3_utils::NotifySync; @@ -85,6 +86,7 @@ impl DocumentPublisher { EnclaveEventData::PublishDocumentRequested(_) => true, EnclaveEventData::ThresholdShareCreated(_) => true, EnclaveEventData::EncryptionKeyCreated(_) => true, + EnclaveEventData::DecryptionKeyShared(_) => true, _ => false, } } @@ -430,6 +432,7 @@ pub struct EventConverter { enum ReceivableDocument { ThresholdShareCreated(ThresholdShareCreated), EncryptionKeyCreated(EncryptionKeyCreated), + DecryptionKeyShared(DecryptionKeyShared), } impl ReceivableDocument { @@ -451,6 +454,7 @@ impl EventConverter { let addr = Self::new(bus).start(); bus.subscribe(EventType::ThresholdShareCreated, addr.clone().into()); bus.subscribe(EventType::EncryptionKeyCreated, addr.clone().into()); + bus.subscribe(EventType::DecryptionKeyShared, addr.clone().into()); bus.subscribe(EventType::DocumentReceived, addr.clone().into()); addr } @@ -517,6 +521,20 @@ impl EventConverter { Ok(()) } + fn handle_decryption_key_shared(&self, msg: TypedEvent) -> Result<()> { + let (msg, ctx) = msg.into_components(); + if msg.external { + return Ok(()); + } + + let meta = DocumentMeta::new(msg.e3_id.clone(), DocumentKind::TrBFV, vec![], None); + let receivable = ReceivableDocument::DecryptionKeyShared(msg); + let value = ArcBytes::from_bytes(&receivable.to_bytes()?); + self.bus + .publish(PublishDocumentRequested::new(meta, value), ctx)?; + Ok(()) + } + /// Convert received document to internal events. /// Note: Filtering already happened in DocumentPublisher before DHT fetch. fn handle_document_received(&self, msg: TypedEvent) -> Result<()> { @@ -535,6 +553,10 @@ impl EventConverter { e3_id: evt.e3_id, share: evt.share, target_party_id: evt.target_party_id, + signed_c2a_proof: evt.signed_c2a_proof, + signed_c2b_proof: evt.signed_c2b_proof, + signed_c3a_proofs: evt.signed_c3a_proofs, + signed_c3b_proofs: evt.signed_c3b_proofs, }, ctx.clone(), )?; @@ -552,6 +574,16 @@ impl EventConverter { ctx, )?; } + ReceivableDocument::DecryptionKeyShared(evt) => { + debug!("Received DecryptionKeyShared from party {}", evt.party_id); + self.bus.publish( + DecryptionKeyShared { + external: true, + ..evt + }, + ctx, + )?; + } } Ok(()) } @@ -572,6 +604,9 @@ impl Handler for EventConverter { EnclaveEventData::EncryptionKeyCreated(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::DecryptionKeyShared(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } EnclaveEventData::DocumentReceived(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } @@ -610,6 +645,21 @@ impl Handler> for EventConverter { } } +impl Handler> for EventConverter { + type Result = (); + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + trap( + EType::DocumentPublishing, + &self.bus.with_ec(msg.get_ctx()), + || self.handle_decryption_key_shared(msg), + ) + } +} + impl Handler> for EventConverter { type Result = (); fn handle( diff --git a/crates/trbfv/src/shares/bfv_encrypted.rs b/crates/trbfv/src/shares/bfv_encrypted.rs index 932bf16a59..5c34cfc93a 100644 --- a/crates/trbfv/src/shares/bfv_encrypted.rs +++ b/crates/trbfv/src/shares/bfv_encrypted.rs @@ -148,6 +148,16 @@ impl BfvEncryptedShare { /// /// # Returns /// The decrypted Shamir share + /// Get the raw ciphertext bytes (one per CRT modulus). + pub fn ciphertext_bytes(&self) -> &[ArcBytes] { + &self.ciphertexts + } + + /// Number of CRT moduli (ciphertexts per share). + pub fn num_moduli(&self) -> usize { + self.ciphertexts.len() + } + pub fn decrypt( self, sk: &SecretKey, diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index ab4642a4ff..6475c1fbea 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use actix::{Actor, Addr, Context, Handler}; @@ -346,6 +346,9 @@ impl ProofRequestActor { ComputeResponseKind::Zk(ZkResponse::ShareEncryption(resp)) => { self.handle_threshold_proof_response(&msg.correlation_id, resp.proof.clone()); } + ComputeResponseKind::Zk(ZkResponse::DkgShareDecryption(resp)) => { + self.handle_threshold_proof_response(&msg.correlation_id, resp.proof.clone()); + } _ => {} } } @@ -408,6 +411,7 @@ impl ProofRequestActor { let party_id = pending.full_share.party_id; let ec = &pending.ec; + // Sign C1 (PkGeneration) let Some(signed_pk_gen) = self.sign_proof( e3_id, ProofType::C1PkGeneration, @@ -417,7 +421,8 @@ impl ProofRequestActor { return; }; - let Some(signed_sk_share) = self.sign_proof( + // Sign C2a (SkShareComputation) + let Some(signed_c2a) = self.sign_proof( e3_id, ProofType::C2aSkShareComputation, pending.sk_share_computation_proof.expect("checked"), @@ -426,7 +431,8 @@ impl ProofRequestActor { return; }; - let Some(signed_e_sm_share) = self.sign_proof( + // Sign C2b (ESmShareComputation) + let Some(signed_c2b) = self.sign_proof( e3_id, ProofType::C2bESmShareComputation, pending.e_sm_share_computation_proof.expect("checked"), @@ -435,6 +441,38 @@ impl ProofRequestActor { return; }; + // Sign C3a proofs (SkShareEncryption) — keyed by (recipient, row) + let mut signed_c3a_map: BTreeMap> = BTreeMap::new(); + for ((_recipient, _row), proof) in &pending.sk_share_encryption_proofs { + if let Some(signed) = + self.sign_proof(e3_id, ProofType::C3aSkShareEncryption, proof.clone()) + { + signed_c3a_map.entry(*_recipient).or_default().push(signed); + } else { + error!( + "Failed to sign C3a proof for recipient {} — shares will not be published", + _recipient + ); + return; + } + } + + // Sign C3b proofs (ESmShareEncryption) — keyed by (esi_index, recipient, row) + let mut signed_c3b_map: BTreeMap> = BTreeMap::new(); + for ((_esi, _recipient, _row), proof) in &pending.e_sm_share_encryption_proofs { + if let Some(signed) = + self.sign_proof(e3_id, ProofType::C3bESmShareEncryption, proof.clone()) + { + signed_c3b_map.entry(*_recipient).or_default().push(signed); + } else { + error!( + "Failed to sign C3b proof for recipient {} — shares will not be published", + _recipient + ); + return; + } + } + info!( "All proofs signed for E3 {} party {} (signer: {})", e3_id, @@ -442,9 +480,7 @@ impl ProofRequestActor { self.signer.address() ); - let share = &pending.full_share; - let num_parties = share.num_parties(); - + // Publish local proof events for the node's own state tracking if let Err(err) = self.bus.publish( PkGenerationProofSigned { e3_id: e3_id.clone(), @@ -460,7 +496,7 @@ impl ProofRequestActor { DkgProofSigned { e3_id: e3_id.clone(), party_id, - signed_proof: signed_sk_share, + signed_proof: signed_c2a.clone(), }, ec.clone(), ) { @@ -471,53 +507,49 @@ impl ProofRequestActor { DkgProofSigned { e3_id: e3_id.clone(), party_id, - signed_proof: signed_e_sm_share, + signed_proof: signed_c2b.clone(), }, ec.clone(), ) { error!("Failed to publish ESmDkgProofSigned: {err}"); } - // Sign and publish C3a proofs (SkShareEncryption) - for ((_recipient, _row), proof) in &pending.sk_share_encryption_proofs { - let Some(signed) = - self.sign_proof(e3_id, ProofType::C3aSkShareEncryption, proof.clone()) - else { - error!("Failed to sign C3a proof — shares will not be published"); - return; - }; - if let Err(err) = self.bus.publish( - DkgProofSigned { - e3_id: e3_id.clone(), - party_id, - signed_proof: signed, - }, - ec.clone(), - ) { - error!("Failed to publish SkShareEncryptionProofSigned: {err}"); + // Publish C3a signed proofs (reuse already-signed proofs from signed_c3a_map) + for signed_proofs in signed_c3a_map.values() { + for signed in signed_proofs { + if let Err(err) = self.bus.publish( + DkgProofSigned { + e3_id: e3_id.clone(), + party_id, + signed_proof: signed.clone(), + }, + ec.clone(), + ) { + error!("Failed to publish SkShareEncryptionProofSigned: {err}"); + } } } - // Sign and publish C3b proofs (ESmShareEncryption) - for ((_esi, _recipient, _row), proof) in &pending.e_sm_share_encryption_proofs { - let Some(signed) = - self.sign_proof(e3_id, ProofType::C3bESmShareEncryption, proof.clone()) - else { - error!("Failed to sign C3b proof — shares will not be published"); - return; - }; - if let Err(err) = self.bus.publish( - DkgProofSigned { - e3_id: e3_id.clone(), - party_id, - signed_proof: signed, - }, - ec.clone(), - ) { - error!("Failed to publish ESmShareEncryptionProofSigned: {err}"); + // Publish C3b signed proofs (reuse already-signed proofs from signed_c3b_map) + for signed_proofs in signed_c3b_map.values() { + for signed in signed_proofs { + if let Err(err) = self.bus.publish( + DkgProofSigned { + e3_id: e3_id.clone(), + party_id, + signed_proof: signed.clone(), + }, + ec.clone(), + ) { + error!("Failed to publish ESmShareEncryptionProofSigned: {err}"); + } } } + // Publish ThresholdShareCreated with proofs attached for each recipient + let share = &pending.full_share; + let num_parties = share.num_parties(); + info!( "Publishing ThresholdShareCreated for E3 {} to {} parties", e3_id, num_parties @@ -525,12 +557,25 @@ impl ProofRequestActor { for recipient_party_id in 0..num_parties { if let Some(party_share) = share.extract_for_party(recipient_party_id) { + let c3a_proofs = signed_c3a_map + .get(&recipient_party_id) + .cloned() + .unwrap_or_default(); + let c3b_proofs = signed_c3b_map + .get(&recipient_party_id) + .cloned() + .unwrap_or_default(); + if let Err(err) = self.bus.publish( ThresholdShareCreated { e3_id: e3_id.clone(), share: Arc::new(party_share), target_party_id: recipient_party_id as u64, external: false, + signed_c2a_proof: Some(signed_c2a.clone()), + signed_c2b_proof: Some(signed_c2b.clone()), + signed_c3a_proofs: c3a_proofs, + signed_c3b_proofs: c3b_proofs, }, ec.clone(), ) { diff --git a/crates/zk-prover/tests/onchain_verification_tests.rs b/crates/zk-prover/tests/onchain_verification_tests.rs index a50225a34c..14b933c3cf 100644 --- a/crates/zk-prover/tests/onchain_verification_tests.rs +++ b/crates/zk-prover/tests/onchain_verification_tests.rs @@ -1,212 +1,212 @@ -// 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. - -//! On-chain ZK proof verification tests. -//! Requires: `bb`, `anvil`, and compiled contract artifacts -//! (`npx hardhat compile` in packages/enclave-contracts). - -mod common; - -use alloy::{ - network::TransactionBuilder, - primitives::{Bytes, FixedBytes}, - providers::{Provider, ProviderBuilder}, - rpc::types::TransactionRequest, - sol, -}; -use common::{find_anvil, find_bb, setup_compiled_circuit, setup_test_prover}; -use e3_fhe_params::BfvPreset; -use e3_zk_helpers::circuits::dkg::pk::circuit::{PkCircuit, PkCircuitData}; -use e3_zk_prover::{Provable, ZkProver}; -use std::path::PathBuf; - -sol! { - #[sol(rpc)] - contract DkgPkVerifier { - function verify(bytes calldata proof, bytes32[] calldata publicInputs) external view returns (bool verified); - } -} - -/// Linker placeholder that gets replaced with the deployed ZKTranscriptLib address. -const ZK_TRANSCRIPT_LIB_PLACEHOLDER: &str = "__$3f925933ac313a1c84f3f4c25b9ea43c90$__"; - -fn artifacts_dir() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol") -} - -fn read_artifact_bytecode_hex(artifact_name: &str) -> Option { - let path = artifacts_dir().join(artifact_name); - let json_str = std::fs::read_to_string(&path).ok()?; - let json: serde_json::Value = serde_json::from_str(&json_str).ok()?; - json["bytecode"].as_str().map(|s| s.to_string()) -} - -fn decode_bytecode(hex_str: &str) -> Vec { - let clean = hex_str.strip_prefix("0x").unwrap_or(hex_str); - hex::decode(clean).expect("failed to decode bytecode hex") -} - -fn link_transcript_lib(bytecode_hex: &str, lib_address: &alloy::primitives::Address) -> Vec { - let addr_hex = hex::encode(lib_address.as_slice()); - let linked = bytecode_hex.replace(ZK_TRANSCRIPT_LIB_PLACEHOLDER, &addr_hex); - decode_bytecode(&linked) -} - -#[tokio::test] -async fn test_pk_bfv_onchain_verification() { - // Generate ZK proof - - let bb = match find_bb().await { - Some(bb) => bb, - None => { - println!("skipping: bb not found"); - return; - } - }; - - let preset = BfvPreset::InsecureThreshold512; - let (backend, _temp) = setup_test_prover(&bb).await; - setup_compiled_circuit(&backend, "dkg", "pk").await; - - let sample = match PkCircuitData::generate_sample(preset) { - Ok(s) => s, - Err(e) => { - println!("skipping: failed to generate sample: {e}"); - return; - } - }; - - let prover = ZkProver::new(&backend); - let e3_id = "0"; - - let proof = PkCircuit - .prove(&prover, &preset, &sample, e3_id) - .expect("proof generation should succeed"); - - assert!(!proof.data.is_empty(), "proof data should not be empty"); - assert!( - !proof.public_signals.is_empty(), - "public signals should not be empty" - ); - - let local_ok = PkCircuit.verify(&prover, &proof, e3_id, 1); - assert!( - local_ok.as_ref().is_ok_and(|&v| v), - "local proof verification failed: {local_ok:?}" - ); - - println!( - "proof: {} bytes, public_inputs: {} bytes", - proof.data.len(), - proof.public_signals.len() - ); - - // Deploy verifier contract to Anvil - - let lib_bytecode_hex = match read_artifact_bytecode_hex("ZKTranscriptLib.json") { - Some(h) => h, - None => { - println!( - "skipping: ZKTranscriptLib artifact not found \ - (run `npx hardhat compile` in packages/enclave-contracts)" - ); - return; - } - }; - let verifier_bytecode_hex = match read_artifact_bytecode_hex("DkgPkVerifier.json") { - Some(h) => h, - None => { - println!( - "skipping: DkgPkVerifier artifact not found \ - (run `npx hardhat compile` in packages/enclave-contracts)" - ); - return; - } - }; - - if !find_anvil().await { - println!("skipping: anvil not found on PATH"); - return; - } - - let provider = ProviderBuilder::new().connect_anvil_with_wallet(); - - let lib_bytecode = decode_bytecode(&lib_bytecode_hex); - let lib_deploy_tx = TransactionRequest::default().with_deploy_code(Bytes::from(lib_bytecode)); - let lib_receipt = provider - .send_transaction(lib_deploy_tx) - .await - .expect("failed to send ZKTranscriptLib deploy tx") - .get_receipt() - .await - .expect("failed to get ZKTranscriptLib deploy receipt"); - let lib_address = lib_receipt - .contract_address - .expect("ZKTranscriptLib deploy receipt missing contract address"); - println!("ZKTranscriptLib deployed at: {lib_address}"); - - let linked_bytecode = link_transcript_lib(&verifier_bytecode_hex, &lib_address); - let verifier_deploy_tx = - TransactionRequest::default().with_deploy_code(Bytes::from(linked_bytecode)); - let verifier_receipt = provider - .send_transaction(verifier_deploy_tx) - .await - .expect("failed to send DkgPkVerifier deploy tx") - .get_receipt() - .await - .expect("failed to get DkgPkVerifier deploy receipt"); - let verifier_address = verifier_receipt - .contract_address - .expect("DkgPkVerifier deploy receipt missing contract address"); - println!("DkgPkVerifier deployed at: {verifier_address}"); - - let verifier = DkgPkVerifier::new(verifier_address, &provider); - - // Verify proof on-chain - - let proof_bytes = Bytes::copy_from_slice(&proof.data); - - // pk_bfv has 17 public inputs, 16 are pairing points baked into the proof, - // so only 1 (the pk commitment) gets passed as publicInputs to the contract. - let public_inputs: Vec> = proof - .public_signals - .chunks(32) - .map(|chunk| { - let mut buf = [0u8; 32]; - buf[..chunk.len()].copy_from_slice(chunk); - FixedBytes::from(buf) - }) - .collect(); - - assert_eq!( - public_inputs.len(), - 1, - "pk_bfv circuit should produce exactly 1 public input (commitment), got {}", - public_inputs.len() - ); - - println!( - "calling on-chain verify with {} proof bytes, {} public input(s)", - proof_bytes.len(), - public_inputs.len() - ); - - let verified = verifier - .verify(proof_bytes, public_inputs) - .call() - .await - .expect("on-chain verification call reverted — the proof should be valid"); - - assert!( - verified, - "on-chain ZK proof verification should return true" - ); - - println!("on-chain verification passed"); - - prover.cleanup(e3_id).unwrap(); -} +// 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. + +//! On-chain ZK proof verification tests. +//! Requires: `bb`, `anvil`, and compiled contract artifacts +//! (`npx hardhat compile` in packages/enclave-contracts). + +mod common; + +use alloy::{ + network::TransactionBuilder, + primitives::{Bytes, FixedBytes}, + providers::{Provider, ProviderBuilder}, + rpc::types::TransactionRequest, + sol, +}; +use common::{find_anvil, find_bb, setup_compiled_circuit, setup_test_prover}; +use e3_fhe_params::BfvPreset; +use e3_zk_helpers::circuits::dkg::pk::circuit::{PkCircuit, PkCircuitData}; +use e3_zk_prover::{Provable, ZkProver}; +use std::path::PathBuf; + +sol! { + #[sol(rpc)] + contract DkgPkVerifier { + function verify(bytes calldata proof, bytes32[] calldata publicInputs) external view returns (bool verified); + } +} + +/// Linker placeholder that gets replaced with the deployed ZKTranscriptLib address. +const ZK_TRANSCRIPT_LIB_PLACEHOLDER: &str = "__$3f925933ac313a1c84f3f4c25b9ea43c90$__"; + +fn artifacts_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol") +} + +fn read_artifact_bytecode_hex(artifact_name: &str) -> Option { + let path = artifacts_dir().join(artifact_name); + let json_str = std::fs::read_to_string(&path).ok()?; + let json: serde_json::Value = serde_json::from_str(&json_str).ok()?; + json["bytecode"].as_str().map(|s| s.to_string()) +} + +fn decode_bytecode(hex_str: &str) -> Vec { + let clean = hex_str.strip_prefix("0x").unwrap_or(hex_str); + hex::decode(clean).expect("failed to decode bytecode hex") +} + +fn link_transcript_lib(bytecode_hex: &str, lib_address: &alloy::primitives::Address) -> Vec { + let addr_hex = hex::encode(lib_address.as_slice()); + let linked = bytecode_hex.replace(ZK_TRANSCRIPT_LIB_PLACEHOLDER, &addr_hex); + decode_bytecode(&linked) +} + +#[tokio::test] +async fn test_pk_bfv_onchain_verification() { + // Generate ZK proof + + let bb = match find_bb().await { + Some(bb) => bb, + None => { + println!("skipping: bb not found"); + return; + } + }; + + let preset = BfvPreset::InsecureThreshold512; + let (backend, _temp) = setup_test_prover(&bb).await; + setup_compiled_circuit(&backend, "dkg", "pk").await; + + let sample = match PkCircuitData::generate_sample(preset) { + Ok(s) => s, + Err(e) => { + println!("skipping: failed to generate sample: {e}"); + return; + } + }; + + let prover = ZkProver::new(&backend); + let e3_id = "0"; + + let proof = PkCircuit + .prove(&prover, &preset, &sample, e3_id) + .expect("proof generation should succeed"); + + assert!(!proof.data.is_empty(), "proof data should not be empty"); + assert!( + !proof.public_signals.is_empty(), + "public signals should not be empty" + ); + + let local_ok = PkCircuit.verify(&prover, &proof, e3_id, 1); + assert!( + local_ok.as_ref().is_ok_and(|&v| v), + "local proof verification failed: {local_ok:?}" + ); + + println!( + "proof: {} bytes, public_inputs: {} bytes", + proof.data.len(), + proof.public_signals.len() + ); + + // Deploy verifier contract to Anvil + + let lib_bytecode_hex = match read_artifact_bytecode_hex("ZKTranscriptLib.json") { + Some(h) => h, + None => { + println!( + "skipping: ZKTranscriptLib artifact not found \ + (run `npx hardhat compile` in packages/enclave-contracts)" + ); + return; + } + }; + let verifier_bytecode_hex = match read_artifact_bytecode_hex("DkgPkVerifier.json") { + Some(h) => h, + None => { + println!( + "skipping: DkgPkVerifier artifact not found \ + (run `npx hardhat compile` in packages/enclave-contracts)" + ); + return; + } + }; + + if !find_anvil().await { + println!("skipping: anvil not found on PATH"); + return; + } + + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + + let lib_bytecode = decode_bytecode(&lib_bytecode_hex); + let lib_deploy_tx = TransactionRequest::default().with_deploy_code(Bytes::from(lib_bytecode)); + let lib_receipt = provider + .send_transaction(lib_deploy_tx) + .await + .expect("failed to send ZKTranscriptLib deploy tx") + .get_receipt() + .await + .expect("failed to get ZKTranscriptLib deploy receipt"); + let lib_address = lib_receipt + .contract_address + .expect("ZKTranscriptLib deploy receipt missing contract address"); + println!("ZKTranscriptLib deployed at: {lib_address}"); + + let linked_bytecode = link_transcript_lib(&verifier_bytecode_hex, &lib_address); + let verifier_deploy_tx = + TransactionRequest::default().with_deploy_code(Bytes::from(linked_bytecode)); + let verifier_receipt = provider + .send_transaction(verifier_deploy_tx) + .await + .expect("failed to send DkgPkVerifier deploy tx") + .get_receipt() + .await + .expect("failed to get DkgPkVerifier deploy receipt"); + let verifier_address = verifier_receipt + .contract_address + .expect("DkgPkVerifier deploy receipt missing contract address"); + println!("DkgPkVerifier deployed at: {verifier_address}"); + + let verifier = DkgPkVerifier::new(verifier_address, &provider); + + // Verify proof on-chain + + let proof_bytes = Bytes::copy_from_slice(&proof.data); + + // pk_bfv has 17 public inputs, 16 are pairing points baked into the proof, + // so only 1 (the pk commitment) gets passed as publicInputs to the contract. + let public_inputs: Vec> = proof + .public_signals + .chunks(32) + .map(|chunk| { + let mut buf = [0u8; 32]; + buf[..chunk.len()].copy_from_slice(chunk); + FixedBytes::from(buf) + }) + .collect(); + + assert_eq!( + public_inputs.len(), + 1, + "pk_bfv circuit should produce exactly 1 public input (commitment), got {}", + public_inputs.len() + ); + + println!( + "calling on-chain verify with {} proof bytes, {} public input(s)", + proof_bytes.len(), + public_inputs.len() + ); + + let verified = verifier + .verify(proof_bytes, public_inputs) + .call() + .await + .expect("on-chain verification call reverted — the proof should be valid"); + + assert!( + verified, + "on-chain ZK proof verification should return true" + ); + + println!("on-chain verification passed"); + + prover.cleanup(e3_id).unwrap(); +} diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index 5aa52de380..71aa512ad3 100644 --- a/templates/default/tests/integration.spec.ts +++ b/templates/default/tests/integration.spec.ts @@ -162,7 +162,7 @@ describe('Integration', () => { const { waitForEvent } = await setupEventListeners(sdk, store) const threshold: [number, number] = [DEFAULT_E3_CONFIG.threshold_min, DEFAULT_E3_CONFIG.threshold_max] - const duration = 180 + const duration = 200 const inputWindow = await calculateInputWindow(publicClient, duration) const thresholdBfvParams = await sdk.getThresholdBfvParamsSet() const e3ProgramParams = encodeBfvParams(thresholdBfvParams)