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 1/2] 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 2/2] 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