From 57fff91fc828e438ceb4d8ed88014a131a98575f Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:39:34 +0000 Subject: [PATCH 01/11] feat: add share proofs verification --- .../src/enclave_event/compute_request/mod.rs | 1 + .../src/enclave_event/compute_request/zk.rs | 40 +++ .../src/decryption_key_shared_collector.rs | 185 +++++++++++++ crates/keyshare/src/lib.rs | 1 + crates/keyshare/src/threshold_keyshare.rs | 252 ++++++++++++++++-- crates/multithread/src/multithread.rs | 65 ++++- examples/CRISP/scripts/dev.sh | 2 +- examples/CRISP/server/.env.example | 2 +- templates/default/deployed_contracts.json | 28 +- templates/default/enclave.config.yaml | 23 +- templates/default/server/input.ts | 2 +- templates/default/tests/integration.spec.ts | 2 +- 12 files changed, 554 insertions(+), 49 deletions(-) create mode 100644 crates/keyshare/src/decryption_key_shared_collector.rs diff --git a/crates/events/src/enclave_event/compute_request/mod.rs b/crates/events/src/enclave_event/compute_request/mod.rs index fdb8254d44..1577491bf4 100644 --- a/crates/events/src/enclave_event/compute_request/mod.rs +++ b/crates/events/src/enclave_event/compute_request/mod.rs @@ -87,6 +87,7 @@ impl ToString for ComputeRequest { ZkRequest::ShareEncryption(_) => "ZkShareEncryption", ZkRequest::DkgShareDecryption(_) => "ZkDkgShareDecryption", ZkRequest::VerifyShareProofs(_) => "ZkVerifyShareProofs", + ZkRequest::VerifyC4Proofs(_) => "ZkVerifyC4Proofs", }, } .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 73105e70e4..f64a0459fb 100644 --- a/crates/events/src/enclave_event/compute_request/zk.rs +++ b/crates/events/src/enclave_event/compute_request/zk.rs @@ -27,6 +27,8 @@ pub enum ZkRequest { DkgShareDecryption(DkgShareDecryptionProofRequest), /// Batch-verify C2/C3 proofs from other parties. VerifyShareProofs(VerifyShareProofsRequest), + /// Batch-verify C4 proofs from DecryptionKeyShared events. + VerifyC4Proofs(VerifyC4ProofsRequest), } /// Request to generate a proof for share computation (C2a or C2b). @@ -172,6 +174,8 @@ pub enum ZkResponse { DkgShareDecryption(DkgShareDecryptionProofResponse), /// Batch verification results for C2/C3 proofs. VerifyShareProofs(VerifyShareProofsResponse), + /// Batch verification results for C4 proofs. + VerifyC4Proofs(VerifyC4ProofsResponse), } /// Response containing a generated share computation proof. @@ -279,6 +283,42 @@ pub struct PartyVerificationResult { pub failed_signed_payload: Option, } +/// Request to batch-verify C4 proofs from DecryptionKeyShared events. +/// +/// Grouped by sender so the verifier can report honest/dishonest per party. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VerifyC4ProofsRequest { + /// C4 proofs grouped by sender party_id. + pub party_proofs: Vec, +} + +/// C4 proofs from a single sender to verify. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PartyC4ProofsToVerify { + /// The party that generated these proofs. + pub sender_party_id: u64, + /// C4a proof (SecretKey decryption). + pub c4a_proof: Proof, + /// C4b proofs (SmudgingNoise decryption), one per smudging noise index. + pub c4b_proofs: Vec, +} + +/// Batch verification results for C4 proofs. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VerifyC4ProofsResponse { + /// Per-party verification results. + pub party_results: Vec, +} + +/// Verification result for C4 proofs from a single sender. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PartyC4VerificationResult { + /// The party whose C4 proofs were verified. + pub sender_party_id: u64, + /// Whether ALL C4 proofs from this party verified successfully. + pub all_verified: bool, +} + /// ZK-specific error variants. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ZkError { diff --git a/crates/keyshare/src/decryption_key_shared_collector.rs b/crates/keyshare/src/decryption_key_shared_collector.rs new file mode 100644 index 0000000000..9ef48dfcc8 --- /dev/null +++ b/crates/keyshare/src/decryption_key_shared_collector.rs @@ -0,0 +1,185 @@ +// 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 std::{ + collections::{HashMap, HashSet}, + time::{Duration, Instant}, +}; + +use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Message, SpawnHandle}; +use e3_events::{DecryptionKeyShared, E3id, TypedEvent}; +use e3_utils::MAILBOX_LIMIT; +use tracing::{info, warn}; + +use crate::ThresholdKeyshare; + +const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(600); + +enum CollectorState { + Collecting, + Finished, + TimedOut, +} + +/// Message sent when all expected DecryptionKeyShared events have been collected. +#[derive(Message)] +#[rtype(result = "()")] +pub struct AllDecryptionKeySharesCollected { + pub shares: HashMap, +} + +/// Message sent when DecryptionKeyShared collection times out. +#[derive(Message, Clone, Debug)] +#[rtype(result = "()")] +pub struct DecryptionKeySharedCollectionTimeout; + +/// Message sent when DecryptionKeyShared collection fails. +#[derive(Message, Clone, Debug)] +#[rtype(result = "()")] +pub struct DecryptionKeySharedCollectionFailed { + pub e3_id: E3id, + pub reason: String, + pub missing_parties: Vec, +} + +/// Collects `DecryptionKeyShared` events from expected parties in H (Exchange #3). +/// +/// Once all expected events are collected, sends `AllDecryptionKeySharesCollected` +/// to the parent `ThresholdKeyshare` actor for C4 proof verification. +pub struct DecryptionKeySharedCollector { + e3_id: E3id, + /// Party IDs we expect to receive from (H minus self). + expected: HashSet, + parent: Addr, + state: CollectorState, + shares: HashMap, + timeout_handle: Option, +} + +impl DecryptionKeySharedCollector { + pub fn setup( + parent: Addr, + expected_parties: HashSet, + e3_id: E3id, + ) -> Addr { + let collector = Self { + e3_id, + expected: expected_parties, + parent, + state: CollectorState::Collecting, + shares: HashMap::new(), + timeout_handle: None, + }; + collector.start() + } +} + +impl Actor for DecryptionKeySharedCollector { + type Context = actix::Context; + + fn started(&mut self, ctx: &mut Self::Context) { + ctx.set_mailbox_capacity(MAILBOX_LIMIT); + info!( + e3_id = %self.e3_id, + "DecryptionKeySharedCollector started, expecting {} parties, timeout {:?}", + self.expected.len(), + DEFAULT_COLLECTION_TIMEOUT + ); + let handle = ctx.notify_later( + DecryptionKeySharedCollectionTimeout, + DEFAULT_COLLECTION_TIMEOUT, + ); + self.timeout_handle = Some(handle); + } +} + +impl Handler> for DecryptionKeySharedCollector { + type Result = (); + fn handle( + &mut self, + msg: TypedEvent, + ctx: &mut Self::Context, + ) -> Self::Result { + let (msg, ec) = msg.into_components(); + let start = Instant::now(); + + if !matches!(self.state, CollectorState::Collecting) { + return; + } + + let pid = msg.party_id; + if !self.expected.remove(&pid) { + info!( + "DecryptionKeySharedCollector: party {} not in expected set, ignoring", + pid + ); + return; + } + + info!( + "DecryptionKeySharedCollector: received from party {}, waiting on {}", + pid, + self.expected.len() + ); + self.shares.insert(pid, msg); + + if self.expected.is_empty() { + info!("All DecryptionKeyShared events collected"); + self.state = CollectorState::Finished; + + if let Some(handle) = self.timeout_handle.take() { + ctx.cancel_future(handle); + } + + let event: TypedEvent = TypedEvent::new( + AllDecryptionKeySharesCollected { + shares: std::mem::take(&mut self.shares), + }, + ec, + ); + self.parent.do_send(event); + } + + info!( + "Finished processing DecryptionKeyShared in {:?}", + start.elapsed() + ); + } +} + +impl Handler for DecryptionKeySharedCollector { + type Result = (); + fn handle( + &mut self, + _: DecryptionKeySharedCollectionTimeout, + ctx: &mut Self::Context, + ) -> Self::Result { + if !matches!(self.state, CollectorState::Collecting) { + return; + } + + warn!( + e3_id = %self.e3_id, + missing_parties = ?self.expected, + "DecryptionKeyShared collection timed out, {} parties missing", + self.expected.len() + ); + + self.state = CollectorState::TimedOut; + + let missing_parties: Vec = self.expected.iter().copied().collect(); + self.parent.do_send(DecryptionKeySharedCollectionFailed { + e3_id: self.e3_id.clone(), + reason: format!( + "Timeout waiting for DecryptionKeyShared from {} parties", + missing_parties.len() + ), + missing_parties, + }); + + ctx.stop(); + } +} diff --git a/crates/keyshare/src/lib.rs b/crates/keyshare/src/lib.rs index c9d9c80cd9..94c71a2a60 100644 --- a/crates/keyshare/src/lib.rs +++ b/crates/keyshare/src/lib.rs @@ -4,6 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +mod decryption_key_shared_collector; mod encryption_key_collector; pub mod ext; mod repo; diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 4a47abf2ea..3f4b3af22b 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -15,11 +15,12 @@ use e3_events::{ 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, + PartyC4ProofsToVerify, PartyId, PartyProofsToVerify, PkGenerationProofRequest, + PkGenerationProofSigned, Proof, ProofType, Sequenced, ShareComputationProofRequest, + ShareEncryptionProofRequest, SignedProofPayload, ThresholdShare, + ThresholdShareCollectionFailed, ThresholdShareCreated, ThresholdSharePending, TypedEvent, + VerifyC4ProofsRequest, VerifyC4ProofsResponse, VerifyShareProofsRequest, + VerifyShareProofsResponse, ZkRequest, ZkResponse, }; use e3_fhe_params::create_deterministic_crp_from_default_seed; use e3_fhe_params::{build_pair_for_preset, BfvParamSet, BfvPreset}; @@ -40,15 +41,18 @@ use e3_zk_helpers::computation::DkgInputType; use e3_zk_helpers::CiphernodesCommitteeSize; use fhe::bfv::{PublicKey, SecretKey}; use fhe_traits::{DeserializeParametrized, Serialize}; -use rand::{rngs::OsRng, SeedableRng}; -use rand_chacha::ChaCha20Rng; +use rand::rngs::OsRng; use std::{ collections::{HashMap, HashSet}, mem, - sync::{Arc, Mutex}, + sync::Arc, }; use tracing::{error, info, trace, warn}; +use crate::decryption_key_shared_collector::{ + AllDecryptionKeySharesCollected, DecryptionKeySharedCollectionFailed, + DecryptionKeySharedCollector, +}; use crate::encryption_key_collector::{AllEncryptionKeysCollected, EncryptionKeyCollector}; use crate::threshold_share_collector::{ReceivedShareProofs, ThresholdShareCollector}; @@ -356,6 +360,8 @@ pub struct ThresholdKeyshare { cipher: Arc, decryption_key_collector: Option>, encryption_key_collector: Option>, + /// Collector for incoming DecryptionKeyShared events (Exchange #3). + decryption_key_shared_collector: Option>, state: Persistable, share_enc_preset: BfvPreset, /// Temporarily holds shares + proofs while C2/C3 proof verification is in flight. @@ -372,6 +378,8 @@ pub struct ThresholdKeyshare { no_proof_dishonest_parties: Option>, /// Party IDs sent for C2/C3 verification — used to detect missing results in the response. expected_verification_parties: Option>, + /// Honest party IDs after C2/C3 verification — used by DecryptionKeySharedCollector and C4. + honest_parties: Option>, } impl ThresholdKeyshare { @@ -381,6 +389,7 @@ impl ThresholdKeyshare { cipher: params.cipher, decryption_key_collector: None, encryption_key_collector: None, + decryption_key_shared_collector: None, state: params.state, share_enc_preset: params.share_enc_preset, pending_verification_shares: None, @@ -390,6 +399,7 @@ impl ThresholdKeyshare { c4b_correlation_map: HashMap::new(), no_proof_dishonest_parties: None, expected_verification_parties: None, + honest_parties: None, } } } @@ -442,6 +452,32 @@ impl ThresholdKeyshare { Ok(addr.clone()) } + pub fn ensure_decryption_key_shared_collector( + &mut self, + self_addr: Addr, + ) -> Result> { + let Some(state) = self.state.get() else { + bail!("State not found on threshold keyshare."); + }; + + let honest = self + .honest_parties + .as_ref() + .ok_or_else(|| anyhow!("Honest parties not yet defined"))?; + + let expected: HashSet = honest + .iter() + .filter(|&&pid| pid != state.party_id) + .copied() + .collect(); + + let e3_id = state.e3_id.clone(); + let addr = self + .decryption_key_shared_collector + .get_or_insert_with(|| DecryptionKeySharedCollector::setup(self_addr, expected, e3_id)); + Ok(addr.clone()) + } + pub fn handle_threshold_share_created( &mut self, msg: TypedEvent, @@ -580,6 +616,7 @@ impl ThresholdKeyshare { ComputeResponseKind::Zk(zk) => match zk { ZkResponse::VerifyShareProofs(_) => self.handle_verify_share_proofs_response(msg), ZkResponse::DkgShareDecryption(_) => self.handle_c4_proof_response(msg), + ZkResponse::VerifyC4Proofs(_) => self.handle_verify_c4_proofs_response(msg), _ => Ok(()), }, } @@ -1328,6 +1365,10 @@ impl ThresholdKeyshare { }) .collect(); + // Store honest party IDs for later use by DecryptionKeySharedCollector + let honest_party_ids: HashSet = honest_shares.iter().map(|s| s.party_id).collect(); + self.honest_parties = Some(honest_party_ids); + let num_honest = honest_shares.len(); info!( "Decrypting shares from {} honest parties for E3 {}", @@ -1424,6 +1465,12 @@ impl ThresholdKeyshare { self.c4b_correlation_map.clear(); self.expected_c4b_count = num_esi; + // Resolve threshold preset for C4 proof requests + let threshold_preset = self + .share_enc_preset + .threshold_counterpart() + .ok_or_else(|| anyhow!("No threshold counterpart for {:?}", self.share_enc_preset))?; + // Dispatch C4a proof generation (SecretKey decryption) info!( "Dispatching C4a DkgShareDecryption proof (SecretKey) for E3 {} ({} honest, {} moduli)", @@ -1436,7 +1483,7 @@ impl ThresholdKeyshare { num_honest_parties: num_honest, num_moduli: num_moduli_sk, dkg_input_type: DkgInputType::SecretKey, - params_preset: self.share_enc_preset.clone(), + params_preset: threshold_preset, }), CorrelationId::new(), e3_id.clone(), @@ -1458,7 +1505,7 @@ impl ThresholdKeyshare { num_honest_parties: num_honest, num_moduli: num_moduli_esi, dkg_input_type: DkgInputType::SmudgingNoise, - params_preset: self.share_enc_preset.clone(), + params_preset: threshold_preset, }), correlation_id, e3_id.clone(), @@ -1628,11 +1675,140 @@ impl ThresholdKeyshare { s.new_state(next) })?; + // KeyshareCreated (Exchange #4) is deferred until after C4 proof verification + // from all honest parties. Check if C4 proofs are already ready for Exchange #3. + self.try_publish_decryption_key_shared(ec)?; + + Ok(()) + } + + /// Handle all DecryptionKeyShared events collected from honest parties. + /// Build VerifyC4ProofsRequest and dispatch for verification. + pub fn handle_all_decryption_key_shares_collected( + &mut self, + msg: TypedEvent, + ) -> Result<()> { + let (msg, ec) = msg.into_components(); + let state = self.state.try_get()?; + let e3_id = state.get_e3_id(); + + info!( + "AllDecryptionKeySharesCollected for E3 {} ({} shares)", + e3_id, + msg.shares.len() + ); + + // Build C4 proof verification requests from collected shares + let party_proofs: Vec = msg + .shares + .iter() + .map(|(&party_id, share)| PartyC4ProofsToVerify { + sender_party_id: party_id, + c4a_proof: share.c4a_proof.clone(), + c4b_proofs: share.c4b_proofs.clone(), + }) + .collect(); + + if party_proofs.is_empty() { + info!("No C4 proofs to verify — publishing KeyshareCreated directly"); + return self.publish_keyshare_created(ec); + } + + info!( + "Dispatching C4 proof verification for E3 {} ({} parties)", + e3_id, + party_proofs.len() + ); + + let event = ComputeRequest::zk( + ZkRequest::VerifyC4Proofs(VerifyC4ProofsRequest { party_proofs }), + CorrelationId::new(), + e3_id.clone(), + ); + self.bus.publish(event, ec)?; + Ok(()) + } + + /// Handle C4 proof verification results — update honest set H and publish KeyshareCreated. + fn handle_verify_c4_proofs_response(&mut self, msg: TypedEvent) -> Result<()> { + let (msg, ec) = msg.into_components(); + let resp: VerifyC4ProofsResponse = match msg.response { + ComputeResponseKind::Zk(ZkResponse::VerifyC4Proofs(r)) => r, + _ => bail!("Expected VerifyC4Proofs response"), + }; + + let state = self.state.try_get()?; + let e3_id = state.get_e3_id(); + + // Partition into honest and dishonest + let mut c4_dishonest: HashSet = HashSet::new(); + for result in &resp.party_results { + if result.all_verified { + info!( + "Party {} passed C4 verification for E3 {}", + result.sender_party_id, e3_id + ); + } else { + warn!( + "Party {} FAILED C4 verification for E3 {}", + result.sender_party_id, e3_id + ); + c4_dishonest.insert(result.sender_party_id); + } + } + + // Update honest parties set + if !c4_dishonest.is_empty() { + if let Some(ref mut honest) = self.honest_parties { + honest.retain(|pid| !c4_dishonest.contains(pid)); + + let threshold = state.threshold_m; + let honest_count = honest.len() as u64; + if honest_count < threshold { + warn!( + "Too few honest parties after C4 verification for E3 {} ({} honest < {} threshold)", + e3_id, honest_count, threshold + ); + if let Err(err) = self.bus.publish( + E3Failed { + e3_id: e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }, + ec, + ) { + error!("Failed to publish E3Failed: {err}"); + } + return Ok(()); + } + + info!( + "Updated honest set after C4 verification for E3 {}: {} honest ({} removed)", + e3_id, + honest.len(), + c4_dishonest.len() + ); + } + } else { + info!( + "All parties passed C4 verification for E3 {} — publishing KeyshareCreated", + e3_id + ); + } + + // Exchange #4: Publish KeyshareCreated + self.publish_keyshare_created(ec) + } + + /// Publish KeyshareCreated (Exchange #4) with pk_share and signed C1 proof. + fn publish_keyshare_created(&mut self, ec: EventContext) -> Result<()> { let state = self.state.try_get()?; let e3_id = state.get_e3_id(); let address = state.get_address().to_owned(); let current: ReadyForDecryption = state.clone().try_into()?; + info!("Publishing Exchange #4 (KeyshareCreated) for E3 {}", e3_id); + self.bus.publish( KeyshareCreated { pubkey: current.pk_share, @@ -1640,12 +1816,9 @@ impl ThresholdKeyshare { node: address, signed_pk_generation_proof: current.signed_pk_generation_proof, }, - ec.clone(), + ec, )?; - // Check if C4 proofs are already ready — if so, publish Exchange #3 now - self.try_publish_decryption_key_shared(ec)?; - Ok(()) } @@ -1789,7 +1962,10 @@ impl Handler for ThresholdKeyshare { "Received DecryptionKeyShared from party {} for E3 {}", data.party_id, data.e3_id ); - // TODO: Verify C4 proofs and store for threshold decryption + match self.ensure_decryption_key_shared_collector(ctx.address()) { + Ok(collector) => collector.do_send(TypedEvent::new(data, ec)), + Err(e) => warn!("Cannot forward DecryptionKeyShared: {}", e), + } } } EnclaveEventData::ComputeResponse(data) => { @@ -1856,6 +2032,51 @@ impl Handler> for ThresholdKeyshare { } } +impl Handler> for ThresholdKeyshare { + type Result = (); + fn handle( + &mut self, + msg: TypedEvent, + _: &mut Self::Context, + ) -> Self::Result { + trap( + EType::KeyGeneration, + &self.bus.with_ec(msg.get_ctx()), + || self.handle_all_decryption_key_shares_collected(msg), + ) + } +} + +impl Handler for ThresholdKeyshare { + type Result = (); + fn handle( + &mut self, + msg: DecryptionKeySharedCollectionFailed, + _: &mut Self::Context, + ) -> Self::Result { + trap(EType::KeyGeneration, &self.bus.clone(), || { + warn!( + e3_id = %msg.e3_id, + missing_parties = ?msg.missing_parties, + "DecryptionKeyShared collection failed: {}", + msg.reason + ); + + // Clear the collector reference since it's stopped + self.decryption_key_shared_collector = None; + + if let Err(err) = self.bus.publish_without_context(E3Failed { + e3_id: msg.e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }) { + error!("Failed to publish E3Failed: {err}"); + } + Ok(()) + }) + } +} + impl Handler> for ThresholdKeyshare { type Result = (); fn handle( @@ -1931,6 +2152,7 @@ impl Handler for ThresholdKeyshare { fn handle(&mut self, _: E3RequestComplete, ctx: &mut Self::Context) -> Self::Result { self.encryption_key_collector = None; self.decryption_key_collector = None; + self.decryption_key_shared_collector = None; self.notify_sync(ctx, Die); } } diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index 9078ef7d4f..b39919005d 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -24,10 +24,11 @@ use e3_events::EffectsEnabled; use e3_events::{ BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeRequestKind, ComputeResponse, DkgShareDecryptionProofRequest, DkgShareDecryptionProofResponse, EnclaveEvent, - EnclaveEventData, EventPublisher, EventSubscriber, EventType, PartyVerificationResult, - PkBfvProofRequest, PkBfvProofResponse, PkGenerationProofRequest, PkGenerationProofResponse, - ShareComputationProofRequest, ShareComputationProofResponse, ShareEncryptionProofRequest, - ShareEncryptionProofResponse, TypedEvent, VerifyShareProofsRequest, VerifyShareProofsResponse, + EnclaveEventData, EventPublisher, EventSubscriber, EventType, PartyC4VerificationResult, + PartyVerificationResult, PkBfvProofRequest, PkBfvProofResponse, PkGenerationProofRequest, + PkGenerationProofResponse, ShareComputationProofRequest, ShareComputationProofResponse, + ShareEncryptionProofRequest, ShareEncryptionProofResponse, TypedEvent, VerifyC4ProofsRequest, + VerifyC4ProofsResponse, VerifyShareProofsRequest, VerifyShareProofsResponse, ZkError as ZkEventError, ZkRequest, ZkResponse, }; use e3_fhe_params::build_pair_for_preset; @@ -424,6 +425,9 @@ fn handle_zk_request( ZkRequest::VerifyShareProofs(req) => timefunc("zk_verify_share_proofs", id, || { handle_verify_share_proofs(&prover, req, request.clone()) }), + ZkRequest::VerifyC4Proofs(req) => timefunc("zk_verify_c4_proofs", id, || { + handle_verify_c4_proofs(&prover, req, request.clone()) + }), } } @@ -859,3 +863,56 @@ fn handle_verify_share_proofs( request.e3_id, )) } + +fn handle_verify_c4_proofs( + prover: &ZkProver, + req: VerifyC4ProofsRequest, + 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; + + // Verify C4a proof + let c4a_result = prover.verify(&party.c4a_proof, &e3_id_str, sender); + match c4a_result { + Ok(true) => {} + Ok(false) | Err(_) => { + return PartyC4VerificationResult { + sender_party_id: sender, + all_verified: false, + }; + } + } + + // Verify all C4b proofs + for c4b_proof in &party.c4b_proofs { + let result = prover.verify(c4b_proof, &e3_id_str, sender); + match result { + Ok(true) => continue, + Ok(false) | Err(_) => { + return PartyC4VerificationResult { + sender_party_id: sender, + all_verified: false, + }; + } + } + } + + PartyC4VerificationResult { + sender_party_id: sender, + all_verified: true, + } + }) + .collect(); + + Ok(ComputeResponse::zk( + ZkResponse::VerifyC4Proofs(VerifyC4ProofsResponse { party_results }), + request.correlation_id, + request.e3_id, + )) +} diff --git a/examples/CRISP/scripts/dev.sh b/examples/CRISP/scripts/dev.sh index 0933e7b82d..6fcee34ef5 100755 --- a/examples/CRISP/scripts/dev.sh +++ b/examples/CRISP/scripts/dev.sh @@ -35,6 +35,6 @@ pnpm concurrently \ -ks first \ --names "ANVIL,DEPLOY" \ --prefix-colors "blue,green" \ - "anvil --host 0.0.0.0 --chain-id 31337 --block-time 1 --mnemonic 'test test test test test test test test test test test junk'" \ + "anvil --host 0.0.0.0 --chain-id 31337 --block-time 1 --mnemonic 'test test test test test test test test test test test junk' --silent" \ "./scripts/crisp_deploy.sh && ./scripts/dev_services.sh" diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 9cd29c40ed..1ffba7c578 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -23,7 +23,7 @@ FEE_TOKEN_ADDRESS="0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" # After this interval, the computation phase starts automatically # After activation + this interval, ciphernodes are then not responsing to # any more decryption requests -E3_DURATION=320 +E3_DURATION=250 E3_THRESHOLD_MIN=2 E3_THRESHOLD_MAX=5 diff --git a/templates/default/deployed_contracts.json b/templates/default/deployed_contracts.json index eed1dbf9a2..d19125d5f6 100644 --- a/templates/default/deployed_contracts.json +++ b/templates/default/deployed_contracts.json @@ -21,21 +21,21 @@ }, "localhost": { "PoseidonT3": { - "blockNumber": 6, + "blockNumber": 8, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 7, + "blockNumber": 9, "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" }, "EnclaveToken": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 8, + "blockNumber": 10, "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" }, "EnclaveTicketToken": { @@ -44,7 +44,7 @@ "registry": "0x0000000000000000000000000000000000000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 10, + "blockNumber": 12, "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" }, "SlashingManager": { @@ -52,7 +52,7 @@ "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "bondingRegistry": "0x0000000000000000000000000000000000000001" }, - "blockNumber": 11, + "blockNumber": 13, "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" }, "BondingRegistry": { @@ -74,7 +74,7 @@ "proxyAdminAddress": "0x9bd03768a7DCc129555dE410FF8E85528A4F88b5", "implementationAddress": "0x0165878A594ca255338adfa4d48449f69242Eb8F" }, - "blockNumber": 11, + "blockNumber": 13, "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" }, "CiphernodeRegistryOwnable": { @@ -90,7 +90,7 @@ "proxyAdminAddress": "0x8aCd85898458400f7Db866d53FCFF6f0D49741FF", "implementationAddress": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, - "blockNumber": 14, + "blockNumber": 16, "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" }, "Enclave": { @@ -113,7 +113,7 @@ "proxyAdminAddress": "0x8dAF17A20c9DBA35f005b6324F493785D239719d", "implementationAddress": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" }, - "blockNumber": 16, + "blockNumber": 18, "address": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" }, "E3RefundManager": { @@ -129,28 +129,28 @@ "proxyAdminAddress": "0x32467b43BFa67273FC7dDda0999Ee9A12F2AaA08", "implementationAddress": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" }, - "blockNumber": 18, + "blockNumber": 20, "address": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" }, "MockComputeProvider": { - "blockNumber": 20, + "blockNumber": 22, "address": "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44" }, "MockDecryptionVerifier": { - "blockNumber": 21, + "blockNumber": 23, "address": "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f" }, "MockE3Program": { - "blockNumber": 22, + "blockNumber": 24, "address": "0x4A679253410272dd5232B3Ff7cF5dbB88f295319" }, "ImageID": { "address": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933", - "blockNumber": 26 + "blockNumber": 28 }, "MyProgram": { "address": "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E", - "blockNumber": 28 + "blockNumber": 30 } } } \ No newline at end of file diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index c113519398..04b67dd3e3 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -6,21 +6,20 @@ chains: address: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" deploy_block: 1 # Set to actual deploy block enclave: - address: '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e' - deploy_block: 16 + address: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" + deploy_block: 18 ciphernode_registry: - address: '0xa513E6E4b8f2a923D98304ec87F64353C4D5C853' - deploy_block: 14 + address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + deploy_block: 16 bonding_registry: - address: '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318' - deploy_block: 10 - slashing_manager: - address: '0x5FC8d32690cc91D4c39d9d3abcBD16989F875707' - deploy_block: 10 + address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + deploy_block: 13 fee_token: - address: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512' - deploy_block: 6 - + address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" + deploy_block: 9 + e3_program: + address: "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E" + deploy_block: 30 program: dev: true nodes: diff --git a/templates/default/server/input.ts b/templates/default/server/input.ts index 188de168fa..e5e076d120 100644 --- a/templates/default/server/input.ts +++ b/templates/default/server/input.ts @@ -22,7 +22,7 @@ export const publishInput = async ( sender: `0x${string}`, programAddress: `0x${string}`, ): Promise<`0x${string}`> => { - return await walletClient.writeContract({ + return walletClient.writeContract({ address: programAddress as `0x${string}`, abi: MyProgram.abi, functionName: 'publishInput', diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index 71aa512ad3..d80e68a3a7 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 = 200 + const duration = 225 const inputWindow = await calculateInputWindow(publicClient, duration) const thresholdBfvParams = await sdk.getThresholdBfvParamsSet() const e3ProgramParams = encodeBfvParams(thresholdBfvParams) From a88bb82bd3d211f2c70e9178f6ae4745459beb5b Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:17:24 +0000 Subject: [PATCH 02/11] chore: rename to share_decryption --- .../src/enclave_event/compute_request/mod.rs | 2 +- .../src/enclave_event/compute_request/zk.rs | 20 +-- .../enclave_event/decryption_key_shared.rs | 4 +- crates/keyshare/src/threshold_keyshare.rs | 148 ++++++++++-------- crates/multithread/src/multithread.rs | 47 +++--- crates/test-helpers/src/lib.rs | 52 ++++-- crates/tests/tests/integration.rs | 37 ++++- templates/default/enclave.config.yaml | 13 +- 8 files changed, 201 insertions(+), 122 deletions(-) diff --git a/crates/events/src/enclave_event/compute_request/mod.rs b/crates/events/src/enclave_event/compute_request/mod.rs index 1577491bf4..2e790ff44e 100644 --- a/crates/events/src/enclave_event/compute_request/mod.rs +++ b/crates/events/src/enclave_event/compute_request/mod.rs @@ -87,7 +87,7 @@ impl ToString for ComputeRequest { ZkRequest::ShareEncryption(_) => "ZkShareEncryption", ZkRequest::DkgShareDecryption(_) => "ZkDkgShareDecryption", ZkRequest::VerifyShareProofs(_) => "ZkVerifyShareProofs", - ZkRequest::VerifyC4Proofs(_) => "ZkVerifyC4Proofs", + ZkRequest::VerifyShareDecryptionProofs(_) => "ZkVerifyShareDecryptionProofs", }, } .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 f64a0459fb..9772e49ab4 100644 --- a/crates/events/src/enclave_event/compute_request/zk.rs +++ b/crates/events/src/enclave_event/compute_request/zk.rs @@ -28,7 +28,7 @@ pub enum ZkRequest { /// Batch-verify C2/C3 proofs from other parties. VerifyShareProofs(VerifyShareProofsRequest), /// Batch-verify C4 proofs from DecryptionKeyShared events. - VerifyC4Proofs(VerifyC4ProofsRequest), + VerifyShareDecryptionProofs(VerifyShareDecryptionProofsRequest), } /// Request to generate a proof for share computation (C2a or C2b). @@ -175,7 +175,7 @@ pub enum ZkResponse { /// Batch verification results for C2/C3 proofs. VerifyShareProofs(VerifyShareProofsResponse), /// Batch verification results for C4 proofs. - VerifyC4Proofs(VerifyC4ProofsResponse), + VerifyShareDecryptionProofs(VerifyShareDecryptionProofsResponse), } /// Response containing a generated share computation proof. @@ -287,32 +287,32 @@ pub struct PartyVerificationResult { /// /// Grouped by sender so the verifier can report honest/dishonest per party. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct VerifyC4ProofsRequest { +pub struct VerifyShareDecryptionProofsRequest { /// C4 proofs grouped by sender party_id. - pub party_proofs: Vec, + pub party_proofs: Vec, } /// C4 proofs from a single sender to verify. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct PartyC4ProofsToVerify { +pub struct PartyShareDecryptionProofsToVerify { /// The party that generated these proofs. pub sender_party_id: u64, /// C4a proof (SecretKey decryption). - pub c4a_proof: Proof, + pub sk_decryption_proof: Proof, /// C4b proofs (SmudgingNoise decryption), one per smudging noise index. - pub c4b_proofs: Vec, + pub esm_decryption_proofs: Vec, } /// Batch verification results for C4 proofs. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct VerifyC4ProofsResponse { +pub struct VerifyShareDecryptionProofsResponse { /// Per-party verification results. - pub party_results: Vec, + pub party_results: Vec, } /// Verification result for C4 proofs from a single sender. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct PartyC4VerificationResult { +pub struct PartyShareDecryptionVerificationResult { /// The party whose C4 proofs were verified. pub sender_party_id: u64, /// Whether ALL C4 proofs from this party verified successfully. diff --git a/crates/events/src/enclave_event/decryption_key_shared.rs b/crates/events/src/enclave_event/decryption_key_shared.rs index 61c308fa47..88f1c15217 100644 --- a/crates/events/src/enclave_event/decryption_key_shared.rs +++ b/crates/events/src/enclave_event/decryption_key_shared.rs @@ -28,9 +28,9 @@ pub struct DecryptionKeyShared { /// Lagrange-interpolated aggregated E_SM polynomials (serialized), one per smudging noise. pub es_poly_sum: Vec, /// C4a proof (SecretKey decryption). - pub c4a_proof: Proof, + pub sk_decryption_proof: Proof, /// C4b proofs (SmudgingNoise decryption), one per smudging noise index. - pub c4b_proofs: Vec, + pub esm_decryption_proofs: Vec, /// Whether this was received from the network. pub external: bool, } diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 3f4b3af22b..2121e7b1eb 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -15,12 +15,12 @@ use e3_events::{ DkgShareDecryptionProofResponse, E3Failed, E3RequestComplete, E3Stage, E3id, EType, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCollectionFailed, EncryptionKeyCreated, EncryptionKeyPending, EventContext, FailureReason, KeyshareCreated, - PartyC4ProofsToVerify, PartyId, PartyProofsToVerify, PkGenerationProofRequest, + PartyId, PartyProofsToVerify, PartyShareDecryptionProofsToVerify, PkGenerationProofRequest, PkGenerationProofSigned, Proof, ProofType, Sequenced, ShareComputationProofRequest, ShareEncryptionProofRequest, SignedProofPayload, ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated, ThresholdSharePending, TypedEvent, - VerifyC4ProofsRequest, VerifyC4ProofsResponse, VerifyShareProofsRequest, - VerifyShareProofsResponse, ZkRequest, ZkResponse, + VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, + VerifyShareProofsRequest, VerifyShareProofsResponse, ZkRequest, ZkResponse, }; use e3_fhe_params::create_deterministic_crp_from_default_seed; use e3_fhe_params::{build_pair_for_preset, BfvParamSet, BfvPreset}; @@ -367,13 +367,13 @@ pub struct ThresholdKeyshare { /// 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, + sk_decryption_proof: Option, /// C4b proofs (SmudgingNoise decryption) — keyed by esi_idx for deterministic ordering. - c4b_proofs: HashMap, + esm_decryption_proofs: HashMap, /// Expected number of C4b proofs (one per smudging noise index). - expected_c4b_count: usize, + expected_esm_decryption_count: usize, /// Maps correlation IDs to esi_idx for C4b proof ordering. - c4b_correlation_map: HashMap, + esm_decryption_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. @@ -393,10 +393,10 @@ impl ThresholdKeyshare { 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(), + sk_decryption_proof: None, + esm_decryption_proofs: HashMap::new(), + expected_esm_decryption_count: 0, + esm_decryption_correlation_map: HashMap::new(), no_proof_dishonest_parties: None, expected_verification_parties: None, honest_parties: None, @@ -615,8 +615,12 @@ impl ThresholdKeyshare { }, ComputeResponseKind::Zk(zk) => match zk { ZkResponse::VerifyShareProofs(_) => self.handle_verify_share_proofs_response(msg), - ZkResponse::DkgShareDecryption(_) => self.handle_c4_proof_response(msg), - ZkResponse::VerifyC4Proofs(_) => self.handle_verify_c4_proofs_response(msg), + ZkResponse::DkgShareDecryption(_) => { + self.handle_share_decryption_proof_response(msg) + } + ZkResponse::VerifyShareDecryptionProofs(_) => { + self.handle_verify_share_decryption_proofs_response(msg) + } _ => Ok(()), }, } @@ -1459,11 +1463,11 @@ impl ThresholdKeyshare { ); 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; + // Reset share decryption proof storage and set expected count + self.sk_decryption_proof = None; + self.esm_decryption_proofs.clear(); + self.esm_decryption_correlation_map.clear(); + self.expected_esm_decryption_count = num_esi; // Resolve threshold preset for C4 proof requests let threshold_preset = self @@ -1476,7 +1480,7 @@ impl ThresholdKeyshare { "Dispatching C4a DkgShareDecryption proof (SecretKey) for E3 {} ({} honest, {} moduli)", e3_id, num_honest, num_moduli_sk ); - let c4a_request = ComputeRequest::zk( + let sk_decryption_request = ComputeRequest::zk( ZkRequest::DkgShareDecryption(DkgShareDecryptionProofRequest { sk_bfv: current.sk_bfv.clone(), honest_ciphertexts_raw: sk_ciphertexts_raw, @@ -1488,7 +1492,7 @@ impl ThresholdKeyshare { CorrelationId::new(), e3_id.clone(), ); - self.bus.publish(c4a_request, ec.clone())?; + self.bus.publish(sk_decryption_request, ec.clone())?; // Dispatch C4b proof generation for each smudging noise index for (esi_idx, esi_cts) in esi_ciphertexts_raw.into_iter().enumerate() { @@ -1497,8 +1501,9 @@ impl ThresholdKeyshare { 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( + self.esm_decryption_correlation_map + .insert(correlation_id, esi_idx); + let esm_decryption_request = ComputeRequest::zk( ZkRequest::DkgShareDecryption(DkgShareDecryptionProofRequest { sk_bfv: current.sk_bfv.clone(), honest_ciphertexts_raw: esi_cts, @@ -1510,14 +1515,17 @@ impl ThresholdKeyshare { correlation_id, e3_id.clone(), ); - self.bus.publish(c4b_request, ec.clone())?; + self.bus.publish(esm_decryption_request, ec.clone())?; } Ok(()) } /// Handle C4 (DkgShareDecryption) proof responses — store for Exchange #3. - fn handle_c4_proof_response(&mut self, msg: TypedEvent) -> Result<()> { + fn handle_share_decryption_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 { @@ -1530,35 +1538,37 @@ impl ThresholdKeyshare { match resp.dkg_input_type { DkgInputType::SecretKey => { - info!("Received C4a proof (SecretKey decryption) for E3 {}", e3_id); - self.c4a_proof = Some(resp.proof); + info!("Received SK share decryption proof for E3 {}", e3_id); + self.sk_decryption_proof = Some(resp.proof); } DkgInputType::SmudgingNoise => { let esi_idx = self - .c4b_correlation_map + .esm_decryption_correlation_map .remove(&correlation_id) .ok_or_else(|| { anyhow!( - "Unknown correlation ID {} for C4b proof in E3 {}", + "Unknown correlation ID {} for ESM share decryption proof in E3 {}", correlation_id, e3_id ) })?; info!( - "Received C4b proof (SmudgingNoise[{}] decryption) for E3 {} ({}/{})", + "Received ESM share decryption proof (SmudgingNoise[{}]) for E3 {} ({}/{})", esi_idx, e3_id, - self.c4b_proofs.len() + 1, - self.expected_c4b_count + self.esm_decryption_proofs.len() + 1, + self.expected_esm_decryption_count ); - self.c4b_proofs.insert(esi_idx, resp.proof); + self.esm_decryption_proofs.insert(esi_idx, resp.proof); } } - if self.c4a_proof.is_some() && self.c4b_proofs.len() == self.expected_c4b_count { + if self.sk_decryption_proof.is_some() + && self.esm_decryption_proofs.len() == self.expected_esm_decryption_count + { info!( - "All C4 proofs received for E3 {} (1 C4a + {} C4b)", - e3_id, self.expected_c4b_count + "All share decryption proofs received for E3 {} (1 SK + {} ESM)", + e3_id, self.expected_esm_decryption_count ); self.try_publish_decryption_key_shared(_ec)?; } @@ -1581,16 +1591,16 @@ impl ThresholdKeyshare { } }; - // Need all C4 proofs - let c4a = match &self.c4a_proof { + // Need all share decryption proofs + let sk_proof = match &self.sk_decryption_proof { Some(p) => p.clone(), None => { - trace!("C4a proof not yet received — deferring Exchange #3"); + trace!("SK share decryption 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"); + if self.esm_decryption_proofs.len() != self.expected_esm_decryption_count { + trace!("Not all ESM share decryption proofs received — deferring Exchange #3"); return Ok(()); } @@ -1613,15 +1623,16 @@ impl ThresholdKeyshare { 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 { + // Assemble ESM decryption proofs in esi_idx order to align with es_poly_sum + let mut esm_proofs_ordered: Vec = + Vec::with_capacity(self.expected_esm_decryption_count); + for idx in 0..self.expected_esm_decryption_count { let proof = self - .c4b_proofs + .esm_decryption_proofs .get(&idx) - .ok_or_else(|| anyhow!("Missing C4b proof for esi_idx {}", idx))? + .ok_or_else(|| anyhow!("Missing ESM share decryption proof for esi_idx {}", idx))? .clone(); - c4b_ordered.push(proof); + esm_proofs_ordered.push(proof); } self.bus.publish( @@ -1631,8 +1642,8 @@ impl ThresholdKeyshare { 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, + sk_decryption_proof: sk_proof, + esm_decryption_proofs: esm_proofs_ordered, external: false, }, ec, @@ -1698,30 +1709,32 @@ impl ThresholdKeyshare { msg.shares.len() ); - // Build C4 proof verification requests from collected shares - let party_proofs: Vec = msg + // Build share decryption proof verification requests from collected shares + let party_proofs: Vec = msg .shares .iter() - .map(|(&party_id, share)| PartyC4ProofsToVerify { + .map(|(&party_id, share)| PartyShareDecryptionProofsToVerify { sender_party_id: party_id, - c4a_proof: share.c4a_proof.clone(), - c4b_proofs: share.c4b_proofs.clone(), + sk_decryption_proof: share.sk_decryption_proof.clone(), + esm_decryption_proofs: share.esm_decryption_proofs.clone(), }) .collect(); if party_proofs.is_empty() { - info!("No C4 proofs to verify — publishing KeyshareCreated directly"); + info!("No share decryption proofs to verify — publishing KeyshareCreated directly"); return self.publish_keyshare_created(ec); } info!( - "Dispatching C4 proof verification for E3 {} ({} parties)", + "Dispatching share decryption proof verification for E3 {} ({} parties)", e3_id, party_proofs.len() ); let event = ComputeRequest::zk( - ZkRequest::VerifyC4Proofs(VerifyC4ProofsRequest { party_proofs }), + ZkRequest::VerifyShareDecryptionProofs(VerifyShareDecryptionProofsRequest { + party_proofs, + }), CorrelationId::new(), e3_id.clone(), ); @@ -1729,19 +1742,22 @@ impl ThresholdKeyshare { Ok(()) } - /// Handle C4 proof verification results — update honest set H and publish KeyshareCreated. - fn handle_verify_c4_proofs_response(&mut self, msg: TypedEvent) -> Result<()> { + /// Handle share decryption proof verification results — update honest set H and publish KeyshareCreated. + fn handle_verify_share_decryption_proofs_response( + &mut self, + msg: TypedEvent, + ) -> Result<()> { let (msg, ec) = msg.into_components(); - let resp: VerifyC4ProofsResponse = match msg.response { - ComputeResponseKind::Zk(ZkResponse::VerifyC4Proofs(r)) => r, - _ => bail!("Expected VerifyC4Proofs response"), + let resp: VerifyShareDecryptionProofsResponse = match msg.response { + ComputeResponseKind::Zk(ZkResponse::VerifyShareDecryptionProofs(r)) => r, + _ => bail!("Expected VerifyShareDecryptionProofs response"), }; let state = self.state.try_get()?; let e3_id = state.get_e3_id(); // Partition into honest and dishonest - let mut c4_dishonest: HashSet = HashSet::new(); + let mut share_decryption_dishonest: HashSet = HashSet::new(); for result in &resp.party_results { if result.all_verified { info!( @@ -1753,14 +1769,14 @@ impl ThresholdKeyshare { "Party {} FAILED C4 verification for E3 {}", result.sender_party_id, e3_id ); - c4_dishonest.insert(result.sender_party_id); + share_decryption_dishonest.insert(result.sender_party_id); } } // Update honest parties set - if !c4_dishonest.is_empty() { + if !share_decryption_dishonest.is_empty() { if let Some(ref mut honest) = self.honest_parties { - honest.retain(|pid| !c4_dishonest.contains(pid)); + honest.retain(|pid| !share_decryption_dishonest.contains(pid)); let threshold = state.threshold_m; let honest_count = honest.len() as u64; @@ -1786,7 +1802,7 @@ impl ThresholdKeyshare { "Updated honest set after C4 verification for E3 {}: {} honest ({} removed)", e3_id, honest.len(), - c4_dishonest.len() + share_decryption_dishonest.len() ); } } else { diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index b39919005d..9aa284140b 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -24,11 +24,12 @@ use e3_events::EffectsEnabled; use e3_events::{ BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeRequestKind, ComputeResponse, DkgShareDecryptionProofRequest, DkgShareDecryptionProofResponse, EnclaveEvent, - EnclaveEventData, EventPublisher, EventSubscriber, EventType, PartyC4VerificationResult, - PartyVerificationResult, PkBfvProofRequest, PkBfvProofResponse, PkGenerationProofRequest, - PkGenerationProofResponse, ShareComputationProofRequest, ShareComputationProofResponse, - ShareEncryptionProofRequest, ShareEncryptionProofResponse, TypedEvent, VerifyC4ProofsRequest, - VerifyC4ProofsResponse, VerifyShareProofsRequest, VerifyShareProofsResponse, + EnclaveEventData, EventPublisher, EventSubscriber, EventType, + PartyShareDecryptionVerificationResult, PartyVerificationResult, PkBfvProofRequest, + PkBfvProofResponse, PkGenerationProofRequest, PkGenerationProofResponse, + ShareComputationProofRequest, ShareComputationProofResponse, ShareEncryptionProofRequest, + ShareEncryptionProofResponse, TypedEvent, VerifyShareDecryptionProofsRequest, + VerifyShareDecryptionProofsResponse, VerifyShareProofsRequest, VerifyShareProofsResponse, ZkError as ZkEventError, ZkRequest, ZkResponse, }; use e3_fhe_params::build_pair_for_preset; @@ -425,9 +426,11 @@ fn handle_zk_request( ZkRequest::VerifyShareProofs(req) => timefunc("zk_verify_share_proofs", id, || { handle_verify_share_proofs(&prover, req, request.clone()) }), - ZkRequest::VerifyC4Proofs(req) => timefunc("zk_verify_c4_proofs", id, || { - handle_verify_c4_proofs(&prover, req, request.clone()) - }), + ZkRequest::VerifyShareDecryptionProofs(req) => { + timefunc("zk_verify_share_decryption_proofs", id, || { + handle_verify_share_decryption_proofs(&prover, req, request.clone()) + }) + } } } @@ -864,38 +867,38 @@ fn handle_verify_share_proofs( )) } -fn handle_verify_c4_proofs( +fn handle_verify_share_decryption_proofs( prover: &ZkProver, - req: VerifyC4ProofsRequest, + req: VerifyShareDecryptionProofsRequest, request: ComputeRequest, ) -> Result { let e3_id_str = request.e3_id.to_string(); - let party_results: Vec = req + let party_results: Vec = req .party_proofs .into_iter() .map(|party| { let sender = party.sender_party_id; - // Verify C4a proof - let c4a_result = prover.verify(&party.c4a_proof, &e3_id_str, sender); - match c4a_result { + // Verify SK decryption proof + let sk_result = prover.verify(&party.sk_decryption_proof, &e3_id_str, sender); + match sk_result { Ok(true) => {} Ok(false) | Err(_) => { - return PartyC4VerificationResult { + return PartyShareDecryptionVerificationResult { sender_party_id: sender, all_verified: false, }; } } - // Verify all C4b proofs - for c4b_proof in &party.c4b_proofs { - let result = prover.verify(c4b_proof, &e3_id_str, sender); + // Verify all ESM decryption proofs + for esm_proof in &party.esm_decryption_proofs { + let result = prover.verify(esm_proof, &e3_id_str, sender); match result { Ok(true) => continue, Ok(false) | Err(_) => { - return PartyC4VerificationResult { + return PartyShareDecryptionVerificationResult { sender_party_id: sender, all_verified: false, }; @@ -903,7 +906,7 @@ fn handle_verify_c4_proofs( } } - PartyC4VerificationResult { + PartyShareDecryptionVerificationResult { sender_party_id: sender, all_verified: true, } @@ -911,7 +914,9 @@ fn handle_verify_c4_proofs( .collect(); Ok(ComputeResponse::zk( - ZkResponse::VerifyC4Proofs(VerifyC4ProofsResponse { party_results }), + ZkResponse::VerifyShareDecryptionProofs(VerifyShareDecryptionProofsResponse { + party_results, + }), request.correlation_id, request.e3_id, )) diff --git a/crates/test-helpers/src/lib.rs b/crates/test-helpers/src/lib.rs index 947f8652ad..a90588bada 100644 --- a/crates/test-helpers/src/lib.rs +++ b/crates/test-helpers/src/lib.rs @@ -15,8 +15,9 @@ use alloy::primitives::Address; use anyhow::*; use e3_ciphernode_builder::{CiphernodeHandle, EventSystem}; use e3_events::{ - BusHandle, CiphernodeAdded, EnclaveEvent, EnclaveEventData, EventBus, EventBusConfig, - EventPublisher, EventType, HistoryCollector, Seed, Subscribe, + BusHandle, CiphernodeAdded, Enabled, EnclaveEvent, EnclaveEventData, EventBus, EventBusConfig, + EventContextAccessors, EventPublisher, EventSubscriber, EventType, HistoryCollector, Seed, + Sequenced, Subscribe, }; use e3_fhe_params::BfvParamSet; use e3_fhe_params::DEFAULT_BFV_PRESET; @@ -94,6 +95,38 @@ pub fn get_common_setup( Ok((handle, rng, seed, params, crpoly, errors, history)) } +/// Actor that pipes events between buses, filtering for broadcastable events +/// and transforming document-publisher events to simulate network receipt +/// (e.g. setting `external: true` on `DecryptionKeyShared`). +struct SimulatedNetPipe { + dest: BusHandle, +} + +impl Actor for SimulatedNetPipe { + type Context = actix::Context; +} + +impl Handler> for SimulatedNetPipe { + type Result = (); + fn handle(&mut self, msg: EnclaveEvent, _: &mut Self::Context) -> Self::Result { + let should_forward = NetEventTranslator::is_forwardable_event(&msg) + || DocumentPublisher::is_document_publisher_event(&msg); + + if should_forward { + let source = msg.source(); + let (mut data, ts) = msg.split(); + + // Simulate network receive: in production, DocumentPublisher + // sets external=true when reconstructing events from the network. + if let EnclaveEventData::DecryptionKeyShared(ref mut dks) = data { + dks.external = true; + } + + let _ = self.dest.publish_from_remote(data, ts, None, source); + } + } +} + /// Simulate libp2p by taking output events on each local bus and filter for !is_local_only() and forward remaining events back to the event bus /// deduplication will remove previously seen events. /// This sets up a set of cyphernodes without libp2p. @@ -108,7 +141,7 @@ pub fn get_common_setup( /// ┌────────────┼────────────┐ /// │ │ │ /// ▼ ▼ ▼ -/// ┌────┐ ┌────┐ ┌────┐ +/// ┌────┐ ┌────┐ ┌────┐ /// │ B1 │ │ B2 │ │ B3 │◀──┐ /// └────┘ └────┘ └────┘ │ /// │ │ │ │ @@ -116,8 +149,8 @@ pub fn get_common_setup( /// └────────────┼────────────┘ │ /// │ │ /// ▼ │ -/// ┌─────┐ │ -/// │ FIL │───────────────┘ +/// ┌─────┐ │ +/// │ FIL │───────────────┘ /// └─────┘ /// ``` pub fn simulate_libp2p_net(nodes: &[CiphernodeHandle]) { @@ -126,13 +159,8 @@ pub fn simulate_libp2p_net(nodes: &[CiphernodeHandle]) { for (_, node) in nodes.iter().enumerate() { let dest = node.bus(); if source != dest { - source.pipe_to(dest, |e: &EnclaveEvent| { - // TODO: Document publisher events need to be - // converted to DocumentReceived events - - NetEventTranslator::is_forwardable_event(e) - || DocumentPublisher::is_document_publisher_event(e) - }); + let pipe = SimulatedNetPipe { dest: dest.clone() }.start(); + source.subscribe(EventType::All, pipe.into()); } else { trace!("Source = Dest! Not piping bus to itself"); } diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index e8ca694818..eeb2f43cfe 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -186,6 +186,24 @@ async fn setup_test_zk_backend() -> (ZkBackend, tempfile::TempDir) { .await .unwrap(); + // Copy C4 (share_decryption) circuit — used for DKG share decryption proofs (Exchange #3) + let share_dec_circuit_dir = circuits_dir.join("dkg").join("share_decryption"); + tokio::fs::create_dir_all(&share_dec_circuit_dir) + .await + .unwrap(); + tokio::fs::copy( + dkg_target.join("share_decryption.json"), + share_dec_circuit_dir.join("share_decryption.json"), + ) + .await + .unwrap(); + tokio::fs::copy( + dkg_target.join("share_decryption.vk"), + share_dec_circuit_dir.join("share_decryption.vk"), + ) + .await + .unwrap(); + let backend = ZkBackend::new(BBPath::Default(bb_binary), circuits_dir, work_dir); (backend, temp) @@ -397,7 +415,7 @@ async fn test_trbfv_actor() -> Result<()> { // Actor system setup // Seems like you cannot send more than one job at a time to rayon - let concurrent_jobs = 1; // leaving at 1 + let concurrent_jobs = 1; let max_threadroom = Multithread::get_max_threads_minus(1); let task_pool = Multithread::create_taskpool(max_threadroom, concurrent_jobs); let multithread_report = MultithreadReport::new(max_threadroom, concurrent_jobs).start(); @@ -575,10 +593,25 @@ async fn test_trbfv_actor() -> Result<()> { .await?; report.push(("All ThresholdShareCreated events", shares_timer.elapsed())); + // Wait for DecryptionKeyShared (Exchange #3) events + // - DecryptionKeyShared × 5 (passes is_document_publisher_event filter) + // Each committee node publishes DecryptionKeyShared after computing its decryption key + // and generating C4 (share decryption) proofs. + let decryption_key_shared_timer = Instant::now(); + let expected: Vec<&str> = (0..5).map(|_| "DecryptionKeyShared").collect(); + let _ = nodes + .take_history_with_timeout(0, expected.len(), Duration::from_secs(1000)) + .await?; + report.push(( + "All DecryptionKeyShared events", + decryption_key_shared_timer.elapsed(), + )); + // Wait for KeyshareCreated + PublicKeyAggregated // - KeyshareCreated × 5 (passes is_forwardable_event filter) // - PublicKeyAggregated × 1 (passes is_forwardable_event filter) - // Internal events (ComputeRequest/Response for CalculateDecryptionKey) stay on local buses. + // After DecryptionKeySharedCollector collects all shares and C4 proofs are verified, + // each party publishes KeyshareCreated. let shares_to_pubkey_agg_timer = Instant::now(); let expected = vec![ "KeyshareCreated", diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index 04b67dd3e3..70c0eb032d 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -2,23 +2,20 @@ chains: - name: "localhost" rpc_url: "ws://localhost:8545" contracts: - e3_program: - address: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" - deploy_block: 1 # Set to actual deploy block enclave: address: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" deploy_block: 18 ciphernode_registry: - address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" - deploy_block: 16 - bonding_registry: address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - deploy_block: 13 + deploy_block: 14 + bonding_registry: + address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + deploy_block: 15 fee_token: address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" deploy_block: 9 e3_program: - address: "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E" + address: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" deploy_block: 30 program: dev: true From 93f6aa575c1ed96c77810fe14d974ea63027f1ae Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sat, 28 Feb 2026 13:18:44 +0500 Subject: [PATCH 03/11] chore: refactor proof gen to zk prover --- .../src/ciphernode_builder.rs | 14 +- .../src/enclave_event/compute_request/zk.rs | 28 +- .../enclave_event/decryption_key_shared.rs | 10 +- .../enclave_event/decryption_share_proofs.rs | 53 + crates/events/src/enclave_event/mod.rs | 16 + .../src/enclave_event/share_verification.rs | 50 + crates/keyshare/src/threshold_keyshare.rs | 955 +++++++++--------- .../keyshare/src/threshold_share_collector.rs | 2 +- crates/multithread/src/multithread.rs | 107 +- crates/zk-prover/src/actors/mod.rs | 6 + crates/zk-prover/src/actors/proof_request.rs | 243 ++++- .../src/actors/proof_verification.rs | 10 + .../src/actors/share_verification.rs | 502 +++++++++ crates/zk-prover/src/lib.rs | 4 +- 14 files changed, 1440 insertions(+), 560 deletions(-) create mode 100644 crates/events/src/enclave_event/decryption_share_proofs.rs create mode 100644 crates/events/src/enclave_event/share_verification.rs create mode 100644 crates/zk-prover/src/actors/share_verification.rs diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index b44edddbbf..b7e936fba0 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -435,6 +435,15 @@ impl CiphernodeBuilder { // Currently hardcoded to InsecureDkg512 for DKG operations. // Production deployments should use BfvPreset::SecureDkg8192. let share_enc_preset = BfvPreset::InsecureDkg512; + + let backend = self + .zk_backend + .as_ref() + .ok_or_else(|| anyhow::anyhow!("ZK backend is required for threshold keyshare"))?; + + // Ensure signer is available before setting up extensions that need it + let signer = provider_cache.ensure_signer().await?; + info!("Setting up ThresholdKeyshareExtension"); e3_builder = e3_builder.with(ThresholdKeyshareExtension::create( &bus, @@ -443,12 +452,7 @@ impl CiphernodeBuilder { share_enc_preset, )); - let backend = self - .zk_backend - .as_ref() - .ok_or_else(|| anyhow::anyhow!("ZK backend is required for threshold keyshare"))?; info!("Setting up ZK actors"); - let signer = provider_cache.ensure_signer().await?; setup_zk_actors(&bus, backend, signer); } diff --git a/crates/events/src/enclave_event/compute_request/zk.rs b/crates/events/src/enclave_event/compute_request/zk.rs index 9772e49ab4..bfe7be131b 100644 --- a/crates/events/src/enclave_event/compute_request/zk.rs +++ b/crates/events/src/enclave_event/compute_request/zk.rs @@ -4,7 +4,8 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::{Proof, ProofType, SignedProofPayload}; +use crate::{Proof, SignedProofPayload}; +use alloy::primitives::Address; use derivative::Derivative; use e3_crypto::SensitiveBytes; use e3_fhe_params::BfvPreset; @@ -271,16 +272,18 @@ pub struct VerifyShareProofsResponse { } /// Verification result for all proofs from a single sender. +/// +/// Used for both C2/C3 and C4 verification results. #[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, + /// ECDSA-recovered address of the signer (set during verification). + pub recovered_address: Option
, } /// Request to batch-verify C4 proofs from DecryptionKeyShared events. @@ -297,26 +300,17 @@ pub struct VerifyShareDecryptionProofsRequest { pub struct PartyShareDecryptionProofsToVerify { /// The party that generated these proofs. pub sender_party_id: u64, - /// C4a proof (SecretKey decryption). - pub sk_decryption_proof: Proof, - /// C4b proofs (SmudgingNoise decryption), one per smudging noise index. - pub esm_decryption_proofs: Vec, + /// Signed C4a proof (SecretKey decryption). + pub signed_sk_decryption_proof: SignedProofPayload, + /// Signed C4b proofs (SmudgingNoise decryption), one per smudging noise index. + pub signed_esm_decryption_proofs: Vec, } /// Batch verification results for C4 proofs. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct VerifyShareDecryptionProofsResponse { /// Per-party verification results. - pub party_results: Vec, -} - -/// Verification result for C4 proofs from a single sender. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct PartyShareDecryptionVerificationResult { - /// The party whose C4 proofs were verified. - pub sender_party_id: u64, - /// Whether ALL C4 proofs from this party verified successfully. - pub all_verified: bool, + pub party_results: Vec, } /// ZK-specific error variants. diff --git a/crates/events/src/enclave_event/decryption_key_shared.rs b/crates/events/src/enclave_event/decryption_key_shared.rs index 88f1c15217..27a173588d 100644 --- a/crates/events/src/enclave_event/decryption_key_shared.rs +++ b/crates/events/src/enclave_event/decryption_key_shared.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::{E3id, Proof}; +use crate::{E3id, SignedProofPayload}; use actix::Message; use derivative::Derivative; use e3_utils::utility_types::ArcBytes; @@ -27,10 +27,10 @@ pub struct DecryptionKeyShared { 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 sk_decryption_proof: Proof, - /// C4b proofs (SmudgingNoise decryption), one per smudging noise index. - pub esm_decryption_proofs: Vec, + /// ECDSA-signed C4a proof (SecretKey decryption) for verification and fault attribution. + pub signed_sk_decryption_proof: SignedProofPayload, + /// ECDSA-signed C4b proofs (SmudgingNoise decryption), one per smudging noise index. + pub signed_esm_decryption_proofs: Vec, /// Whether this was received from the network. pub external: bool, } diff --git a/crates/events/src/enclave_event/decryption_share_proofs.rs b/crates/events/src/enclave_event/decryption_share_proofs.rs new file mode 100644 index 0000000000..d5f8916f2c --- /dev/null +++ b/crates/events/src/enclave_event/decryption_share_proofs.rs @@ -0,0 +1,53 @@ +// 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. + +//! Events for C4 proof generation and signing flow. +//! +//! `DecryptionShareProofsPending` is published by [`ThresholdKeyshare`] when it +//! has computed the decryption data and needs C4 proofs generated and signed. +//! +//! `DecryptionShareProofsSigned` is published by [`ProofRequestActor`] after it +//! has generated C4 proofs, signed them, and is returning them to +//! [`ThresholdKeyshare`] for Exchange #3 publication. + +use crate::{DkgShareDecryptionProofRequest, E3id, SignedProofPayload}; +use e3_utils::utility_types::ArcBytes; +use serde::{Deserialize, Serialize}; + +/// ThresholdKeyshare → ProofRequestActor: generate and sign C4 proofs. +/// +/// Carries both the proof generation inputs (sk_request, esm_requests) +/// and the protocol data (sk_poly_sum, es_poly_sum, node) so that +/// ProofRequestActor can pass them back in [`DecryptionShareProofsSigned`]. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DecryptionShareProofsPending { + pub e3_id: E3id, + pub party_id: u64, + pub node: String, + /// Decrypted SK polynomial sum (for Exchange #3). + pub sk_poly_sum: ArcBytes, + /// Decrypted ES polynomial sums (for Exchange #3). + pub es_poly_sum: Vec, + /// C4a proof request (SecretKey decryption). + pub sk_request: DkgShareDecryptionProofRequest, + /// C4b proof requests (SmudgingNoise decryption), one per ESI index. + pub esm_requests: Vec, +} + +/// ProofRequestActor → ThresholdKeyshare: signed C4 proofs ready. +/// +/// ThresholdKeyshare combines these with state data to publish +/// `DecryptionKeyShared` (Exchange #3). +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DecryptionShareProofsSigned { + pub e3_id: E3id, + pub party_id: u64, + pub node: String, + pub sk_poly_sum: ArcBytes, + pub es_poly_sum: Vec, + pub signed_sk_decryption_proof: SignedProofPayload, + pub signed_esm_decryption_proofs: Vec, +} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 5276e37409..5ddeaa60ea 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -15,6 +15,7 @@ mod committee_requested; mod compute_request; mod configuration_updated; mod decryption_key_shared; +mod decryption_share_proofs; mod decryptionshare_created; mod die; mod e3_failed; @@ -38,6 +39,7 @@ mod proof; mod publickey_aggregated; mod publish_document; mod share_computation_proof_signed; +mod share_verification; mod shutdown; mod signed_proof; mod sync_effect; @@ -63,6 +65,7 @@ pub use committee_requested::*; pub use compute_request::*; pub use configuration_updated::*; pub use decryption_key_shared::*; +pub use decryption_share_proofs::*; pub use decryptionshare_created::*; pub use die::*; pub use e3_failed::*; @@ -87,6 +90,7 @@ pub use proof::*; pub use publickey_aggregated::*; pub use publish_document::*; pub use share_computation_proof_signed::*; +pub use share_verification::*; pub use shutdown::*; pub use signed_proof::*; use strum::IntoStaticStr; @@ -238,6 +242,10 @@ pub enum EnclaveEventData { ComputeResponse(ComputeResponse), // ComputeResponseReceived ComputeRequestError(ComputeRequestError), // ComputeRequestFailed SignedProofFailed(SignedProofFailed), + DecryptionShareProofsPending(DecryptionShareProofsPending), + DecryptionShareProofsSigned(DecryptionShareProofsSigned), + ShareVerificationDispatched(ShareVerificationDispatched), + ShareVerificationComplete(ShareVerificationComplete), OutgoingSyncRequested(OutgoingSyncRequested), NetSyncEventsReceived(NetSyncEventsReceived), HistoricalEvmSyncStart(HistoricalEvmSyncStart), @@ -492,6 +500,10 @@ impl EnclaveEventData { EnclaveEventData::ComputeResponse(ref data) => Some(data.e3_id.clone()), EnclaveEventData::TestEvent(ref data) => data.e3_id.clone(), EnclaveEventData::SignedProofFailed(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::DecryptionShareProofsPending(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::DecryptionShareProofsSigned(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::ShareVerificationDispatched(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::ShareVerificationComplete(ref data) => Some(data.e3_id.clone()), EnclaveEventData::E3Failed(ref data) => Some(data.e3_id.clone()), EnclaveEventData::E3StageChanged(ref data) => Some(data.e3_id.clone()), _ => None, @@ -560,6 +572,10 @@ impl_event_types!( ComputeResponse, ComputeRequestError, SignedProofFailed, + DecryptionShareProofsPending, + DecryptionShareProofsSigned, + ShareVerificationDispatched, + ShareVerificationComplete, OutgoingSyncRequested, NetSyncEventsReceived, HistoricalEvmSyncStart, diff --git a/crates/events/src/enclave_event/share_verification.rs b/crates/events/src/enclave_event/share_verification.rs new file mode 100644 index 0000000000..2ca3c37243 --- /dev/null +++ b/crates/events/src/enclave_event/share_verification.rs @@ -0,0 +1,50 @@ +// 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. + +//! Events for C2/C3/C4 share proof verification flow. +//! +//! `ShareVerificationDispatched` is published by [`ThresholdKeyshare`] when +//! proof verification is needed. [`ShareVerificationActor`] subscribes and +//! orchestrates ECDSA validation + ZK verification via multithread. +//! +//! `ShareVerificationComplete` is published by [`ShareVerificationActor`] +//! when verification finishes, carrying the set of dishonest party IDs. + +use crate::{E3id, PartyProofsToVerify, PartyShareDecryptionProofsToVerify}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; + +/// Which verification phase this request/result refers to. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum VerificationKind { + /// C2/C3 share proof verification (after AllThresholdSharesCollected). + ShareProofs, + /// C4 share decryption proof verification (after AllDecryptionKeySharesCollected). + DecryptionProofs, +} + +/// ThresholdKeyshare → ShareVerificationActor: verify party proofs. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ShareVerificationDispatched { + pub e3_id: E3id, + pub kind: VerificationKind, + /// C2/C3 party proofs (when kind == ShareProofs). + pub share_proofs: Vec, + /// C4 party proofs (when kind == DecryptionProofs). + pub decryption_proofs: Vec, + /// Parties already identified as dishonest before verification + /// (e.g., missing/incomplete proofs). Merged into the final result. + pub pre_dishonest: BTreeSet, +} + +/// ShareVerificationActor → ThresholdKeyshare: verification results. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ShareVerificationComplete { + pub e3_id: E3id, + pub kind: VerificationKind, + /// All dishonest parties (pre-dishonest + ECDSA-failed + ZK-failed). + pub dishonest_parties: BTreeSet, +} diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 2121e7b1eb..9f342a5496 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -11,16 +11,15 @@ use e3_data::Persistable; use e3_events::{ prelude::*, trap, BusHandle, CiphernodeSelected, CiphertextOutputPublished, ComputeRequest, ComputeResponse, ComputeResponseKind, CorrelationId, DecryptionKeyShared, - DecryptionshareCreated, Die, DkgProofSigned, DkgShareDecryptionProofRequest, - DkgShareDecryptionProofResponse, E3Failed, E3RequestComplete, E3Stage, E3id, EType, + DecryptionShareProofsPending, DecryptionshareCreated, Die, DkgProofSigned, + DkgShareDecryptionProofRequest, E3Failed, E3RequestComplete, E3Stage, E3id, EType, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCollectionFailed, EncryptionKeyCreated, EncryptionKeyPending, EventContext, FailureReason, KeyshareCreated, PartyId, PartyProofsToVerify, PartyShareDecryptionProofsToVerify, PkGenerationProofRequest, - PkGenerationProofSigned, Proof, ProofType, Sequenced, ShareComputationProofRequest, - ShareEncryptionProofRequest, SignedProofPayload, ThresholdShare, - ThresholdShareCollectionFailed, ThresholdShareCreated, ThresholdSharePending, TypedEvent, - VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, - VerifyShareProofsRequest, VerifyShareProofsResponse, ZkRequest, ZkResponse, + PkGenerationProofSigned, ProofType, Sequenced, ShareComputationProofRequest, + ShareEncryptionProofRequest, ShareVerificationComplete, ShareVerificationDispatched, + SignedProofPayload, ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated, + ThresholdSharePending, TypedEvent, VerificationKind, }; use e3_fhe_params::create_deterministic_crp_from_default_seed; use e3_fhe_params::{build_pair_for_preset, BfvParamSet, BfvPreset}; @@ -43,7 +42,7 @@ use fhe::bfv::{PublicKey, SecretKey}; use fhe_traits::{DeserializeParametrized, Serialize}; use rand::rngs::OsRng; use std::{ - collections::{HashMap, HashSet}, + collections::{BTreeSet, HashMap, HashSet}, mem, sync::Arc, }; @@ -360,26 +359,21 @@ pub struct ThresholdKeyshare { cipher: Arc, decryption_key_collector: Option>, encryption_key_collector: Option>, - /// Collector for incoming DecryptionKeyShared events (Exchange #3). decryption_key_shared_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. - sk_decryption_proof: Option, - /// C4b proofs (SmudgingNoise decryption) — keyed by esi_idx for deterministic ordering. - esm_decryption_proofs: HashMap, - /// Expected number of C4b proofs (one per smudging noise index). - expected_esm_decryption_count: usize, - /// Maps correlation IDs to esi_idx for C4b proof ordering. - esm_decryption_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>, - /// Honest party IDs after C2/C3 verification — used by DecryptionKeySharedCollector and C4. + /// Transient coordination data bridging async gaps — not persisted. + /// Shares pending C2/C3 verification, consumed in `proceed_with_decryption_key_calculation`. + pending_shares: Vec, + /// C4 proof data built during aggregation, consumed after CalculateDecryptionKey. + pending_c4_proof_data: Option<( + DkgShareDecryptionProofRequest, + Vec, + )>, + /// Honest party IDs determined by C2/C3 verification, narrowed by C4. honest_parties: Option>, + /// DecryptionKeyShared events arriving before ReadyForDecryption. + early_decryption_key_shares: HashMap, } impl ThresholdKeyshare { @@ -392,14 +386,10 @@ impl ThresholdKeyshare { decryption_key_shared_collector: None, state: params.state, share_enc_preset: params.share_enc_preset, - pending_verification_shares: None, - sk_decryption_proof: None, - esm_decryption_proofs: HashMap::new(), - expected_esm_decryption_count: 0, - esm_decryption_correlation_map: HashMap::new(), - no_proof_dishonest_parties: None, - expected_verification_parties: None, + pending_shares: Vec::new(), + pending_c4_proof_data: None, honest_parties: None, + early_decryption_key_shares: HashMap::new(), } } } @@ -407,7 +397,7 @@ impl ThresholdKeyshare { impl Actor for ThresholdKeyshare { type Context = actix::Context; fn started(&mut self, ctx: &mut Self::Context) { - ctx.set_mailbox_capacity(MAILBOX_LIMIT) + ctx.set_mailbox_capacity(MAILBOX_LIMIT); } } @@ -452,22 +442,23 @@ impl ThresholdKeyshare { Ok(addr.clone()) } + /// Create or return the DecryptionKeySharedCollector. + /// Uses honest_parties from the struct. pub fn ensure_decryption_key_shared_collector( &mut self, self_addr: Addr, ) -> Result> { - let Some(state) = self.state.get() else { - bail!("State not found on threshold keyshare."); - }; + let state = self.state.try_get()?; + let my_party_id = state.party_id; let honest = self .honest_parties .as_ref() - .ok_or_else(|| anyhow!("Honest parties not yet defined"))?; + .ok_or_else(|| anyhow!("honest_parties not set when creating collector"))?; let expected: HashSet = honest .iter() - .filter(|&&pid| pid != state.party_id) + .filter(|&&pid| pid != my_party_id) .copied() .collect(); @@ -598,7 +589,11 @@ impl ThresholdKeyshare { Ok(()) } - pub fn handle_compute_response(&mut self, msg: TypedEvent) -> Result<()> { + pub fn handle_compute_response( + &mut self, + msg: TypedEvent, + self_addr: Addr, + ) -> Result<()> { match &msg.response { ComputeResponseKind::TrBFV(trbfv) => match trbfv { TrBFVResponse::GenEsiSss(_) => self.handle_gen_esi_sss_response(msg), @@ -606,23 +601,16 @@ impl ThresholdKeyshare { self.handle_gen_pk_share_and_sk_sss_response(msg) } TrBFVResponse::CalculateDecryptionKey(_) => { - self.handle_calculate_decryption_key_response(msg) + self.handle_calculate_decryption_key_response(msg, self_addr) } TrBFVResponse::CalculateDecryptionShare(_) => { self.handle_calculate_decryption_share_response(msg) } _ => Ok(()), }, - ComputeResponseKind::Zk(zk) => match zk { - ZkResponse::VerifyShareProofs(_) => self.handle_verify_share_proofs_response(msg), - ZkResponse::DkgShareDecryption(_) => { - self.handle_share_decryption_proof_response(msg) - } - ZkResponse::VerifyShareDecryptionProofs(_) => { - self.handle_verify_share_decryption_proofs_response(msg) - } - _ => Ok(()), - }, + // ZK responses (C4 proofs, share/decryption verification) are now + // handled by ProofRequestActor and ShareVerificationActor respectively. + _ => Ok(()), } } @@ -1175,8 +1163,8 @@ impl ThresholdKeyshare { }); } - // Store shares for use after verification completes - self.pending_verification_shares = Some(msg.shares); + // Store shares on the actor for use after verification completes + self.pending_shares = msg.shares.iter().map(|arc| (**arc).clone()).collect(); // Backward compat: only when ALL non-self parties have zero proofs // AND none have incomplete proofs (incomplete proofs are always dishonest) @@ -1195,165 +1183,164 @@ impl ThresholdKeyshare { 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() { + // Merge no-proof and incomplete-proof parties — both are pre-dishonest + let mut pre_dishonest: BTreeSet = BTreeSet::new(); + pre_dishonest.extend(incomplete_proof_parties); + pre_dishonest.extend(no_proof_parties); + if !pre_dishonest.is_empty() { warn!( - "{} parties have missing/incomplete C2/C3 proofs for E3 {} — marking as dishonest: {:?}", - unverified_dishonest.len(), + "{} parties have missing/incomplete C2/C3 proofs for E3 {} — marking as pre-dishonest: {:?}", + pre_dishonest.len(), e3_id, - unverified_dishonest + pre_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); + let dishonest_set: HashSet = pre_dishonest.into_iter().collect(); + return self.proceed_with_decryption_key_calculation(Some(dishonest_set), 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)", + "Dispatching C2/C3 share verification for E3 {} ({} parties, {} pre-dishonest)", e3_id, - party_proofs_to_verify.len() + party_proofs_to_verify.len(), + pre_dishonest.len() ); - let event = ComputeRequest::zk( - ZkRequest::VerifyShareProofs(VerifyShareProofsRequest { - party_proofs: party_proofs_to_verify, - }), - CorrelationId::new(), - e3_id.clone(), - ); - self.bus.publish(event, ec)?; + self.bus.publish( + ShareVerificationDispatched { + e3_id: e3_id.clone(), + kind: VerificationKind::ShareProofs, + share_proofs: party_proofs_to_verify, + decryption_proofs: Vec::new(), + pre_dishonest, + }, + ec, + )?; Ok(()) } - /// Handle C2/C3 proof verification results — define honest set H and proceed. - pub fn handle_verify_share_proofs_response( + /// Handle ShareVerificationComplete from ShareVerificationActor. + /// Dispatched for both C2/C3 and C4 verification. + pub fn handle_share_verification_complete( &mut self, - msg: TypedEvent, + 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); - } - } + match msg.kind { + VerificationKind::ShareProofs => { + // C2/C3 verification complete + if msg.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 - msg.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 + ); + // Clear pending shares + self.pending_shares.clear(); + self.bus.publish( + E3Failed { + e3_id: e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }, + ec, + )?; + return Ok(()); + } - // 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 + let dishonest_set: HashSet = msg.dishonest_parties.into_iter().collect(); + info!( + "Proceeding with {} honest parties for E3 {} ({} dishonest excluded)", + honest_count, + e3_id, + dishonest_set.len() ); - dishonest_parties.insert(*party_id); + self.proceed_with_decryption_key_calculation(Some(dishonest_set), ec) } } - } + VerificationKind::DecryptionProofs => { + // C4 verification complete — update honest set and publish KeyshareCreated + if !msg.dishonest_parties.is_empty() { + if let Some(ref mut honest) = self.honest_parties { + honest.retain(|pid| !msg.dishonest_parties.contains(pid)); + } - 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; + let threshold = state.threshold_m; + let honest_count = self + .honest_parties + .as_ref() + .map(|h| h.len() as u64) + .unwrap_or(0); + + if honest_count < threshold { + warn!( + "Too few honest parties after C4 for E3 {} ({} honest < {} threshold)", + e3_id, honest_count, threshold + ); + self.bus.publish( + E3Failed { + e3_id: e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }, + ec, + )?; + return Ok(()); + } - 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}"); + info!( + "Updated honest set after C4 for E3 {}: {} honest ({} removed)", + e3_id, + honest_count, + msg.dishonest_parties.len() + ); + } else { + info!( + "All parties passed C4 verification for E3 {} — publishing KeyshareCreated", + e3_id + ); } - 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) + self.publish_keyshare_created(ec) + } } } - /// After verification, decrypt shares from honest parties, compute decryption key, - /// and dispatch C4 proof generation. + /// After verification, decrypt shares from honest parties and compute decryption key. + /// C4 proof generation is deferred to ProofRequestActor via DecryptionShareProofsPending. 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(); let party_id = state.party_id as usize; let trbfv_config = state.get_trbfv_config(); - // Get our BFV secret key from state + // Get our BFV secret key from state, pending shares from the actor let current: AggregatingDecryptionKey = state.clone().try_into()?; + let shares = std::mem::take(&mut self.pending_shares); + if shares.is_empty() { + bail!("No pending verification shares"); + } let sk_bytes = current.sk_bfv.access(&cipher)?; let params = BfvParamSet::from(self.share_enc_preset.clone()).build_arc(); let sk_bfv = deserialize_secret_key(&sk_bytes, ¶ms)?; @@ -1369,9 +1356,8 @@ impl ThresholdKeyshare { }) .collect(); - // Store honest party IDs for later use by DecryptionKeySharedCollector + // Store honest party IDs in state let honest_party_ids: HashSet = honest_shares.iter().map(|s| s.party_id).collect(); - self.honest_parties = Some(honest_party_ids); let num_honest = honest_shares.len(); info!( @@ -1379,42 +1365,75 @@ impl ThresholdKeyshare { num_honest, e3_id ); - // Collect ciphertext bytes for C4 proof generation BEFORE decrypting + // Collect ciphertext bytes for C4 proof requests (built here, sent after CalculateDecryptionKey) // C4a: sk_sss ciphertexts from honest parties [H * L] let mut sk_ciphertexts_raw = Vec::new(); - let mut num_moduli_sk = 0; + let mut num_moduli_sk: Option = None; 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(); + let moduli = share.num_moduli(); + match num_moduli_sk { + Some(expected) if expected != moduli => { + bail!( + "Party {} has inconsistent sk num_moduli ({} vs expected {})", + ts.party_id, + moduli, + expected + ); + } + None => num_moduli_sk = Some(moduli), + _ => {} + } for ct_bytes in share.ciphertext_bytes() { sk_ciphertexts_raw.push(ct_bytes.clone()); } } + let num_moduli_sk = num_moduli_sk.unwrap_or(0); // 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); + for ts in &honest_shares { + if ts.esi_sss.len() != num_esi { + bail!( + "Party {} has inconsistent esi_sss count ({} vs expected {})", + ts.party_id, + ts.esi_sss.len(), + num_esi + ); + } + } let mut esi_ciphertexts_raw: Vec> = vec![Vec::new(); num_esi]; - let mut num_moduli_esi = 0; + let mut num_moduli_esi: Option = None; 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(); + let moduli = share.num_moduli(); + match num_moduli_esi { + Some(expected) if expected != moduli => { + bail!( + "Party {} has inconsistent esi num_moduli at esi_idx {} ({} vs expected {})", + ts.party_id, esi_idx, moduli, expected + ); + } + None => num_moduli_esi = Some(moduli), + _ => {} + } for ct_bytes in share.ciphertext_bytes() { esi_ciphertexts_raw[esi_idx].push(ct_bytes.clone()); } } } + let num_moduli_esi = num_moduli_esi.unwrap_or(0); // Decrypt our share from each honest sender using BFV let sk_sss_collected: Vec = honest_shares @@ -1429,7 +1448,6 @@ impl ThresholdKeyshare { }) .collect::>()?; - // Similarly decrypt esi_sss for each ciphertext let esi_sss_collected: Vec> = honest_shares .iter() .map(|ts| { @@ -1463,151 +1481,92 @@ impl ThresholdKeyshare { ); self.bus.publish(event, ec.clone())?; - // Reset share decryption proof storage and set expected count - self.sk_decryption_proof = None; - self.esm_decryption_proofs.clear(); - self.esm_decryption_correlation_map.clear(); - self.expected_esm_decryption_count = num_esi; - - // Resolve threshold preset for C4 proof requests + // Build C4 proof requests — stored for sending after CalculateDecryptionKey completes let threshold_preset = self .share_enc_preset .threshold_counterpart() .ok_or_else(|| anyhow!("No threshold counterpart for {:?}", self.share_enc_preset))?; - // Dispatch C4a proof generation (SecretKey decryption) - info!( - "Dispatching C4a DkgShareDecryption proof (SecretKey) for E3 {} ({} honest, {} moduli)", - e3_id, num_honest, num_moduli_sk - ); - let sk_decryption_request = ComputeRequest::zk( - ZkRequest::DkgShareDecryption(DkgShareDecryptionProofRequest { + let sk_request = 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: threshold_preset, + }; + + let esm_requests: Vec = esi_ciphertexts_raw + .into_iter() + .map(|esi_cts| DkgShareDecryptionProofRequest { sk_bfv: current.sk_bfv.clone(), - honest_ciphertexts_raw: sk_ciphertexts_raw, + honest_ciphertexts_raw: esi_cts, num_honest_parties: num_honest, - num_moduli: num_moduli_sk, - dkg_input_type: DkgInputType::SecretKey, + num_moduli: num_moduli_esi, + dkg_input_type: DkgInputType::SmudgingNoise, params_preset: threshold_preset, - }), - CorrelationId::new(), - e3_id.clone(), - ); - self.bus.publish(sk_decryption_request, ec.clone())?; + }) + .collect(); - // 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.esm_decryption_correlation_map - .insert(correlation_id, esi_idx); - let esm_decryption_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: threshold_preset, - }), - correlation_id, - e3_id.clone(), - ); - self.bus.publish(esm_decryption_request, ec.clone())?; - } + // Store honest parties and C4 data on the actor (transient coordination) + self.honest_parties = Some(honest_party_ids); + self.pending_c4_proof_data = Some((sk_request, esm_requests)); Ok(()) } - /// Handle C4 (DkgShareDecryption) proof responses — store for Exchange #3. - fn handle_share_decryption_proof_response( + /// 5a. CalculateDecryptionKeyResponse — transition to ReadyForDecryption, + /// then publish DecryptionShareProofsPending so ProofRequestActor can + /// generate C4 proofs, sign them, and publish DecryptionKeyShared. + pub fn handle_calculate_decryption_key_response( &mut self, - msg: TypedEvent, + res: TypedEvent, + self_addr: Addr, ) -> 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 (res, ec) = res.into_components(); + let output: CalculateDecryptionKeyResponse = res + .try_into() + .context("Error extracting data from compute process")?; - let state = self.state.try_get()?; - let e3_id = state.get_e3_id(); + let (sk_poly_sum, es_poly_sum) = (output.sk_poly_sum, output.es_poly_sum); - match resp.dkg_input_type { - DkgInputType::SecretKey => { - info!("Received SK share decryption proof for E3 {}", e3_id); - self.sk_decryption_proof = Some(resp.proof); - } - DkgInputType::SmudgingNoise => { - let esi_idx = self - .esm_decryption_correlation_map - .remove(&correlation_id) - .ok_or_else(|| { - anyhow!( - "Unknown correlation ID {} for ESM share decryption proof in E3 {}", - correlation_id, - e3_id - ) - })?; - info!( - "Received ESM share decryption proof (SmudgingNoise[{}]) for E3 {} ({}/{})", - esi_idx, - e3_id, - self.esm_decryption_proofs.len() + 1, - self.expected_esm_decryption_count - ); - self.esm_decryption_proofs.insert(esi_idx, resp.proof); - } - } + // Extract C4 data from the actor (stored by proceed_with_decryption_key_calculation) + let (sk_request, esm_requests) = self + .pending_c4_proof_data + .take() + .ok_or_else(|| anyhow!("No pending C4 proof data — CalculateDecryptionKey responded before proof requests were built"))?; - if self.sk_decryption_proof.is_some() - && self.esm_decryption_proofs.len() == self.expected_esm_decryption_count - { - info!( - "All share decryption proofs received for E3 {} (1 SK + {} ESM)", - e3_id, self.expected_esm_decryption_count - ); - self.try_publish_decryption_key_shared(_ec)?; - } + // Take early shares from the actor before transitioning + let early_shares = std::mem::take(&mut self.early_decryption_key_shares); - Ok(()) - } + // Transition to ReadyForDecryption + self.state.try_mutate(&ec, |s| { + use KeyshareState as K; + info!("Try store decryption key"); - /// 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(); + let current: AggregatingDecryptionKey = s.clone().try_into()?; - // 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(()); - } - }; + let next = K::ReadyForDecryption(ReadyForDecryption { + pk_share: current.pk_share, + sk_poly_sum, + es_poly_sum, + signed_pk_generation_proof: current.signed_pk_generation_proof, + signed_sk_share_computation_proof: current.signed_sk_share_computation_proof, + signed_e_sm_share_computation_proof: current.signed_e_sm_share_computation_proof, + signed_sk_share_encryption_proofs: current.signed_sk_share_encryption_proofs, + signed_e_sm_share_encryption_proofs: current.signed_e_sm_share_encryption_proofs, + }); - // Need all share decryption proofs - let sk_proof = match &self.sk_decryption_proof { - Some(p) => p.clone(), - None => { - trace!("SK share decryption proof not yet received — deferring Exchange #3"); - return Ok(()); - } - }; - if self.esm_decryption_proofs.len() != self.expected_esm_decryption_count { - trace!("Not all ESM share decryption proofs received — deferring Exchange #3"); - return Ok(()); - } + s.new_state(next) + })?; + // Publish DecryptionShareProofsPending to ProofRequestActor + let state = self.state.try_get()?; + let e3_id = state.get_e3_id(); 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 ready: ReadyForDecryption = state.clone().try_into()?; let sk_poly_sum_bytes = ready.sk_poly_sum.access(&self.cipher)?; let es_poly_sum_bytes: Vec = ready .es_poly_sum @@ -1619,203 +1578,139 @@ impl ThresholdKeyshare { .collect::>()?; info!( - "Publishing Exchange #3 (DecryptionKeyShared) for E3 {} party {}", - e3_id, party_id + "Publishing DecryptionShareProofsPending for E3 {} party {} (1 SK + {} ESM requests)", + e3_id, + party_id, + esm_requests.len() ); - // Assemble ESM decryption proofs in esi_idx order to align with es_poly_sum - let mut esm_proofs_ordered: Vec = - Vec::with_capacity(self.expected_esm_decryption_count); - for idx in 0..self.expected_esm_decryption_count { - let proof = self - .esm_decryption_proofs - .get(&idx) - .ok_or_else(|| anyhow!("Missing ESM share decryption proof for esi_idx {}", idx))? - .clone(); - esm_proofs_ordered.push(proof); - } - self.bus.publish( - DecryptionKeyShared { + DecryptionShareProofsPending { 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, - sk_decryption_proof: sk_proof, - esm_decryption_proofs: esm_proofs_ordered, - external: false, + sk_request, + esm_requests, }, - ec, + ec.clone(), )?; + // Create collector and replay any early-arriving DecryptionKeyShared events + let state = self.state.try_get()?; + let my_party_id = state.party_id; + let honest = self.honest_parties.as_ref().cloned().unwrap_or_default(); + let expected: HashSet = honest + .iter() + .filter(|&&pid| pid != my_party_id) + .copied() + .collect(); + + if !expected.is_empty() { + let collector = self.ensure_decryption_key_shared_collector(self_addr)?; + for (_pid, share) in early_shares { + collector.do_send(TypedEvent::new(share, ec.clone())); + } + } + Ok(()) } - /// 5a. CalculateDecryptionKeyResponse -> KeyshareCreated - pub fn handle_calculate_decryption_key_response( + /// Handle an external DecryptionKeyShared event while in AggregatingDecryptionKey state. + /// Store it for later processing when we transition to ReadyForDecryption. + fn handle_early_decryption_key_share( &mut self, - res: TypedEvent, + data: DecryptionKeyShared, + _ec: EventContext, ) -> Result<()> { - let (res, ec) = res.into_components(); - let output: CalculateDecryptionKeyResponse = res - .try_into() - .context("Error extracting data from compute process")?; - - let (sk_poly_sum, es_poly_sum) = (output.sk_poly_sum, output.es_poly_sum); - - self.state.try_mutate(&ec, |s| { - use KeyshareState as K; - info!("Try store decryption key"); - - // Get pk_share and signed proof from current state - let current: AggregatingDecryptionKey = s.clone().try_into()?; - - // Transition to ReadyForDecryption, carrying the signed proof - let next = K::ReadyForDecryption(ReadyForDecryption { - pk_share: current.pk_share, - sk_poly_sum, - es_poly_sum, - signed_pk_generation_proof: current.signed_pk_generation_proof, - signed_sk_share_computation_proof: current.signed_sk_share_computation_proof, - signed_e_sm_share_computation_proof: current.signed_e_sm_share_computation_proof, - signed_sk_share_encryption_proofs: current.signed_sk_share_encryption_proofs, - signed_e_sm_share_encryption_proofs: current.signed_e_sm_share_encryption_proofs, - }); - - s.new_state(next) - })?; - - // KeyshareCreated (Exchange #4) is deferred until after C4 proof verification - // from all honest parties. Check if C4 proofs are already ready for Exchange #3. - self.try_publish_decryption_key_shared(ec)?; - + let party_id = data.party_id; + info!( + "Storing early DecryptionKeyShared from party {} (state: AggregatingDecryptionKey)", + party_id + ); + self.early_decryption_key_shares.insert(party_id, data); Ok(()) } - /// Handle all DecryptionKeyShared events collected from honest parties. - /// Build VerifyC4ProofsRequest and dispatch for verification. - pub fn handle_all_decryption_key_shares_collected( + /// Dispatch C4 verification for all collected DecryptionKeyShared events. + /// Shares are provided by the DecryptionKeySharedCollector. + fn dispatch_c4_verification( &mut self, - msg: TypedEvent, + collected_shares: HashMap, + ec: EventContext, ) -> Result<()> { - let (msg, ec) = msg.into_components(); let state = self.state.try_get()?; let e3_id = state.get_e3_id(); + let ready: ReadyForDecryption = state.clone().try_into()?; info!( - "AllDecryptionKeySharesCollected for E3 {} ({} shares)", + "All DecryptionKeyShared collected for E3 {} ({} shares)", e3_id, - msg.shares.len() + collected_shares.len() ); - // Build share decryption proof verification requests from collected shares - let party_proofs: Vec = msg - .shares + // Validate ESM proof count — each party must provide exactly + // one C4b proof per smudging noise index. + let expected_esm = ready.es_poly_sum.len(); + let mut c4_count_dishonest: HashSet = HashSet::new(); + let party_proofs: Vec = collected_shares .iter() - .map(|(&party_id, share)| PartyShareDecryptionProofsToVerify { - sender_party_id: party_id, - sk_decryption_proof: share.sk_decryption_proof.clone(), - esm_decryption_proofs: share.esm_decryption_proofs.clone(), + .filter_map(|(&party_id, share)| { + if share.signed_esm_decryption_proofs.len() != expected_esm { + warn!( + "Party {} has wrong ESM proof count ({} vs expected {}) for E3 {} — treating as dishonest", + party_id, + share.signed_esm_decryption_proofs.len(), + expected_esm, + e3_id + ); + c4_count_dishonest.insert(party_id); + None + } else { + Some(PartyShareDecryptionProofsToVerify { + sender_party_id: party_id, + signed_sk_decryption_proof: share.signed_sk_decryption_proof.clone(), + signed_esm_decryption_proofs: share.signed_esm_decryption_proofs.clone(), + }) + } }) .collect(); + // Evict pre-dishonest parties (wrong ESM count) from honest set + if !c4_count_dishonest.is_empty() { + if let Some(ref mut honest) = self.honest_parties { + honest.retain(|pid| !c4_count_dishonest.contains(pid)); + } + } + if party_proofs.is_empty() { - info!("No share decryption proofs to verify — publishing KeyshareCreated directly"); + info!("No C4 proofs to verify — publishing KeyshareCreated directly"); return self.publish_keyshare_created(ec); } + let pre_dishonest: BTreeSet = c4_count_dishonest.into_iter().collect(); + info!( - "Dispatching share decryption proof verification for E3 {} ({} parties)", + "Dispatching C4 share verification for E3 {} ({} parties, {} pre-dishonest)", e3_id, - party_proofs.len() + party_proofs.len(), + pre_dishonest.len() ); - let event = ComputeRequest::zk( - ZkRequest::VerifyShareDecryptionProofs(VerifyShareDecryptionProofsRequest { - party_proofs, - }), - CorrelationId::new(), - e3_id.clone(), - ); - self.bus.publish(event, ec)?; + self.bus.publish( + ShareVerificationDispatched { + e3_id: e3_id.clone(), + kind: VerificationKind::DecryptionProofs, + share_proofs: Vec::new(), + decryption_proofs: party_proofs, + pre_dishonest, + }, + ec, + )?; Ok(()) } - /// Handle share decryption proof verification results — update honest set H and publish KeyshareCreated. - fn handle_verify_share_decryption_proofs_response( - &mut self, - msg: TypedEvent, - ) -> Result<()> { - let (msg, ec) = msg.into_components(); - let resp: VerifyShareDecryptionProofsResponse = match msg.response { - ComputeResponseKind::Zk(ZkResponse::VerifyShareDecryptionProofs(r)) => r, - _ => bail!("Expected VerifyShareDecryptionProofs response"), - }; - - let state = self.state.try_get()?; - let e3_id = state.get_e3_id(); - - // Partition into honest and dishonest - let mut share_decryption_dishonest: HashSet = HashSet::new(); - for result in &resp.party_results { - if result.all_verified { - info!( - "Party {} passed C4 verification for E3 {}", - result.sender_party_id, e3_id - ); - } else { - warn!( - "Party {} FAILED C4 verification for E3 {}", - result.sender_party_id, e3_id - ); - share_decryption_dishonest.insert(result.sender_party_id); - } - } - - // Update honest parties set - if !share_decryption_dishonest.is_empty() { - if let Some(ref mut honest) = self.honest_parties { - honest.retain(|pid| !share_decryption_dishonest.contains(pid)); - - let threshold = state.threshold_m; - let honest_count = honest.len() as u64; - if honest_count < threshold { - warn!( - "Too few honest parties after C4 verification for E3 {} ({} honest < {} threshold)", - e3_id, honest_count, threshold - ); - if let Err(err) = self.bus.publish( - E3Failed { - e3_id: e3_id.clone(), - failed_at_stage: E3Stage::CommitteeFinalized, - reason: FailureReason::InsufficientCommitteeMembers, - }, - ec, - ) { - error!("Failed to publish E3Failed: {err}"); - } - return Ok(()); - } - - info!( - "Updated honest set after C4 verification for E3 {}: {} honest ({} removed)", - e3_id, - honest.len(), - share_decryption_dishonest.len() - ); - } - } else { - info!( - "All parties passed C4 verification for E3 {} — publishing KeyshareCreated", - e3_id - ); - } - - // Exchange #4: Publish KeyshareCreated - self.publish_keyshare_created(ec) - } - /// Publish KeyshareCreated (Exchange #4) with pk_share and signed C1 proof. fn publish_keyshare_created(&mut self, ec: EventContext) -> Result<()> { let state = self.state.try_get()?; @@ -1974,16 +1869,66 @@ impl Handler for ThresholdKeyshare { } EnclaveEventData::DecryptionKeyShared(data) => { if data.external { - info!( - "Received DecryptionKeyShared from party {} for E3 {}", - data.party_id, data.e3_id - ); - match self.ensure_decryption_key_shared_collector(ctx.address()) { - Ok(collector) => collector.do_send(TypedEvent::new(data, ec)), - Err(e) => warn!("Cannot forward DecryptionKeyShared: {}", e), + // Route based on current state + if let Some(state) = self.state.get() { + let result = match &state.state { + KeyshareState::AggregatingDecryptionKey(_) => { + self.handle_early_decryption_key_share(data, ec) + } + KeyshareState::ReadyForDecryption(_) => { + // Delegate to the collector actor + if let Some(ref collector) = self.decryption_key_shared_collector { + collector.do_send(TypedEvent::new(data, ec)); + Ok(()) + } else { + warn!( + "DecryptionKeyShared from party {} in ReadyForDecryption but no collector", + data.party_id + ); + Ok(()) + } + } + other => { + trace!( + "DecryptionKeyShared from party {} in unexpected state {:?}, ignoring", + data.party_id, + other.variant_name() + ); + Ok(()) + } + }; + if let Err(err) = result { + error!("Failed to handle DecryptionKeyShared: {err}"); + } + } + } else { + // Own DecryptionKeyShared published by ProofRequestActor. + // A3 fast-path: if no other honest parties, publish KeyshareCreated directly. + if let Some(state) = self.state.get() { + if data.party_id == state.party_id { + if let KeyshareState::ReadyForDecryption(_) = state.state { + let others = self + .honest_parties + .as_ref() + .map(|h| h.iter().filter(|&&pid| pid != state.party_id).count()) + .unwrap_or(0); + if others == 0 { + info!( + "No other honest parties for E3 {} — publishing KeyshareCreated directly", + data.e3_id + ); + if let Err(err) = self.publish_keyshare_created(ec) { + error!("Failed to publish KeyshareCreated: {err}"); + } + } + } + } } } } + EnclaveEventData::ShareVerificationComplete(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } EnclaveEventData::ComputeResponse(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } @@ -1994,11 +1939,15 @@ impl Handler for ThresholdKeyshare { impl Handler> for ThresholdKeyshare { type Result = (); - fn handle(&mut self, msg: TypedEvent, _: &mut Self::Context) -> Self::Result { + fn handle( + &mut self, + msg: TypedEvent, + ctx: &mut Self::Context, + ) -> Self::Result { trap( EType::KeyGeneration, &self.bus.with_ec(msg.get_ctx()), - || self.handle_compute_response(msg), + || self.handle_compute_response(msg, ctx.address()), ) } } @@ -2033,66 +1982,36 @@ impl Handler> for ThresholdKeyshare { } } -impl Handler> for ThresholdKeyshare { +impl Handler> for ThresholdKeyshare { type Result = (); fn handle( &mut self, - msg: TypedEvent, + msg: TypedEvent, _: &mut Self::Context, ) -> Self::Result { trap( EType::KeyGeneration, &self.bus.with_ec(msg.get_ctx()), - || self.handle_all_threshold_shares_collected(msg), + || self.handle_share_verification_complete(msg), ) } } -impl Handler> for ThresholdKeyshare { +impl Handler> for ThresholdKeyshare { type Result = (); fn handle( &mut self, - msg: TypedEvent, + msg: TypedEvent, _: &mut Self::Context, ) -> Self::Result { trap( EType::KeyGeneration, &self.bus.with_ec(msg.get_ctx()), - || self.handle_all_decryption_key_shares_collected(msg), + || self.handle_all_threshold_shares_collected(msg), ) } } -impl Handler for ThresholdKeyshare { - type Result = (); - fn handle( - &mut self, - msg: DecryptionKeySharedCollectionFailed, - _: &mut Self::Context, - ) -> Self::Result { - trap(EType::KeyGeneration, &self.bus.clone(), || { - warn!( - e3_id = %msg.e3_id, - missing_parties = ?msg.missing_parties, - "DecryptionKeyShared collection failed: {}", - msg.reason - ); - - // Clear the collector reference since it's stopped - self.decryption_key_shared_collector = None; - - if let Err(err) = self.bus.publish_without_context(E3Failed { - e3_id: msg.e3_id.clone(), - failed_at_stage: E3Stage::CommitteeFinalized, - reason: FailureReason::InsufficientCommitteeMembers, - }) { - error!("Failed to publish E3Failed: {err}"); - } - Ok(()) - }) - } -} - impl Handler> for ThresholdKeyshare { type Result = (); fn handle( @@ -2163,12 +2082,64 @@ impl Handler for ThresholdKeyshare { } } +impl Handler> for ThresholdKeyshare { + type Result = (); + fn handle( + &mut self, + msg: TypedEvent, + _: &mut Self::Context, + ) -> Self::Result { + trap( + EType::KeyGeneration, + &self.bus.with_ec(msg.get_ctx()), + || { + let (msg, ec) = msg.into_components(); + self.decryption_key_shared_collector = None; + self.dispatch_c4_verification(msg.shares, ec) + }, + ) + } +} + +impl Handler for ThresholdKeyshare { + type Result = (); + fn handle( + &mut self, + msg: DecryptionKeySharedCollectionFailed, + ctx: &mut Self::Context, + ) -> Self::Result { + trap(EType::KeyGeneration, &self.bus.clone(), || { + warn!( + e3_id = %msg.e3_id, + missing_parties = ?msg.missing_parties, + "DecryptionKeyShared collection failed: {}", + msg.reason + ); + + self.decryption_key_shared_collector = None; + + self.bus.publish_without_context(E3Failed { + e3_id: msg.e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + })?; + + ctx.stop(); + Ok(()) + }) + } +} + impl Handler for ThresholdKeyshare { type Result = (); fn handle(&mut self, _: E3RequestComplete, ctx: &mut Self::Context) -> Self::Result { self.encryption_key_collector = None; self.decryption_key_collector = None; self.decryption_key_shared_collector = None; + self.pending_shares.clear(); + self.pending_c4_proof_data = None; + self.honest_parties = None; + self.early_decryption_key_shares.clear(); self.notify_sync(ctx, Die); } } diff --git a/crates/keyshare/src/threshold_share_collector.rs b/crates/keyshare/src/threshold_share_collector.rs index 31682fc21e..8d097d34c3 100644 --- a/crates/keyshare/src/threshold_share_collector.rs +++ b/crates/keyshare/src/threshold_share_collector.rs @@ -22,7 +22,7 @@ use tracing::{info, warn}; use crate::{AllThresholdSharesCollected, ThresholdKeyshare}; /// Proofs received alongside a threshold share from a sender. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct ReceivedShareProofs { /// Signed C2a proof (sk share computation) from the sender. pub signed_c2a_proof: Option, diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index 9aa284140b..0482b723b2 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -19,14 +19,14 @@ use anyhow::Result; use e3_crypto::Cipher; use e3_events::run_once; use e3_events::trap_fut; + use e3_events::EType; use e3_events::EffectsEnabled; use e3_events::{ BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeRequestKind, ComputeResponse, DkgShareDecryptionProofRequest, DkgShareDecryptionProofResponse, EnclaveEvent, - EnclaveEventData, EventPublisher, EventSubscriber, EventType, - PartyShareDecryptionVerificationResult, PartyVerificationResult, PkBfvProofRequest, - PkBfvProofResponse, PkGenerationProofRequest, PkGenerationProofResponse, + EnclaveEventData, EventPublisher, EventSubscriber, EventType, PartyVerificationResult, + PkBfvProofRequest, PkBfvProofResponse, PkGenerationProofRequest, PkGenerationProofResponse, ShareComputationProofRequest, ShareComputationProofResponse, ShareEncryptionProofRequest, ShareEncryptionProofResponse, TypedEvent, VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, VerifyShareProofsRequest, VerifyShareProofsResponse, @@ -815,38 +815,49 @@ fn handle_verify_share_proofs( ) -> Result { let e3_id_str = request.e3_id.to_string(); + // ECDSA validation (signature recovery, signer consistency, e3_id match) + // is handled by ShareVerificationActor before dispatching to multithread. + // This function performs ZK-only proof verification. let party_results: Vec = req .party_proofs .into_iter() .map(|party| { let sender = party.sender_party_id; + for signed_proof in &party.signed_proofs { + // 1. Validate CircuitName matches expected circuits for this ProofType + let expected_circuits = signed_proof.payload.proof_type.circuit_names(); + if !expected_circuits.contains(&signed_proof.payload.proof.circuit) { + info!( + "Circuit name mismatch for party {} ({:?}): expected {:?}, got {:?}", + sender, + signed_proof.payload.proof_type, + expected_circuits, + signed_proof.payload.proof.circuit + ); + return PartyVerificationResult { + sender_party_id: sender, + all_verified: false, + failed_signed_payload: Some(signed_proof.clone()), + recovered_address: None, + }; + } + + // 2. ZK proof verification let proof = &signed_proof.payload.proof; let result = prover.verify(proof, &e3_id_str, sender); match result { Ok(true) => continue, - Ok(false) => { + Ok(false) | Err(_) => { info!( - "Proof verification failed for party {} ({:?})", + "ZK 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()), + recovered_address: None, }; } } @@ -854,8 +865,8 @@ fn handle_verify_share_proofs( PartyVerificationResult { sender_party_id: sender, all_verified: true, - failed_proof_type: None, failed_signed_payload: None, + recovered_address: None, } }) .collect(); @@ -874,41 +885,73 @@ fn handle_verify_share_decryption_proofs( ) -> Result { let e3_id_str = request.e3_id.to_string(); - let party_results: Vec = req + // ECDSA validation (signature recovery, signer consistency, e3_id match) + // is handled by ShareVerificationActor before dispatching to multithread. + // This function performs ZK-only proof verification. + let party_results: Vec = req .party_proofs .into_iter() .map(|party| { let sender = party.sender_party_id; - // Verify SK decryption proof - let sk_result = prover.verify(&party.sk_decryption_proof, &e3_id_str, sender); - match sk_result { - Ok(true) => {} - Ok(false) | Err(_) => { - return PartyShareDecryptionVerificationResult { + // Guard: an empty esm_decryption_proofs vec would make this loop + // vacuously true. Defence-in-depth: reject any party with zero ESM proofs. + if party.signed_esm_decryption_proofs.is_empty() { + return PartyVerificationResult { + sender_party_id: sender, + all_verified: false, + failed_signed_payload: None, + recovered_address: None, + }; + } + + // Flatten all signed proofs (SK + ESMs) and verify uniformly. + let all_signed: Vec<&e3_events::SignedProofPayload> = + std::iter::once(&party.signed_sk_decryption_proof) + .chain(party.signed_esm_decryption_proofs.iter()) + .collect(); + + for signed_proof in &all_signed { + // 1. Validate CircuitName matches expected circuits for this ProofType + let expected_circuits = signed_proof.payload.proof_type.circuit_names(); + if !expected_circuits.contains(&signed_proof.payload.proof.circuit) { + info!( + "C4 circuit mismatch for party {}: expected {:?}, got {:?}", + sender, expected_circuits, signed_proof.payload.proof.circuit + ); + return PartyVerificationResult { sender_party_id: sender, all_verified: false, + failed_signed_payload: Some((*signed_proof).clone()), + recovered_address: None, }; } - } - // Verify all ESM decryption proofs - for esm_proof in &party.esm_decryption_proofs { - let result = prover.verify(esm_proof, &e3_id_str, sender); + // 2. ZK proof verification + let proof = &signed_proof.payload.proof; + let result = prover.verify(proof, &e3_id_str, sender); match result { Ok(true) => continue, Ok(false) | Err(_) => { - return PartyShareDecryptionVerificationResult { + info!( + "C4 ZK proof verification failed for party {} ({:?})", + sender, signed_proof.payload.proof_type + ); + return PartyVerificationResult { sender_party_id: sender, all_verified: false, + failed_signed_payload: Some((*signed_proof).clone()), + recovered_address: None, }; } } } - PartyShareDecryptionVerificationResult { + PartyVerificationResult { sender_party_id: sender, all_verified: true, + failed_signed_payload: None, + recovered_address: None, } }) .collect(); diff --git a/crates/zk-prover/src/actors/mod.rs b/crates/zk-prover/src/actors/mod.rs index 4a589ac764..e9cf0d71d5 100644 --- a/crates/zk-prover/src/actors/mod.rs +++ b/crates/zk-prover/src/actors/mod.rs @@ -13,6 +13,7 @@ //! ### Core Actors (Business Logic - No IO) //! - [`ProofRequestActor`]: Converts `EncryptionKeyPending` → `ComputeRequest` and handles responses //! - [`ProofVerificationActor`]: Verifies `EncryptionKeyReceived` and converts to `EncryptionKeyCreated` +//! - [`ShareVerificationActor`]: Handles ECDSA + ZK verification for C2/C3/C4 share proofs //! //! ### IO Actors (File System Operations) //! - [`ZkActor`]: Performs actual proof generation/verification using disk-based circuits and bb binary @@ -34,12 +35,14 @@ pub mod proof_request; pub mod proof_verification; +pub mod share_verification; pub mod zk_actor; pub use proof_request::ProofRequestActor; pub use proof_verification::{ ProofVerificationActor, ZkVerificationRequest, ZkVerificationResponse, }; +pub use share_verification::ShareVerificationActor; pub use zk_actor::ZkActor; use actix::{Actor, Addr}; @@ -58,11 +61,13 @@ pub fn setup_zk_actors(bus: &BusHandle, backend: &ZkBackend, signer: PrivateKeyS let proof_request = ProofRequestActor::setup(bus, signer); let proof_verification = ProofVerificationActor::setup(bus, verifier); + let share_verification = ShareVerificationActor::setup(bus); ZkActors { zk_actor, proof_request, proof_verification, + share_verification, } } @@ -71,4 +76,5 @@ pub struct ZkActors { pub zk_actor: Addr, pub proof_request: Addr, pub proof_verification: Addr, + pub share_verification: Addr, } diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index 6475c1fbea..6833e7a49b 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -11,12 +11,14 @@ use actix::{Actor, Addr, Context, Handler}; use alloy::signers::local::PrivateKeySigner; use e3_events::{ BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeResponse, - ComputeResponseKind, CorrelationId, DkgProofSigned, E3id, EnclaveEvent, EnclaveEventData, - EncryptionKey, EncryptionKeyCreated, EncryptionKeyPending, EventContext, EventPublisher, - EventSubscriber, EventType, PkBfvProofRequest, PkGenerationProofSigned, Proof, ProofPayload, - ProofType, Sequenced, SignedProofPayload, ThresholdShare, ThresholdShareCreated, - ThresholdSharePending, TypedEvent, ZkRequest, ZkResponse, + ComputeResponseKind, CorrelationId, DecryptionKeyShared, DecryptionShareProofsPending, + DkgProofSigned, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCreated, + EncryptionKeyPending, EventContext, EventPublisher, EventSubscriber, EventType, + PkBfvProofRequest, PkGenerationProofSigned, Proof, ProofPayload, ProofType, Sequenced, + SignedProofPayload, ThresholdShare, ThresholdShareCreated, ThresholdSharePending, TypedEvent, + ZkRequest, ZkResponse, }; +use e3_utils::utility_types::ArcBytes; use e3_utils::NotifySync; use tracing::{error, info}; @@ -130,6 +132,31 @@ impl PendingThresholdProofs { } } +#[derive(Clone, Debug)] +enum DecryptionProofKind { + SecretKey, + SmudgingNoise { esi_idx: usize }, +} + +/// Pending C4 (DkgShareDecryption) proof generation state. +#[derive(Clone, Debug)] +struct PendingDecryptionProofs { + party_id: u64, + node: String, + sk_poly_sum: ArcBytes, + es_poly_sum: Vec, + ec: EventContext, + sk_proof: Option, + esm_proofs: HashMap, + expected_esm_count: usize, +} + +impl PendingDecryptionProofs { + fn is_complete(&self) -> bool { + self.sk_proof.is_some() && self.esm_proofs.len() == self.expected_esm_count + } +} + /// Core actor that handles encryption key proof requests. /// /// Proofs are always wrapped in a [`SignedProofPayload`] before being published, @@ -141,6 +168,10 @@ pub struct ProofRequestActor { pending: HashMap, threshold_correlation: HashMap, pending_threshold: HashMap, + /// C4 proof staging: correlation → (e3_id, kind) + decryption_correlation: HashMap, + /// C4 pending proofs per E3 + pending_decryption: HashMap, } impl ProofRequestActor { @@ -151,6 +182,8 @@ impl ProofRequestActor { pending: HashMap::new(), pending_threshold: HashMap::new(), threshold_correlation: HashMap::new(), + decryption_correlation: HashMap::new(), + pending_decryption: HashMap::new(), } } @@ -160,6 +193,7 @@ impl ProofRequestActor { bus.subscribe(EventType::ComputeResponse, addr.clone().into()); bus.subscribe(EventType::ComputeRequestError, addr.clone().into()); bus.subscribe(EventType::ThresholdSharePending, addr.clone().into()); + bus.subscribe(EventType::DecryptionShareProofsPending, addr.clone().into()); addr } @@ -347,12 +381,194 @@ impl ProofRequestActor { 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()); + // Try C4 decryption proof first, then fall back to C1/C2/C3 threshold + if self + .decryption_correlation + .contains_key(&msg.correlation_id) + { + self.handle_decryption_proof_response(&msg.correlation_id, resp.proof.clone()); + } else { + self.handle_threshold_proof_response(&msg.correlation_id, resp.proof.clone()); + } } _ => {} } } + /// Handle DecryptionShareProofsPending: dispatch C4 proof generation. + fn handle_decryption_share_proofs_pending( + &mut self, + msg: TypedEvent, + ) { + let (msg, ec) = msg.into_components(); + let e3_id = msg.e3_id.clone(); + let esm_count = msg.esm_requests.len(); + + self.pending_decryption.insert( + e3_id.clone(), + PendingDecryptionProofs { + party_id: msg.party_id, + node: msg.node, + sk_poly_sum: msg.sk_poly_sum, + es_poly_sum: msg.es_poly_sum, + ec: ec.clone(), + sk_proof: None, + esm_proofs: HashMap::new(), + expected_esm_count: esm_count, + }, + ); + + // C4a: SecretKey decryption proof + let sk_corr = CorrelationId::new(); + self.decryption_correlation + .insert(sk_corr, (e3_id.clone(), DecryptionProofKind::SecretKey)); + info!( + "Requesting C4a DkgShareDecryption proof (SecretKey) for E3 {}", + e3_id + ); + if let Err(err) = self.bus.publish( + ComputeRequest::zk( + ZkRequest::DkgShareDecryption(msg.sk_request), + sk_corr, + e3_id.clone(), + ), + ec.clone(), + ) { + error!("Failed to publish C4a proof request: {err}"); + self.decryption_correlation + .retain(|_, (eid, _)| *eid != e3_id); + self.pending_decryption.remove(&e3_id); + return; + } + + // C4b: SmudgingNoise decryption proofs + for (esi_idx, esm_req) in msg.esm_requests.into_iter().enumerate() { + let esm_corr = CorrelationId::new(); + self.decryption_correlation.insert( + esm_corr, + ( + e3_id.clone(), + DecryptionProofKind::SmudgingNoise { esi_idx }, + ), + ); + info!( + "Requesting C4b DkgShareDecryption proof (SmudgingNoise[{}]) for E3 {}", + esi_idx, e3_id + ); + if let Err(err) = self.bus.publish( + ComputeRequest::zk( + ZkRequest::DkgShareDecryption(esm_req), + esm_corr, + e3_id.clone(), + ), + ec.clone(), + ) { + error!("Failed to publish C4b proof request: {err}"); + self.decryption_correlation + .retain(|_, (eid, _)| *eid != e3_id); + self.pending_decryption.remove(&e3_id); + return; + } + } + } + + /// Handle a C4 proof response — store and check completeness. + fn handle_decryption_proof_response(&mut self, correlation_id: &CorrelationId, proof: Proof) { + let Some((e3_id, kind)) = self.decryption_correlation.remove(correlation_id) else { + return; + }; + + let Some(pending) = self.pending_decryption.get_mut(&e3_id) else { + error!( + "No pending decryption proofs for E3 {} — orphan correlation", + e3_id + ); + return; + }; + + match kind { + DecryptionProofKind::SecretKey => { + info!("Received C4a SK decryption proof for E3 {}", e3_id); + pending.sk_proof = Some(proof); + } + DecryptionProofKind::SmudgingNoise { esi_idx } => { + info!( + "Received C4b ESM decryption proof [{}] for E3 {}", + esi_idx, e3_id + ); + pending.esm_proofs.insert(esi_idx, proof); + } + } + + if pending.is_complete() { + info!( + "All C4 proofs complete for E3 {} — signing and publishing DecryptionKeyShared", + e3_id + ); + let pending = self.pending_decryption.remove(&e3_id).unwrap(); + self.sign_and_publish_decryption_key_shared(&e3_id, pending); + } + } + + /// Sign all C4 proofs and publish DecryptionKeyShared (Exchange #3). + fn sign_and_publish_decryption_key_shared( + &mut self, + e3_id: &E3id, + pending: PendingDecryptionProofs, + ) { + // Sign C4a (SK decryption proof) + let Some(signed_sk) = self.sign_proof( + e3_id, + ProofType::T2DkgShareDecryption, + pending.sk_proof.expect("checked in is_complete"), + ) else { + error!("Failed to sign C4a SK proof — DecryptionKeyShared will not be published"); + return; + }; + + // Sign C4b (ESM decryption proofs) in esi_idx order + let mut signed_esms = Vec::with_capacity(pending.expected_esm_count); + for idx in 0..pending.expected_esm_count { + let proof = pending + .esm_proofs + .get(&idx) + .expect("checked in is_complete") + .clone(); + let Some(signed) = self.sign_proof(e3_id, ProofType::T2DkgShareDecryption, proof) + else { + error!( + "Failed to sign C4b ESM proof [{}] — DecryptionKeyShared will not be published", + idx + ); + return; + }; + signed_esms.push(signed); + } + + info!( + "All C4 proofs signed for E3 {} party {} (signer: {})", + e3_id, + pending.party_id, + self.signer.address() + ); + + if let Err(err) = self.bus.publish( + DecryptionKeyShared { + e3_id: e3_id.clone(), + party_id: pending.party_id, + node: pending.node, + sk_poly_sum: pending.sk_poly_sum, + es_poly_sum: pending.es_poly_sum, + signed_sk_decryption_proof: signed_sk, + signed_esm_decryption_proofs: signed_esms, + external: false, + }, + pending.ec, + ) { + error!("Failed to publish DecryptionKeyShared: {err}"); + } + } + fn handle_threshold_proof_response(&mut self, correlation_id: &CorrelationId, proof: Proof) { let Some((e3_id, kind)) = self.threshold_correlation.remove(correlation_id) else { return; @@ -688,6 +904,9 @@ impl Handler for ProofRequestActor { EnclaveEventData::ComputeRequestError(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::DecryptionShareProofsPending(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } _ => (), } } @@ -740,3 +959,15 @@ impl Handler> for ProofRequestActor { self.handle_compute_request_error(msg) } } + +impl Handler> for ProofRequestActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_decryption_share_proofs_pending(msg) + } +} diff --git a/crates/zk-prover/src/actors/proof_verification.rs b/crates/zk-prover/src/actors/proof_verification.rs index c0fe9ae3c4..eb1ded01b7 100644 --- a/crates/zk-prover/src/actors/proof_verification.rs +++ b/crates/zk-prover/src/actors/proof_verification.rs @@ -134,6 +134,16 @@ impl ProofVerificationActor { } }; + // Validate circuit name matches expected ProofType circuits + let expected_circuits = signed.payload.proof_type.circuit_names(); + if !expected_circuits.contains(&signed.payload.proof.circuit) { + error!( + "Circuit name mismatch for key from party {}: expected {:?}, got {:?}", + msg.key.party_id, expected_circuits, signed.payload.proof.circuit + ); + return; + } + // Store the signed payload so we can reference it in the verification response self.pending.insert( (msg.e3_id.clone(), msg.key.party_id), diff --git a/crates/zk-prover/src/actors/share_verification.rs b/crates/zk-prover/src/actors/share_verification.rs new file mode 100644 index 0000000000..e65246ae5e --- /dev/null +++ b/crates/zk-prover/src/actors/share_verification.rs @@ -0,0 +1,502 @@ +// 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. + +//! Actor for C2/C3/C4 share proof verification. +//! +//! Follows the same pattern as [`ProofVerificationActor`] (for C0/T0) — sits +//! between the raw proof data and the verified result, handling ECDSA validation +//! and ZK verification orchestration. +//! +//! ## Flow +//! +//! 1. Receives [`ShareVerificationDispatched`] from [`ThresholdKeyshare`]. +//! 2. Performs ECDSA validation (signature recovery, signer consistency, e3_id, +//! circuit name) — lightweight, no thread pool needed. +//! 3. Dispatches ZK-only verification to multithread via [`ComputeRequest`]. +//! 4. Receives [`ComputeResponse`] from multithread with pure ZK results. +//! 5. Combines ECDSA + ZK results. +//! 6. Emits [`SignedProofFailed`] for any failing proofs. +//! 7. Publishes [`ShareVerificationComplete`] with dishonest party set. + +use std::collections::{BTreeSet, HashMap, HashSet}; + +use actix::{Actor, Addr, Context, Handler}; +use alloy::primitives::Address; +use e3_events::{ + BusHandle, ComputeRequest, ComputeResponse, ComputeResponseKind, CorrelationId, E3id, + EnclaveEvent, EnclaveEventData, EventContext, EventPublisher, EventSubscriber, EventType, + PartyProofsToVerify, PartyShareDecryptionProofsToVerify, PartyVerificationResult, Sequenced, + ShareVerificationComplete, ShareVerificationDispatched, SignedProofFailed, SignedProofPayload, + TypedEvent, VerificationKind, VerifyShareDecryptionProofsRequest, + VerifyShareDecryptionProofsResponse, VerifyShareProofsRequest, VerifyShareProofsResponse, + ZkRequest, ZkResponse, +}; +use e3_utils::NotifySync; +use tracing::{error, info, warn}; + +/// ECDSA validation result for a single party. +struct EcdsaPartyResult { + sender_party_id: u64, + passed: bool, + /// The pair (signed_payload, recovered_address) of the first failing proof, if any. + failed_payload: Option<(SignedProofPayload, Option
)>, +} + +/// Pending verification state — stored while ZK verification is in flight. +struct PendingVerification { + e3_id: E3id, + kind: VerificationKind, + ec: EventContext, + /// Parties that failed ECDSA (dishonest before ZK runs). + ecdsa_dishonest: HashSet, + /// Pre-dishonest parties from the dispatch (missing/incomplete proofs). + pre_dishonest: BTreeSet, + /// Signed payloads for each party, indexed by party_id. + /// Used for SignedProofFailed emission when ZK also fails. + party_signed_payloads: HashMap>, + /// Recovered address for each party (from ECDSA step). + party_addresses: HashMap, +} + +/// Actor that handles C2/C3/C4 share proof verification. +/// +/// Separates ECDSA validation (lightweight, done inline) from ZK proof +/// verification (heavyweight, delegated to multithread). Emits +/// [`SignedProofFailed`] for fault attribution and [`ShareVerificationComplete`] +/// with the final dishonest party set. +pub struct ShareVerificationActor { + bus: BusHandle, + /// Tracks pending verifications by correlation ID. + pending: HashMap, +} + +impl ShareVerificationActor { + pub fn new(bus: &BusHandle) -> Self { + Self { + bus: bus.clone(), + pending: HashMap::new(), + } + } + + pub fn setup(bus: &BusHandle) -> Addr { + let addr = Self::new(bus).start(); + bus.subscribe(EventType::ShareVerificationDispatched, addr.clone().into()); + bus.subscribe(EventType::ComputeResponse, addr.clone().into()); + addr + } + + fn handle_share_verification_dispatched( + &mut self, + msg: TypedEvent, + ) { + let (msg, ec) = msg.into_components(); + let e3_id = msg.e3_id.clone(); + + match msg.kind { + VerificationKind::ShareProofs => { + self.verify_share_proofs(e3_id, msg.share_proofs, msg.pre_dishonest, ec); + } + VerificationKind::DecryptionProofs => { + self.verify_decryption_proofs(e3_id, msg.decryption_proofs, msg.pre_dishonest, ec); + } + } + } + + /// C2/C3 verification: ECDSA check on each party, then dispatch ZK. + fn verify_share_proofs( + &mut self, + e3_id: E3id, + party_proofs: Vec, + pre_dishonest: BTreeSet, + ec: EventContext, + ) { + let e3_id_str = e3_id.to_string(); + let mut ecdsa_dishonest = HashSet::new(); + let mut ecdsa_passed_parties = Vec::new(); + let mut party_signed_payloads: HashMap> = HashMap::new(); + let mut party_addresses: HashMap = HashMap::new(); + + for party in &party_proofs { + let result = self.ecdsa_validate_signed_proofs( + party.sender_party_id, + &party.signed_proofs, + &e3_id_str, + "C2/C3", + ); + party_signed_payloads.insert(party.sender_party_id, party.signed_proofs.clone()); + if result.passed { + ecdsa_passed_parties.push(party.clone()); + if let Some((_, Some(addr))) = &result.failed_payload { + party_addresses.insert(party.sender_party_id, *addr); + } + } else { + ecdsa_dishonest.insert(party.sender_party_id); + // Emit SignedProofFailed for ECDSA failure + if let Some((ref signed, addr)) = result.failed_payload { + self.emit_signed_proof_failed(&e3_id, signed, addr, &ec); + } + } + } + + // Store recovered addresses for passed parties + for party in &party_proofs { + if !ecdsa_dishonest.contains(&party.sender_party_id) { + if let Some(first_signed) = party.signed_proofs.first() { + if let Ok(addr) = first_signed.recover_address() { + party_addresses.insert(party.sender_party_id, addr); + } + } + } + } + + if ecdsa_passed_parties.is_empty() { + // All parties failed ECDSA — publish result immediately + let mut all_dishonest: BTreeSet = pre_dishonest; + all_dishonest.extend(ecdsa_dishonest); + self.publish_complete(e3_id, VerificationKind::ShareProofs, all_dishonest, ec); + return; + } + + // Dispatch ZK-only verification to multithread + let correlation_id = CorrelationId::new(); + self.pending.insert( + correlation_id, + PendingVerification { + e3_id: e3_id.clone(), + kind: VerificationKind::ShareProofs, + ec: ec.clone(), + ecdsa_dishonest, + pre_dishonest, + party_signed_payloads, + party_addresses, + }, + ); + + let request = ComputeRequest::zk( + ZkRequest::VerifyShareProofs(VerifyShareProofsRequest { + party_proofs: ecdsa_passed_parties, + }), + correlation_id, + e3_id, + ); + + if let Err(err) = self.bus.publish(request, ec) { + error!("Failed to dispatch ZK verification: {err}"); + self.pending.remove(&correlation_id); + } + } + + /// C4 verification: ECDSA check on each party, then dispatch ZK. + fn verify_decryption_proofs( + &mut self, + e3_id: E3id, + party_proofs: Vec, + pre_dishonest: BTreeSet, + ec: EventContext, + ) { + let e3_id_str = e3_id.to_string(); + let mut ecdsa_dishonest = HashSet::new(); + let mut ecdsa_passed_parties = Vec::new(); + let mut party_signed_payloads: HashMap> = HashMap::new(); + let mut party_addresses: HashMap = HashMap::new(); + + for party in &party_proofs { + // Flatten all signed proofs (SK + ESMs) + let all_signed: Vec<&SignedProofPayload> = + std::iter::once(&party.signed_sk_decryption_proof) + .chain(party.signed_esm_decryption_proofs.iter()) + .collect(); + let all_signed_cloned: Vec = + all_signed.iter().map(|s| (*s).clone()).collect(); + + let result = self.ecdsa_validate_signed_proofs( + party.sender_party_id, + &all_signed_cloned, + &e3_id_str, + "C4", + ); + party_signed_payloads.insert(party.sender_party_id, all_signed_cloned); + + if result.passed { + ecdsa_passed_parties.push(party.clone()); + if let Some((_, Some(addr))) = &result.failed_payload { + party_addresses.insert(party.sender_party_id, *addr); + } + } else { + ecdsa_dishonest.insert(party.sender_party_id); + if let Some((ref signed, addr)) = result.failed_payload { + self.emit_signed_proof_failed(&e3_id, signed, addr, &ec); + } + } + } + + // Store recovered addresses for passed parties + for party in &party_proofs { + if !ecdsa_dishonest.contains(&party.sender_party_id) { + if let Ok(addr) = party.signed_sk_decryption_proof.recover_address() { + party_addresses.insert(party.sender_party_id, addr); + } + } + } + + if ecdsa_passed_parties.is_empty() { + let mut all_dishonest: BTreeSet = pre_dishonest; + all_dishonest.extend(ecdsa_dishonest); + self.publish_complete(e3_id, VerificationKind::DecryptionProofs, all_dishonest, ec); + return; + } + + let correlation_id = CorrelationId::new(); + self.pending.insert( + correlation_id, + PendingVerification { + e3_id: e3_id.clone(), + kind: VerificationKind::DecryptionProofs, + ec: ec.clone(), + ecdsa_dishonest, + pre_dishonest, + party_signed_payloads, + party_addresses, + }, + ); + + let request = ComputeRequest::zk( + ZkRequest::VerifyShareDecryptionProofs(VerifyShareDecryptionProofsRequest { + party_proofs: ecdsa_passed_parties, + }), + correlation_id, + e3_id, + ); + + if let Err(err) = self.bus.publish(request, ec) { + error!("Failed to dispatch C4 ZK verification: {err}"); + self.pending.remove(&correlation_id); + } + } + + /// Validate ECDSA properties for a set of signed proofs from one party: + /// 1. e3_id match + /// 2. Signature recovery (valid ECDSA) + /// 3. Signer consistency (all proofs from same address) + /// 4. Circuit name matches expected ProofType circuits + fn ecdsa_validate_signed_proofs( + &self, + sender_party_id: u64, + signed_proofs: &[SignedProofPayload], + e3_id_str: &str, + label: &str, + ) -> EcdsaPartyResult { + let mut expected_addr: Option
= None; + + for signed in signed_proofs { + // 1. e3_id match + if signed.payload.e3_id.to_string() != e3_id_str { + info!( + "{} proof from party {} has wrong e3_id ({} vs {})", + label, sender_party_id, signed.payload.e3_id, e3_id_str + ); + return EcdsaPartyResult { + sender_party_id, + passed: false, + failed_payload: Some((signed.clone(), expected_addr)), + }; + } + + // 2. Signature recovery + match signed.recover_address() { + Ok(addr) => { + // 3. Signer consistency + match &expected_addr { + Some(ea) if *ea != addr => { + info!( + "{} inconsistent signer for party {}", + label, sender_party_id + ); + return EcdsaPartyResult { + sender_party_id, + passed: false, + failed_payload: Some((signed.clone(), Some(addr))), + }; + } + None => expected_addr = Some(addr), + _ => {} + } + } + Err(e) => { + info!( + "{} signature recovery failed for party {} ({:?}): {}", + label, sender_party_id, signed.payload.proof_type, e + ); + return EcdsaPartyResult { + sender_party_id, + passed: false, + failed_payload: Some((signed.clone(), expected_addr)), + }; + } + } + + // 4. Circuit name validation + let expected_circuits = signed.payload.proof_type.circuit_names(); + if !expected_circuits.contains(&signed.payload.proof.circuit) { + info!( + "{} circuit mismatch for party {}: expected {:?}, got {:?}", + label, sender_party_id, expected_circuits, signed.payload.proof.circuit + ); + return EcdsaPartyResult { + sender_party_id, + passed: false, + failed_payload: Some((signed.clone(), expected_addr)), + }; + } + } + + EcdsaPartyResult { + sender_party_id, + passed: true, + failed_payload: None, + } + } + + /// Handle ZK verification response from multithread. + fn handle_compute_response(&mut self, msg: TypedEvent) { + let (msg, _ec) = msg.into_components(); + + let correlation_id = msg.correlation_id; + let Some(pending) = self.pending.remove(&correlation_id) else { + return; // Not our correlation ID + }; + + let zk_results: Vec = match (&pending.kind, msg.response) { + ( + VerificationKind::ShareProofs, + ComputeResponseKind::Zk(ZkResponse::VerifyShareProofs(r)), + ) => r.party_results, + ( + VerificationKind::DecryptionProofs, + ComputeResponseKind::Zk(ZkResponse::VerifyShareDecryptionProofs(r)), + ) => r.party_results, + _ => { + error!("Unexpected ComputeResponse kind for verification"); + return; + } + }; + + let mut all_dishonest: BTreeSet = pending.pre_dishonest; + all_dishonest.extend(&pending.ecdsa_dishonest); + + for result in &zk_results { + if !result.all_verified { + all_dishonest.insert(result.sender_party_id); + + // Emit SignedProofFailed for ZK failure + if let Some(ref signed) = result.failed_signed_payload { + let addr = pending + .party_addresses + .get(&result.sender_party_id) + .copied(); + self.emit_signed_proof_failed(&pending.e3_id, signed, addr, &pending.ec); + } + } + } + + self.publish_complete(pending.e3_id, pending.kind, all_dishonest, pending.ec); + } + + fn emit_signed_proof_failed( + &self, + e3_id: &E3id, + signed_payload: &SignedProofPayload, + recovered_addr: Option
, + ec: &EventContext, + ) { + let faulting_node = match recovered_addr { + Some(addr) => addr, + None => match signed_payload.recover_address() { + Ok(addr) => addr, + Err(err) => { + warn!("Cannot attribute fault — signature recovery failed: {err}"); + return; + } + }, + }; + + if let Err(err) = self.bus.publish( + SignedProofFailed { + e3_id: e3_id.clone(), + faulting_node, + proof_type: signed_payload.payload.proof_type, + signed_payload: signed_payload.clone(), + }, + ec.clone(), + ) { + error!("Failed to publish SignedProofFailed: {err}"); + } + } + + fn publish_complete( + &self, + e3_id: E3id, + kind: VerificationKind, + dishonest_parties: BTreeSet, + ec: EventContext, + ) { + if let Err(err) = self.bus.publish( + ShareVerificationComplete { + e3_id, + kind, + dishonest_parties, + }, + ec, + ) { + error!("Failed to publish ShareVerificationComplete: {err}"); + } + } +} + +impl Actor for ShareVerificationActor { + type Context = Context; +} + +impl Handler for ShareVerificationActor { + type Result = (); + + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + let (msg, ec) = msg.into_components(); + match msg { + EnclaveEventData::ShareVerificationDispatched(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::ComputeResponse(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + _ => (), + } + } +} + +impl Handler> for ShareVerificationActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_share_verification_dispatched(msg) + } +} + +impl Handler> for ShareVerificationActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_compute_response(msg) + } +} diff --git a/crates/zk-prover/src/lib.rs b/crates/zk-prover/src/lib.rs index 3958210e2b..4d03e13923 100644 --- a/crates/zk-prover/src/lib.rs +++ b/crates/zk-prover/src/lib.rs @@ -15,8 +15,8 @@ mod traits; mod witness; pub use actors::{ - setup_zk_actors, ProofRequestActor, ProofVerificationActor, ZkActors, ZkVerificationRequest, - ZkVerificationResponse, + setup_zk_actors, ProofRequestActor, ProofVerificationActor, ShareVerificationActor, ZkActors, + ZkVerificationRequest, ZkVerificationResponse, }; pub use backend::{SetupStatus, ZkBackend}; From 51396a5b36b3d7c6be7b7c9955610a430e172601 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sat, 28 Feb 2026 13:41:08 +0500 Subject: [PATCH 04/11] fix: resolve conflicts --- .../src/enclave_event/compute_request/zk.rs | 36 --------------- crates/keyshare/src/threshold_keyshare.rs | 45 ------------------- crates/multithread/src/multithread.rs | 5 +-- 3 files changed, 2 insertions(+), 84 deletions(-) diff --git a/crates/events/src/enclave_event/compute_request/zk.rs b/crates/events/src/enclave_event/compute_request/zk.rs index c371e696c6..bfe7be131b 100644 --- a/crates/events/src/enclave_event/compute_request/zk.rs +++ b/crates/events/src/enclave_event/compute_request/zk.rs @@ -313,42 +313,6 @@ pub struct VerifyShareDecryptionProofsResponse { pub party_results: Vec, } -/// Request to batch-verify C4 proofs from DecryptionKeyShared events. -/// -/// Grouped by sender so the verifier can report honest/dishonest per party. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct VerifyShareDecryptionProofsRequest { - /// C4 proofs grouped by sender party_id. - pub party_proofs: Vec, -} - -/// C4 proofs from a single sender to verify. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct PartyShareDecryptionProofsToVerify { - /// The party that generated these proofs. - pub sender_party_id: u64, - /// C4a proof (SecretKey decryption). - pub sk_decryption_proof: Proof, - /// C4b proofs (SmudgingNoise decryption), one per smudging noise index. - pub esm_decryption_proofs: Vec, -} - -/// Batch verification results for C4 proofs. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct VerifyShareDecryptionProofsResponse { - /// Per-party verification results. - pub party_results: Vec, -} - -/// Verification result for C4 proofs from a single sender. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct PartyShareDecryptionVerificationResult { - /// The party whose C4 proofs were verified. - pub sender_party_id: u64, - /// Whether ALL C4 proofs from this party verified successfully. - pub all_verified: bool, -} - /// ZK-specific error variants. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ZkError { diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 98ef127540..9f342a5496 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -2012,51 +2012,6 @@ impl Handler> for ThresholdKeyshare { } } -impl Handler> for ThresholdKeyshare { - type Result = (); - fn handle( - &mut self, - msg: TypedEvent, - _: &mut Self::Context, - ) -> Self::Result { - trap( - EType::KeyGeneration, - &self.bus.with_ec(msg.get_ctx()), - || self.handle_all_decryption_key_shares_collected(msg), - ) - } -} - -impl Handler for ThresholdKeyshare { - type Result = (); - fn handle( - &mut self, - msg: DecryptionKeySharedCollectionFailed, - _: &mut Self::Context, - ) -> Self::Result { - trap(EType::KeyGeneration, &self.bus.clone(), || { - warn!( - e3_id = %msg.e3_id, - missing_parties = ?msg.missing_parties, - "DecryptionKeyShared collection failed: {}", - msg.reason - ); - - // Clear the collector reference since it's stopped - self.decryption_key_shared_collector = None; - - if let Err(err) = self.bus.publish_without_context(E3Failed { - e3_id: msg.e3_id.clone(), - failed_at_stage: E3Stage::CommitteeFinalized, - reason: FailureReason::InsufficientCommitteeMembers, - }) { - error!("Failed to publish E3Failed: {err}"); - } - Ok(()) - }) - } -} - impl Handler> for ThresholdKeyshare { type Result = (); fn handle( diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index b26ffae392..0482b723b2 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -25,9 +25,8 @@ use e3_events::EffectsEnabled; use e3_events::{ BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeRequestKind, ComputeResponse, DkgShareDecryptionProofRequest, DkgShareDecryptionProofResponse, EnclaveEvent, - EnclaveEventData, EventPublisher, EventSubscriber, EventType, - PartyShareDecryptionVerificationResult, PartyVerificationResult, PkBfvProofRequest, - PkBfvProofResponse, PkGenerationProofRequest, PkGenerationProofResponse, + EnclaveEventData, EventPublisher, EventSubscriber, EventType, PartyVerificationResult, + PkBfvProofRequest, PkBfvProofResponse, PkGenerationProofRequest, PkGenerationProofResponse, ShareComputationProofRequest, ShareComputationProofResponse, ShareEncryptionProofRequest, ShareEncryptionProofResponse, TypedEvent, VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, VerifyShareProofsRequest, VerifyShareProofsResponse, From 6e599157d5ed7d0aec87e99d9e26439cfd0514f1 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sat, 28 Feb 2026 13:48:21 +0500 Subject: [PATCH 05/11] chore: remove unused imports --- crates/ciphernode-builder/src/eventbus_factory.rs | 3 --- crates/ciphernode-builder/src/evm_system.rs | 7 ++----- crates/cli/src/ciphernode/setup.rs | 3 +-- crates/cli/src/start.rs | 2 +- crates/entrypoint/src/start/aggregator_start.rs | 1 - crates/events/src/eventstore_router.rs | 2 +- crates/events/src/snapshot_buffer/batch.rs | 1 - crates/events/src/snapshot_buffer/batch_router.rs | 2 +- crates/events/src/snapshot_buffer/snapshot_buffer.rs | 2 +- crates/evm/src/evm_parser.rs | 2 +- crates/evm/src/evm_read_interface.rs | 6 +++--- crates/evm/src/fix_historical_order.rs | 2 +- crates/indexer/tests/integration.rs | 2 +- crates/sync/src/sync.rs | 3 +-- crates/zk-prover/src/actors/share_verification.rs | 3 +-- crates/zk-prover/tests/common/mod.rs | 1 - 16 files changed, 15 insertions(+), 27 deletions(-) diff --git a/crates/ciphernode-builder/src/eventbus_factory.rs b/crates/ciphernode-builder/src/eventbus_factory.rs index 268678ea2a..8842010150 100644 --- a/crates/ciphernode-builder/src/eventbus_factory.rs +++ b/crates/ciphernode-builder/src/eventbus_factory.rs @@ -6,11 +6,8 @@ use actix::Actor; use actix::Addr; -use e3_config::AppConfig; -use e3_data::Repositories; use e3_events::Disabled; use e3_events::EventType; -use e3_evm::EthPrivateKeyRepositoryFactory; use once_cell::sync::Lazy; use std::any::Any; use std::any::TypeId; diff --git a/crates/ciphernode-builder/src/evm_system.rs b/crates/ciphernode-builder/src/evm_system.rs index 5d88eb4c90..0609b7b15f 100644 --- a/crates/ciphernode-builder/src/evm_system.rs +++ b/crates/ciphernode-builder/src/evm_system.rs @@ -8,14 +8,11 @@ use std::mem::replace; use actix::Actor; use alloy::{primitives::Address, providers::Provider}; -use e3_events::{ - run_once, BusHandle, EventExtractor, EventSubscriber, EventType, HistoricalEvmSyncStart, -}; +use e3_events::{run_once, BusHandle, EventSubscriber, EventType, HistoricalEvmSyncStart}; use e3_evm::{ EthProvider, EvmChainGateway, EvmEventProcessor, EvmReadInterface, EvmRouter, Filters, - FixHistoricalOrder, SyncStartExtractor, + FixHistoricalOrder, }; -use e3_utils::actix::oneshot_runner::OneShotRunner; pub trait RouteFn: FnOnce(EvmEventProcessor) -> EvmEventProcessor + Send {} impl RouteFn for F where F: FnOnce(EvmEventProcessor) -> EvmEventProcessor + Send {} diff --git a/crates/cli/src/ciphernode/setup.rs b/crates/cli/src/ciphernode/setup.rs index f351ac1cac..e95a910807 100644 --- a/crates/cli/src/ciphernode/setup.rs +++ b/crates/cli/src/ciphernode/setup.rs @@ -9,12 +9,11 @@ use anyhow::Result; use dialoguer::{theme::ColorfulTheme, Input}; use e3_config::AppConfig; use e3_entrypoint::config::setup; -use e3_utils::{colorize, eth_address_from_private_key, Color}; +use e3_utils::{colorize, Color}; use std::path::PathBuf; use tracing::instrument; use zeroize::Zeroizing; -use crate::password_set; use crate::password_set::ask_for_password; use crate::wallet_set::ask_for_private_key; diff --git a/crates/cli/src/start.rs b/crates/cli/src/start.rs index 2eada86c1e..94d3e59410 100644 --- a/crates/cli/src/start.rs +++ b/crates/cli/src/start.rs @@ -5,7 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use crate::owo; -use anyhow::{anyhow, Result}; +use anyhow::Result; use e3_config::{AppConfig, NodeRole}; use e3_entrypoint::helpers::listen_for_shutdown; use tracing::{info, instrument}; diff --git a/crates/entrypoint/src/start/aggregator_start.rs b/crates/entrypoint/src/start/aggregator_start.rs index 1ad2275633..912c396b40 100644 --- a/crates/entrypoint/src/start/aggregator_start.rs +++ b/crates/entrypoint/src/start/aggregator_start.rs @@ -4,7 +4,6 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use alloy::primitives::Address; use anyhow::Result; use e3_ciphernode_builder::{CiphernodeBuilder, CiphernodeHandle}; use e3_config::AppConfig; diff --git a/crates/events/src/eventstore_router.rs b/crates/events/src/eventstore_router.rs index 3f3da61f3b..3679957b50 100644 --- a/crates/events/src/eventstore_router.rs +++ b/crates/events/src/eventstore_router.rs @@ -78,7 +78,7 @@ impl Handler for QueryAggregator { impl Handler for QueryAggregator { type Result = (); - fn handle(&mut self, msg: Die, ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, _msg: Die, ctx: &mut Self::Context) -> Self::Result { ctx.stop() } } diff --git a/crates/events/src/snapshot_buffer/batch.rs b/crates/events/src/snapshot_buffer/batch.rs index acbb517557..722cb069c5 100644 --- a/crates/events/src/snapshot_buffer/batch.rs +++ b/crates/events/src/snapshot_buffer/batch.rs @@ -7,7 +7,6 @@ use std::mem::replace; use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Message, Recipient}; use e3_utils::MAILBOX_LIMIT; -use tracing::debug; use crate::{trap, Die, EType, Insert, InsertBatch, PanicDispatcher}; diff --git a/crates/events/src/snapshot_buffer/batch_router.rs b/crates/events/src/snapshot_buffer/batch_router.rs index 71b88f3147..a7882a25da 100644 --- a/crates/events/src/snapshot_buffer/batch_router.rs +++ b/crates/events/src/snapshot_buffer/batch_router.rs @@ -16,7 +16,7 @@ use actix::{Actor, Addr, Handler, Message, Recipient}; use anyhow::Context; use e3_utils::MAILBOX_LIMIT; use std::{collections::HashMap, sync::Arc, time::Duration}; -use tracing::{debug, info, trace, warn}; +use tracing::debug; type Seq = u64; diff --git a/crates/events/src/snapshot_buffer/snapshot_buffer.rs b/crates/events/src/snapshot_buffer/snapshot_buffer.rs index bf73313580..b28ddb14ea 100644 --- a/crates/events/src/snapshot_buffer/snapshot_buffer.rs +++ b/crates/events/src/snapshot_buffer/snapshot_buffer.rs @@ -140,7 +140,7 @@ impl Handler for SnapshotBuffer { impl Handler for SnapshotBuffer { type Result = (); - fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: EnclaveEvent, _ctx: &mut Self::Context) -> Self::Result { trap(EType::IO, &PanicDispatcher::new(), || { if let Some(ref router) = self.router { router.try_send(msg)?; diff --git a/crates/evm/src/evm_parser.rs b/crates/evm/src/evm_parser.rs index e4549fd8ff..473e67629c 100644 --- a/crates/evm/src/evm_parser.rs +++ b/crates/evm/src/evm_parser.rs @@ -7,7 +7,7 @@ use actix::{Actor, Handler}; use e3_events::{hlc::HlcTimestamp, EnclaveEventData}; use e3_utils::MAILBOX_LIMIT; -use tracing::{debug, info}; +use tracing::debug; use crate::{ events::{EnclaveEvmEvent, EvmEventProcessor, EvmLog}, diff --git a/crates/evm/src/evm_read_interface.rs b/crates/evm/src/evm_read_interface.rs index ef832fd4c9..44093726b7 100644 --- a/crates/evm/src/evm_read_interface.rs +++ b/crates/evm/src/evm_read_interface.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::events::{EnclaveEvmEvent, EvmEventProcessor, EvmLog}; +use crate::events::{EnclaveEvmEvent, EvmEventProcessor}; use crate::helpers::EthProvider; use crate::log_fetcher::{backfill_to_head, fetch_logs_chunked, process_log, TimestampTracker}; use crate::HistoricalSyncComplete; @@ -16,14 +16,14 @@ use alloy::providers::Provider; use alloy::rpc::types::Filter; use alloy_primitives::Address; use anyhow::anyhow; -use e3_events::{BusHandle, CorrelationId, ErrorDispatcher, Event, EventSubscriber, EventType}; +use e3_events::{BusHandle, ErrorDispatcher, Event, EventSubscriber, EventType}; use e3_events::{EType, EnclaveEvent, EnclaveEventData, EventId}; use e3_utils::MAILBOX_LIMIT; use futures_util::stream::StreamExt; use std::collections::{HashMap, HashSet}; use tokio::select; use tokio::sync::oneshot; -use tracing::{debug, error, info, instrument, warn}; +use tracing::{error, info, instrument, warn}; const MAX_RECONNECT_DELAY_SECS: u64 = 60; diff --git a/crates/evm/src/fix_historical_order.rs b/crates/evm/src/fix_historical_order.rs index 1ff824290f..ec4746fb7b 100644 --- a/crates/evm/src/fix_historical_order.rs +++ b/crates/evm/src/fix_historical_order.rs @@ -9,7 +9,7 @@ use actix::{Actor, Addr, Handler}; use bloom::{BloomFilter, ASMS}; use e3_events::CorrelationId; use e3_utils::MAILBOX_LIMIT; -use tracing::{debug, info}; +use tracing::debug; pub struct FixHistoricalOrder { dest: EvmEventProcessor, diff --git a/crates/indexer/tests/integration.rs b/crates/indexer/tests/integration.rs index dcb0641d25..52f553ad21 100644 --- a/crates/indexer/tests/integration.rs +++ b/crates/indexer/tests/integration.rs @@ -6,7 +6,7 @@ mod helpers; use alloy::{ - primitives::{Bytes, FixedBytes, Uint}, + primitives::{Bytes, Uint}, sol, }; use e3_bfv_client::compute_pk_commitment; diff --git a/crates/sync/src/sync.rs b/crates/sync/src/sync.rs index 19d97b3dfc..adadeda54c 100644 --- a/crates/sync/src/sync.rs +++ b/crates/sync/src/sync.rs @@ -12,8 +12,7 @@ use e3_events::{ AggregateConfig, AggregateId, BusHandle, CorrelationId, EffectsEnabled, EnclaveEvent, EnclaveEventData, Event, EventContextAccessors, EventPublisher, EventStoreQueryBy, EventStoreQueryResponse, EvmEventConfig, EvmEventConfigChain, HistoricalEvmEventsReceived, - HistoricalEvmSyncStart, HistoricalNetEventsReceived, HistoricalNetSyncStart, SeqAgg, SyncEnded, - Unsequenced, + HistoricalEvmSyncStart, SeqAgg, SyncEnded, Unsequenced, }; use e3_utils::actix::channel as actix_toolbox; use std::{ diff --git a/crates/zk-prover/src/actors/share_verification.rs b/crates/zk-prover/src/actors/share_verification.rs index e65246ae5e..69906ac347 100644 --- a/crates/zk-prover/src/actors/share_verification.rs +++ b/crates/zk-prover/src/actors/share_verification.rs @@ -30,8 +30,7 @@ use e3_events::{ EnclaveEvent, EnclaveEventData, EventContext, EventPublisher, EventSubscriber, EventType, PartyProofsToVerify, PartyShareDecryptionProofsToVerify, PartyVerificationResult, Sequenced, ShareVerificationComplete, ShareVerificationDispatched, SignedProofFailed, SignedProofPayload, - TypedEvent, VerificationKind, VerifyShareDecryptionProofsRequest, - VerifyShareDecryptionProofsResponse, VerifyShareProofsRequest, VerifyShareProofsResponse, + TypedEvent, VerificationKind, VerifyShareDecryptionProofsRequest, VerifyShareProofsRequest, ZkRequest, ZkResponse, }; use e3_utils::NotifySync; diff --git a/crates/zk-prover/tests/common/mod.rs b/crates/zk-prover/tests/common/mod.rs index 077e55df08..4393676cb1 100644 --- a/crates/zk-prover/tests/common/mod.rs +++ b/crates/zk-prover/tests/common/mod.rs @@ -5,7 +5,6 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pub mod helpers; -pub use helpers::*; use std::path::PathBuf; From f98949bca1ee08282ca962e81c4ce6820b4aa963 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sat, 28 Feb 2026 13:51:50 +0500 Subject: [PATCH 06/11] chore: add helper import --- crates/zk-prover/tests/common/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/zk-prover/tests/common/mod.rs b/crates/zk-prover/tests/common/mod.rs index 4393676cb1..a16725b4f2 100644 --- a/crates/zk-prover/tests/common/mod.rs +++ b/crates/zk-prover/tests/common/mod.rs @@ -4,7 +4,8 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -pub mod helpers; +mod helpers; +pub use helpers::*; use std::path::PathBuf; From 8ac623f3d27bbf78978b237e9a08dd3e811bed3a Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sat, 28 Feb 2026 14:29:02 +0500 Subject: [PATCH 07/11] fix: review comments --- crates/events/src/enclave_event/proof.rs | 2 +- .../src/decryption_key_shared_collector.rs | 1 + crates/keyshare/src/threshold_keyshare.rs | 195 ++++++++++++++---- crates/zk-prover/src/actors/proof_request.rs | 10 + .../src/actors/share_verification.rs | 50 ++++- 5 files changed, 208 insertions(+), 50 deletions(-) diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index 9541f17f7c..f10abfd10d 100644 --- a/crates/events/src/enclave_event/proof.rs +++ b/crates/events/src/enclave_event/proof.rs @@ -96,6 +96,6 @@ impl CircuitName { impl fmt::Display for CircuitName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_str()) + write!(f, "{}", self.dir_path()) } } diff --git a/crates/keyshare/src/decryption_key_shared_collector.rs b/crates/keyshare/src/decryption_key_shared_collector.rs index 9ef48dfcc8..e6fc4872b3 100644 --- a/crates/keyshare/src/decryption_key_shared_collector.rs +++ b/crates/keyshare/src/decryption_key_shared_collector.rs @@ -141,6 +141,7 @@ impl Handler> for DecryptionKeySharedCollector { ec, ); self.parent.do_send(event); + ctx.stop(); } info!( diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 9f342a5496..97fdce8eb3 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -1198,6 +1198,27 @@ impl ThresholdKeyshare { if party_proofs_to_verify.is_empty() { // All non-self parties are dishonest (missing or incomplete proofs), none to verify + let threshold = state.threshold_m; + let total = state.threshold_n; + let honest_count = total - pre_dishonest.len() as u64; + + if honest_count < threshold { + warn!( + "Too few honest parties for E3 {} ({} honest < {} threshold) after C2/C3 pre-dishonest filtering — cannot proceed", + e3_id, honest_count, threshold + ); + self.pending_shares.clear(); + self.bus.publish( + E3Failed { + e3_id: e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }, + ec, + )?; + return Ok(()); + } + let dishonest_set: HashSet = pre_dishonest.into_iter().collect(); return self.proceed_with_decryption_key_calculation(Some(dishonest_set), ec); } @@ -1356,7 +1377,109 @@ impl ThresholdKeyshare { }) .collect(); - // Store honest party IDs in state + // Derive expected dimensions from our own share (trusted source). + // All parties use the same on-chain BFV params, so dimensions must be identical. + let own_share = honest_shares + .iter() + .find(|ts| ts.party_id == state.party_id as u64) + .ok_or_else(|| anyhow!("Own share not found in honest shares"))?; + + let expected_num_esi = own_share.esi_sss.len(); + let own_sk_share = own_share + .sk_sss + .clone_share(if own_share.sk_sss.len() == 1 { + 0 + } else { + party_id + }) + .ok_or(anyhow!("No own sk_sss share"))?; + let expected_num_moduli_sk = own_sk_share.num_moduli(); + let expected_num_moduli_esi = if expected_num_esi > 0 { + own_share.esi_sss[0] + .clone_share(if own_share.esi_sss[0].len() == 1 { + 0 + } else { + party_id + }) + .map(|s| s.num_moduli()) + .unwrap_or(0) + } else { + 0 + }; + + // Validate per-party dimensions and exclude mismatched parties. + // This prevents a malicious party with wrong-sized shares from + // causing a panic or opaque error in downstream matrix building. + let mut dimension_excluded: Vec = Vec::new(); + let honest_shares: Vec<_> = honest_shares + .into_iter() + .filter(|ts| { + // Own share is always valid + if ts.party_id == state.party_id as u64 { + return true; + } + // Check esi count + if ts.esi_sss.len() != expected_num_esi { + warn!( + "Party {} has wrong esi_sss count ({} vs expected {}) — excluding from honest set", + ts.party_id, ts.esi_sss.len(), expected_num_esi + ); + dimension_excluded.push(ts.party_id); + return false; + } + // Check sk moduli count + let idx = if ts.sk_sss.len() == 1 { 0 } else { party_id }; + if let Some(share) = ts.sk_sss.clone_share(idx) { + if share.num_moduli() != expected_num_moduli_sk { + warn!( + "Party {} has wrong sk num_moduli ({} vs expected {}) — excluding from honest set", + ts.party_id, share.num_moduli(), expected_num_moduli_sk + ); + dimension_excluded.push(ts.party_id); + return false; + } + } + // Check esi moduli counts + for (esi_idx, esi_shares) in ts.esi_sss.iter().enumerate() { + let idx = if esi_shares.len() == 1 { 0 } else { party_id }; + if let Some(share) = esi_shares.clone_share(idx) { + if share.num_moduli() != expected_num_moduli_esi { + warn!( + "Party {} has wrong esi num_moduli at index {} ({} vs expected {}) — excluding from honest set", + ts.party_id, esi_idx, share.num_moduli(), expected_num_moduli_esi + ); + dimension_excluded.push(ts.party_id); + return false; + } + } + } + true + }) + .collect(); + + if !dimension_excluded.is_empty() { + warn!( + "Excluded {} parties with dimension mismatches: {:?}", + dimension_excluded.len(), + dimension_excluded + ); + // Re-check threshold after exclusion + let threshold = state.threshold_m; + if (honest_shares.len() as u64) < threshold { + self.pending_shares.clear(); + self.bus.publish( + E3Failed { + e3_id: e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }, + ec, + )?; + return Ok(()); + } + } + + // Store honest party IDs in state (after dimension exclusion) let honest_party_ids: HashSet = honest_shares.iter().map(|s| s.party_id).collect(); let num_honest = honest_shares.len(); @@ -1366,74 +1489,36 @@ impl ThresholdKeyshare { ); // Collect ciphertext bytes for C4 proof requests (built here, sent after CalculateDecryptionKey) + // Dimensions are validated per-party above, so all shares are consistent. // C4a: sk_sss ciphertexts from honest parties [H * L] + let num_moduli_sk = expected_num_moduli_sk; let mut sk_ciphertexts_raw = Vec::new(); - let mut num_moduli_sk: Option = None; 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))?; - let moduli = share.num_moduli(); - match num_moduli_sk { - Some(expected) if expected != moduli => { - bail!( - "Party {} has inconsistent sk num_moduli ({} vs expected {})", - ts.party_id, - moduli, - expected - ); - } - None => num_moduli_sk = Some(moduli), - _ => {} - } for ct_bytes in share.ciphertext_bytes() { sk_ciphertexts_raw.push(ct_bytes.clone()); } } - let num_moduli_sk = num_moduli_sk.unwrap_or(0); // C4b: esi_sss ciphertexts from honest parties — one set per smudging noise - let num_esi = honest_shares - .first() - .map(|ts| ts.esi_sss.len()) - .unwrap_or(0); - for ts in &honest_shares { - if ts.esi_sss.len() != num_esi { - bail!( - "Party {} has inconsistent esi_sss count ({} vs expected {})", - ts.party_id, - ts.esi_sss.len(), - num_esi - ); - } - } + let num_esi = expected_num_esi; + let num_moduli_esi = expected_num_moduli_esi; let mut esi_ciphertexts_raw: Vec> = vec![Vec::new(); num_esi]; - let mut num_moduli_esi: Option = None; 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))?; - let moduli = share.num_moduli(); - match num_moduli_esi { - Some(expected) if expected != moduli => { - bail!( - "Party {} has inconsistent esi num_moduli at esi_idx {} ({} vs expected {})", - ts.party_id, esi_idx, moduli, expected - ); - } - None => num_moduli_esi = Some(moduli), - _ => {} - } for ct_bytes in share.ciphertext_bytes() { esi_ciphertexts_raw[esi_idx].push(ct_bytes.clone()); } } } - let num_moduli_esi = num_moduli_esi.unwrap_or(0); // Decrypt our share from each honest sender using BFV let sk_sss_collected: Vec = honest_shares @@ -1685,6 +1770,30 @@ impl ThresholdKeyshare { } if party_proofs.is_empty() { + // Check threshold viability after removing pre-dishonest parties + let threshold = state.threshold_m; + let honest_count = self + .honest_parties + .as_ref() + .map(|h| h.len() as u64) + .unwrap_or(0); + + if honest_count < threshold { + warn!( + "Too few honest parties after C4 pre-filtering for E3 {} ({} honest < {} threshold)", + e3_id, honest_count, threshold + ); + self.bus.publish( + E3Failed { + e3_id: e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }, + ec, + )?; + return Ok(()); + } + info!("No C4 proofs to verify — publishing KeyshareCreated directly"); return self.publish_keyshare_created(ec); } diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index 6833e7a49b..f2f1fc562c 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -878,6 +878,16 @@ impl ProofRequestActor { .retain(|_, (eid, _)| *eid != e3_id); self.pending_threshold.remove(&e3_id); } + + if let Some((e3_id, kind)) = self.decryption_correlation.remove(msg.correlation_id()) { + error!( + "C4 {:?} proof request failed for E3 {}: {err} — DecryptionKeyShared will not be published", + kind, e3_id + ); + self.decryption_correlation + .retain(|_, (eid, _)| *eid != e3_id); + self.pending_decryption.remove(&e3_id); + } } } diff --git a/crates/zk-prover/src/actors/share_verification.rs b/crates/zk-prover/src/actors/share_verification.rs index 69906ac347..dedef49d9a 100644 --- a/crates/zk-prover/src/actors/share_verification.rs +++ b/crates/zk-prover/src/actors/share_verification.rs @@ -53,6 +53,8 @@ struct PendingVerification { ecdsa_dishonest: HashSet, /// Pre-dishonest parties from the dispatch (missing/incomplete proofs). pre_dishonest: BTreeSet, + /// Party IDs dispatched for ZK verification (for cross-checking results). + dispatched_party_ids: HashSet, /// Signed payloads for each party, indexed by party_id. /// Used for SignedProofFailed emission when ZK also fails. party_signed_payloads: HashMap>, @@ -161,6 +163,10 @@ impl ShareVerificationActor { // Dispatch ZK-only verification to multithread let correlation_id = CorrelationId::new(); + let dispatched_party_ids: HashSet = ecdsa_passed_parties + .iter() + .map(|p| p.sender_party_id) + .collect(); self.pending.insert( correlation_id, PendingVerification { @@ -169,6 +175,7 @@ impl ShareVerificationActor { ec: ec.clone(), ecdsa_dishonest, pre_dishonest, + dispatched_party_ids, party_signed_payloads, party_addresses, }, @@ -179,12 +186,18 @@ impl ShareVerificationActor { party_proofs: ecdsa_passed_parties, }), correlation_id, - e3_id, + e3_id.clone(), ); - if let Err(err) = self.bus.publish(request, ec) { + if let Err(err) = self.bus.publish(request, ec.clone()) { error!("Failed to dispatch ZK verification: {err}"); - self.pending.remove(&correlation_id); + if let Some(pending) = self.pending.remove(&correlation_id) { + let mut all_dishonest: BTreeSet = pending.pre_dishonest; + all_dishonest.extend(pending.ecdsa_dishonest); + // Dispatched parties were never ZK-verified — treat as dishonest + all_dishonest.extend(pending.dispatched_party_ids); + self.publish_complete(e3_id, VerificationKind::ShareProofs, all_dishonest, ec); + } } } @@ -249,6 +262,10 @@ impl ShareVerificationActor { } let correlation_id = CorrelationId::new(); + let dispatched_party_ids: HashSet = ecdsa_passed_parties + .iter() + .map(|p| p.sender_party_id) + .collect(); self.pending.insert( correlation_id, PendingVerification { @@ -257,6 +274,7 @@ impl ShareVerificationActor { ec: ec.clone(), ecdsa_dishonest, pre_dishonest, + dispatched_party_ids, party_signed_payloads, party_addresses, }, @@ -267,12 +285,18 @@ impl ShareVerificationActor { party_proofs: ecdsa_passed_parties, }), correlation_id, - e3_id, + e3_id.clone(), ); - if let Err(err) = self.bus.publish(request, ec) { + if let Err(err) = self.bus.publish(request, ec.clone()) { error!("Failed to dispatch C4 ZK verification: {err}"); - self.pending.remove(&correlation_id); + if let Some(pending) = self.pending.remove(&correlation_id) { + let mut all_dishonest: BTreeSet = pending.pre_dishonest; + all_dishonest.extend(pending.ecdsa_dishonest); + // Dispatched parties were never ZK-verified — treat as dishonest + all_dishonest.extend(pending.dispatched_party_ids); + self.publish_complete(e3_id, VerificationKind::DecryptionProofs, all_dishonest, ec); + } } } @@ -386,6 +410,20 @@ impl ShareVerificationActor { let mut all_dishonest: BTreeSet = pending.pre_dishonest; all_dishonest.extend(&pending.ecdsa_dishonest); + // Cross-check: every dispatched party must appear in results. + // If any party is missing from the ZK response, treat as dishonest (defense-in-depth). + let returned_party_ids: HashSet = + zk_results.iter().map(|r| r.sender_party_id).collect(); + for &dispatched_pid in &pending.dispatched_party_ids { + if !returned_party_ids.contains(&dispatched_pid) { + warn!( + "Party {} was dispatched for ZK verification but missing from results — treating as dishonest", + dispatched_pid + ); + all_dishonest.insert(dispatched_pid); + } + } + for result in &zk_results { if !result.all_verified { all_dishonest.insert(result.sender_party_id); From 9579e2baceee74623218e1c0e20b12855210e7c3 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sat, 28 Feb 2026 16:40:42 +0500 Subject: [PATCH 08/11] fix: review comments --- .../enclave_event/decryption_share_proofs.rs | 25 ++------ crates/events/src/enclave_event/mod.rs | 3 - crates/keyshare/src/threshold_keyshare.rs | 63 ++++++++++++++----- crates/zk-prover/src/actors/proof_request.rs | 14 ++++- .../src/actors/share_verification.rs | 7 --- 5 files changed, 62 insertions(+), 50 deletions(-) diff --git a/crates/events/src/enclave_event/decryption_share_proofs.rs b/crates/events/src/enclave_event/decryption_share_proofs.rs index d5f8916f2c..eebb227bb7 100644 --- a/crates/events/src/enclave_event/decryption_share_proofs.rs +++ b/crates/events/src/enclave_event/decryption_share_proofs.rs @@ -8,12 +8,10 @@ //! //! `DecryptionShareProofsPending` is published by [`ThresholdKeyshare`] when it //! has computed the decryption data and needs C4 proofs generated and signed. -//! -//! `DecryptionShareProofsSigned` is published by [`ProofRequestActor`] after it -//! has generated C4 proofs, signed them, and is returning them to -//! [`ThresholdKeyshare`] for Exchange #3 publication. +//! `ProofRequestActor` generates the proofs, signs them, and publishes +//! `DecryptionKeyShared` (Exchange #3) directly. -use crate::{DkgShareDecryptionProofRequest, E3id, SignedProofPayload}; +use crate::{DkgShareDecryptionProofRequest, E3id}; use e3_utils::utility_types::ArcBytes; use serde::{Deserialize, Serialize}; @@ -21,7 +19,7 @@ use serde::{Deserialize, Serialize}; /// /// Carries both the proof generation inputs (sk_request, esm_requests) /// and the protocol data (sk_poly_sum, es_poly_sum, node) so that -/// ProofRequestActor can pass them back in [`DecryptionShareProofsSigned`]. +/// ProofRequestActor can publish `DecryptionKeyShared` directly. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct DecryptionShareProofsPending { pub e3_id: E3id, @@ -36,18 +34,3 @@ pub struct DecryptionShareProofsPending { /// C4b proof requests (SmudgingNoise decryption), one per ESI index. pub esm_requests: Vec, } - -/// ProofRequestActor → ThresholdKeyshare: signed C4 proofs ready. -/// -/// ThresholdKeyshare combines these with state data to publish -/// `DecryptionKeyShared` (Exchange #3). -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct DecryptionShareProofsSigned { - pub e3_id: E3id, - pub party_id: u64, - pub node: String, - pub sk_poly_sum: ArcBytes, - pub es_poly_sum: Vec, - pub signed_sk_decryption_proof: SignedProofPayload, - pub signed_esm_decryption_proofs: Vec, -} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 5ddeaa60ea..ccaa29d5fb 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -243,7 +243,6 @@ pub enum EnclaveEventData { ComputeRequestError(ComputeRequestError), // ComputeRequestFailed SignedProofFailed(SignedProofFailed), DecryptionShareProofsPending(DecryptionShareProofsPending), - DecryptionShareProofsSigned(DecryptionShareProofsSigned), ShareVerificationDispatched(ShareVerificationDispatched), ShareVerificationComplete(ShareVerificationComplete), OutgoingSyncRequested(OutgoingSyncRequested), @@ -501,7 +500,6 @@ impl EnclaveEventData { EnclaveEventData::TestEvent(ref data) => data.e3_id.clone(), EnclaveEventData::SignedProofFailed(ref data) => Some(data.e3_id.clone()), EnclaveEventData::DecryptionShareProofsPending(ref data) => Some(data.e3_id.clone()), - EnclaveEventData::DecryptionShareProofsSigned(ref data) => Some(data.e3_id.clone()), EnclaveEventData::ShareVerificationDispatched(ref data) => Some(data.e3_id.clone()), EnclaveEventData::ShareVerificationComplete(ref data) => Some(data.e3_id.clone()), EnclaveEventData::E3Failed(ref data) => Some(data.e3_id.clone()), @@ -573,7 +571,6 @@ impl_event_types!( ComputeRequestError, SignedProofFailed, DecryptionShareProofsPending, - DecryptionShareProofsSigned, ShareVerificationDispatched, ShareVerificationComplete, OutgoingSyncRequested, diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 97fdce8eb3..5b7c1e7c92 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -364,9 +364,9 @@ pub struct ThresholdKeyshare { share_enc_preset: BfvPreset, /// Transient coordination data bridging async gaps — not persisted. /// Shares pending C2/C3 verification, consumed in `proceed_with_decryption_key_calculation`. - pending_shares: Vec, - /// C4 proof data built during aggregation, consumed after CalculateDecryptionKey. - pending_c4_proof_data: Option<( + pending_shares: Vec>, + /// Share decryption proof data built during aggregation, consumed after CalculateDecryptionKey. + pending_share_decryption_data: Option<( DkgShareDecryptionProofRequest, Vec, )>, @@ -387,7 +387,7 @@ impl ThresholdKeyshare { state: params.state, share_enc_preset: params.share_enc_preset, pending_shares: Vec::new(), - pending_c4_proof_data: None, + pending_share_decryption_data: None, honest_parties: None, early_decryption_key_shares: HashMap::new(), } @@ -1163,8 +1163,8 @@ impl ThresholdKeyshare { }); } - // Store shares on the actor for use after verification completes - self.pending_shares = msg.shares.iter().map(|arc| (**arc).clone()).collect(); + // Store shares on the actor for use after verification completes (keep Arc to avoid deep clone) + self.pending_shares = msg.shares.iter().cloned().collect(); // Backward compat: only when ALL non-self parties have zero proofs // AND none have incomplete proofs (incomplete proofs are always dishonest) @@ -1427,10 +1427,10 @@ impl ThresholdKeyshare { dimension_excluded.push(ts.party_id); return false; } - // Check sk moduli count + // Check sk share exists and moduli count let idx = if ts.sk_sss.len() == 1 { 0 } else { party_id }; - if let Some(share) = ts.sk_sss.clone_share(idx) { - if share.num_moduli() != expected_num_moduli_sk { + match ts.sk_sss.clone_share(idx) { + Some(share) if share.num_moduli() != expected_num_moduli_sk => { warn!( "Party {} has wrong sk num_moduli ({} vs expected {}) — excluding from honest set", ts.party_id, share.num_moduli(), expected_num_moduli_sk @@ -1438,12 +1438,21 @@ impl ThresholdKeyshare { dimension_excluded.push(ts.party_id); return false; } + None => { + warn!( + "Party {} has no sk_sss share at index {} — excluding from honest set", + ts.party_id, idx + ); + dimension_excluded.push(ts.party_id); + return false; + } + _ => {} } - // Check esi moduli counts + // Check esi shares exist and moduli counts for (esi_idx, esi_shares) in ts.esi_sss.iter().enumerate() { let idx = if esi_shares.len() == 1 { 0 } else { party_id }; - if let Some(share) = esi_shares.clone_share(idx) { - if share.num_moduli() != expected_num_moduli_esi { + match esi_shares.clone_share(idx) { + Some(share) if share.num_moduli() != expected_num_moduli_esi => { warn!( "Party {} has wrong esi num_moduli at index {} ({} vs expected {}) — excluding from honest set", ts.party_id, esi_idx, share.num_moduli(), expected_num_moduli_esi @@ -1451,6 +1460,15 @@ impl ThresholdKeyshare { dimension_excluded.push(ts.party_id); return false; } + None => { + warn!( + "Party {} has no esi_sss share at index {} (esi {}) — excluding from honest set", + ts.party_id, idx, esi_idx + ); + dimension_excluded.push(ts.party_id); + return false; + } + _ => {} } } true @@ -1533,7 +1551,8 @@ impl ThresholdKeyshare { }) .collect::>()?; - let esi_sss_collected: Vec> = honest_shares + // Decrypt per-party ESI shares: shape [party][esm_idx] + let per_party_esi: Vec> = honest_shares .iter() .map(|ts| { ts.esi_sss @@ -1549,6 +1568,16 @@ impl ThresholdKeyshare { }) .collect::>()?; + // Transpose to [esm_idx][party] — CalculateDecryptionKey aggregates per smudging noise + let esi_sss_collected: Vec> = (0..num_esi) + .map(|esm_idx| { + per_party_esi + .iter() + .map(|party_esi| party_esi[esm_idx].clone()) + .collect() + }) + .collect(); + // Publish CalculateDecryptionKey request let request = CalculateDecryptionKeyRequest { trbfv_config, @@ -1595,7 +1624,7 @@ impl ThresholdKeyshare { // Store honest parties and C4 data on the actor (transient coordination) self.honest_parties = Some(honest_party_ids); - self.pending_c4_proof_data = Some((sk_request, esm_requests)); + self.pending_share_decryption_data = Some((sk_request, esm_requests)); Ok(()) } @@ -1617,9 +1646,9 @@ impl ThresholdKeyshare { // Extract C4 data from the actor (stored by proceed_with_decryption_key_calculation) let (sk_request, esm_requests) = self - .pending_c4_proof_data + .pending_share_decryption_data .take() - .ok_or_else(|| anyhow!("No pending C4 proof data — CalculateDecryptionKey responded before proof requests were built"))?; + .ok_or_else(|| anyhow!("No pending share decryption data — CalculateDecryptionKey responded before proof requests were built"))?; // Take early shares from the actor before transitioning let early_shares = std::mem::take(&mut self.early_decryption_key_shares); @@ -2246,7 +2275,7 @@ impl Handler for ThresholdKeyshare { self.decryption_key_collector = None; self.decryption_key_shared_collector = None; self.pending_shares.clear(); - self.pending_c4_proof_data = None; + self.pending_share_decryption_data = None; self.honest_parties = None; self.early_decryption_key_shares.clear(); self.notify_sync(ctx, Die); diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index f2f1fc562c..4b9e07a003 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -20,7 +20,7 @@ use e3_events::{ }; use e3_utils::utility_types::ArcBytes; use e3_utils::NotifySync; -use tracing::{error, info}; +use tracing::{error, info, warn}; #[derive(Clone, Debug)] enum ThresholdProofKind { @@ -153,7 +153,9 @@ struct PendingDecryptionProofs { impl PendingDecryptionProofs { fn is_complete(&self) -> bool { - self.sk_proof.is_some() && self.esm_proofs.len() == self.expected_esm_count + self.sk_proof.is_some() + && self.esm_proofs.len() == self.expected_esm_count + && (0..self.expected_esm_count).all(|i| self.esm_proofs.contains_key(&i)) } } @@ -404,6 +406,14 @@ impl ProofRequestActor { let e3_id = msg.e3_id.clone(); let esm_count = msg.esm_requests.len(); + if self.pending_decryption.contains_key(&e3_id) { + warn!( + "Duplicate DecryptionShareProofsPending for E3 {} — ignoring", + e3_id + ); + return; + } + self.pending_decryption.insert( e3_id.clone(), PendingDecryptionProofs { diff --git a/crates/zk-prover/src/actors/share_verification.rs b/crates/zk-prover/src/actors/share_verification.rs index dedef49d9a..b8340678c6 100644 --- a/crates/zk-prover/src/actors/share_verification.rs +++ b/crates/zk-prover/src/actors/share_verification.rs @@ -130,12 +130,8 @@ impl ShareVerificationActor { party_signed_payloads.insert(party.sender_party_id, party.signed_proofs.clone()); if result.passed { ecdsa_passed_parties.push(party.clone()); - if let Some((_, Some(addr))) = &result.failed_payload { - party_addresses.insert(party.sender_party_id, *addr); - } } else { ecdsa_dishonest.insert(party.sender_party_id); - // Emit SignedProofFailed for ECDSA failure if let Some((ref signed, addr)) = result.failed_payload { self.emit_signed_proof_failed(&e3_id, signed, addr, &ec); } @@ -234,9 +230,6 @@ impl ShareVerificationActor { if result.passed { ecdsa_passed_parties.push(party.clone()); - if let Some((_, Some(addr))) = &result.failed_payload { - party_addresses.insert(party.sender_party_id, *addr); - } } else { ecdsa_dishonest.insert(party.sender_party_id); if let Some((ref signed, addr)) = result.failed_payload { From aff71e0a14fe3e21d275d438320249b8563fb620 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sat, 28 Feb 2026 17:01:55 +0500 Subject: [PATCH 09/11] fix: review comments --- crates/keyshare/src/threshold_keyshare.rs | 2 +- .../src/actors/share_verification.rs | 55 ++++++++++++++++--- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 5b7c1e7c92..976b9e1752 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -2020,7 +2020,7 @@ impl Handler for ThresholdKeyshare { Ok(()) } else { warn!( - "DecryptionKeyShared from party {} in ReadyForDecryption but no collector", + "DecryptionKeyShared from party {} dropped — no collector (sole honest party)", data.party_id ); Ok(()) diff --git a/crates/zk-prover/src/actors/share_verification.rs b/crates/zk-prover/src/actors/share_verification.rs index b8340678c6..434bcfe0a1 100644 --- a/crates/zk-prover/src/actors/share_verification.rs +++ b/crates/zk-prover/src/actors/share_verification.rs @@ -26,12 +26,12 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use actix::{Actor, Addr, Context, Handler}; use alloy::primitives::Address; use e3_events::{ - BusHandle, ComputeRequest, ComputeResponse, ComputeResponseKind, CorrelationId, E3id, - EnclaveEvent, EnclaveEventData, EventContext, EventPublisher, EventSubscriber, EventType, - PartyProofsToVerify, PartyShareDecryptionProofsToVerify, PartyVerificationResult, Sequenced, - ShareVerificationComplete, ShareVerificationDispatched, SignedProofFailed, SignedProofPayload, - TypedEvent, VerificationKind, VerifyShareDecryptionProofsRequest, VerifyShareProofsRequest, - ZkRequest, ZkResponse, + BusHandle, ComputeRequest, ComputeRequestError, ComputeResponse, ComputeResponseKind, + CorrelationId, E3id, EnclaveEvent, EnclaveEventData, EventContext, EventPublisher, + EventSubscriber, EventType, PartyProofsToVerify, PartyShareDecryptionProofsToVerify, + PartyVerificationResult, Sequenced, ShareVerificationComplete, ShareVerificationDispatched, + SignedProofFailed, SignedProofPayload, TypedEvent, VerificationKind, + VerifyShareDecryptionProofsRequest, VerifyShareProofsRequest, ZkRequest, ZkResponse, }; use e3_utils::NotifySync; use tracing::{error, info, warn}; @@ -86,6 +86,7 @@ impl ShareVerificationActor { let addr = Self::new(bus).start(); bus.subscribe(EventType::ShareVerificationDispatched, addr.clone().into()); bus.subscribe(EventType::ComputeResponse, addr.clone().into()); + bus.subscribe(EventType::ComputeRequestError, addr.clone().into()); addr } @@ -395,7 +396,11 @@ impl ShareVerificationActor { ComputeResponseKind::Zk(ZkResponse::VerifyShareDecryptionProofs(r)), ) => r.party_results, _ => { - error!("Unexpected ComputeResponse kind for verification"); + error!("Unexpected ComputeResponse kind for verification — treating all dispatched parties as dishonest"); + let mut all_dishonest: BTreeSet = pending.pre_dishonest; + all_dishonest.extend(pending.ecdsa_dishonest); + all_dishonest.extend(pending.dispatched_party_ids); + self.publish_complete(pending.e3_id, pending.kind, all_dishonest, pending.ec); return; } }; @@ -466,6 +471,27 @@ impl ShareVerificationActor { } } + /// Handle computation error from multithread — clean up pending state and + /// publish ShareVerificationComplete treating all dispatched parties as dishonest. + fn handle_compute_request_error(&mut self, msg: TypedEvent) { + let (msg, _ec) = msg.into_components(); + + let correlation_id = msg.correlation_id(); + let Some(pending) = self.pending.remove(correlation_id) else { + return; + }; + + error!( + "ZK verification computation failed for E3 {} ({:?}): {} — treating all dispatched parties as dishonest", + pending.e3_id, pending.kind, msg + ); + + let mut all_dishonest: BTreeSet = pending.pre_dishonest; + all_dishonest.extend(pending.ecdsa_dishonest); + all_dishonest.extend(pending.dispatched_party_ids); + self.publish_complete(pending.e3_id, pending.kind, all_dishonest, pending.ec); + } + fn publish_complete( &self, e3_id: E3id, @@ -502,6 +528,9 @@ impl Handler for ShareVerificationActor { EnclaveEventData::ComputeResponse(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::ComputeRequestError(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } _ => (), } } @@ -530,3 +559,15 @@ impl Handler> for ShareVerificationActor { self.handle_compute_response(msg) } } + +impl Handler> for ShareVerificationActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_compute_request_error(msg) + } +} From fdb7e400cd4d203b3cea2e92a406c86fc89c6689 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sat, 28 Feb 2026 18:28:06 +0500 Subject: [PATCH 10/11] fix: review comments --- crates/keyshare/src/threshold_keyshare.rs | 6 ++++-- crates/zk-prover/src/actors/share_verification.rs | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 976b9e1752..5be5aadebb 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -1200,7 +1200,8 @@ impl ThresholdKeyshare { // All non-self parties are dishonest (missing or incomplete proofs), none to verify let threshold = state.threshold_m; let total = state.threshold_n; - let honest_count = total - pre_dishonest.len() as u64; + let dishonest_count = (pre_dishonest.len() as u64).min(total); + let honest_count = total - dishonest_count; if honest_count < threshold { warn!( @@ -1265,7 +1266,8 @@ impl ThresholdKeyshare { } else { let threshold = state.threshold_m; let total = state.threshold_n; - let honest_count = total - msg.dishonest_parties.len() as u64; + let dishonest_count = (msg.dishonest_parties.len() as u64).min(total); + let honest_count = total - dishonest_count; if honest_count < threshold { warn!( diff --git a/crates/zk-prover/src/actors/share_verification.rs b/crates/zk-prover/src/actors/share_verification.rs index 434bcfe0a1..439122f6ef 100644 --- a/crates/zk-prover/src/actors/share_verification.rs +++ b/crates/zk-prover/src/actors/share_verification.rs @@ -423,6 +423,17 @@ impl ShareVerificationActor { } for result in &zk_results { + // Ignore results for parties we never dispatched (defense-in-depth) + if !pending + .dispatched_party_ids + .contains(&result.sender_party_id) + { + warn!( + "ZK result for party {} was not dispatched — ignoring", + result.sender_party_id + ); + continue; + } if !result.all_verified { all_dishonest.insert(result.sender_party_id); From 8b388cd89e4ea3f95810712daa513aa47da75061 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Sat, 28 Feb 2026 19:43:33 +0500 Subject: [PATCH 11/11] fix: test --- crates/test-helpers/src/usecase_helpers.rs | 15 +++++++++++++-- crates/trbfv/src/calculate_decryption_share.rs | 6 +++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/test-helpers/src/usecase_helpers.rs b/crates/test-helpers/src/usecase_helpers.rs index 3372e9ca60..467d126963 100644 --- a/crates/test-helpers/src/usecase_helpers.rs +++ b/crates/test-helpers/src/usecase_helpers.rs @@ -168,8 +168,8 @@ pub fn get_decryption_keys( }) .collect::>()?; - // Similarly decrypt esi_sss - let esi_sss_collected: Vec> = shares + // Similarly decrypt esi_sss — shape [sender][esi_idx] + let per_sender_esi: Vec> = shares .iter() .map(|ts| { ts.esi_sss @@ -184,6 +184,17 @@ pub fn get_decryption_keys( }) .collect::>()?; + // Transpose to [esi_idx][sender] — CalculateDecryptionKey aggregates per smudging noise + let num_esi = per_sender_esi.first().map_or(0, |v| v.len()); + let esi_sss_collected: Vec> = (0..num_esi) + .map(|esi_idx| { + per_sender_esi + .iter() + .map(|sender_esi| sender_esi[esi_idx].clone()) + .collect() + }) + .collect(); + let CalculateDecryptionKeyResponse { es_poly_sum, sk_poly_sum, diff --git a/crates/trbfv/src/calculate_decryption_share.rs b/crates/trbfv/src/calculate_decryption_share.rs index e2a5e5e5ec..cc7cdcb18f 100644 --- a/crates/trbfv/src/calculate_decryption_share.rs +++ b/crates/trbfv/src/calculate_decryption_share.rs @@ -120,11 +120,15 @@ pub fn calculate_decryption_share( .map(|(index, ciphertext)| { let share_manager = ShareManager::new(num_ciphernodes, threshold, params.clone()); info!("Create decryption share for ct index {}...", index); + // Currently there is a single smudging noise polynomial shared across all + // ciphertexts. When multiple per-ciphertext noises are supported, the + // index mapping here will need to change. + let es_idx = index % es_poly_sum.len(); share_manager .decryption_share( Arc::new(ciphertext), sk_poly_sum.clone(), - es_poly_sum[index].clone(), + es_poly_sum[es_idx].clone(), ) .context(format!("Could not decrypt ciphertext {}", index)) })