From 627ec6c33e2c404e5867fe88c61c307c0eba5679 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:16:38 +0000 Subject: [PATCH 1/9] feat: add proof of correct aggregration --- templates/default/enclave.config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index cbbafd874c..05cceece53 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -6,11 +6,11 @@ chains: address: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" deploy_block: 19 ciphernode_registry: + address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + deploy_block: 14 + bonding_registry: address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" deploy_block: 15 - bonding_registry: - address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" - deploy_block: 16 fee_token: address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" deploy_block: 10 From a448dad45f17371f206bb44f1d87c408314259ac Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:34:36 +0000 Subject: [PATCH 2/9] feat: share decryption proof integration --- .../src/enclave_event/compute_request/mod.rs | 1 + .../src/enclave_event/compute_request/zk.rs | 30 ++++ .../enclave_event/decryptionshare_created.rs | 4 +- crates/keyshare/src/threshold_keyshare.rs | 156 ++++++++++++++++-- crates/multithread/src/multithread.rs | 89 +++++++++- 5 files changed, 260 insertions(+), 20 deletions(-) diff --git a/crates/events/src/enclave_event/compute_request/mod.rs b/crates/events/src/enclave_event/compute_request/mod.rs index c307d6e4d6..9b258fd519 100644 --- a/crates/events/src/enclave_event/compute_request/mod.rs +++ b/crates/events/src/enclave_event/compute_request/mod.rs @@ -89,6 +89,7 @@ impl ToString for ComputeRequest { ZkRequest::VerifyShareProofs(_) => "ZkVerifyShareProofs", ZkRequest::VerifyShareDecryptionProofs(_) => "ZkVerifyShareDecryptionProofs", ZkRequest::PkAggregation(_) => "ZkPkAggregation", + ZkRequest::ThresholdShareDecryption(_) => "ZkThresholdShareDecryption", }, } .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 588e39b9dd..3bb905002d 100644 --- a/crates/events/src/enclave_event/compute_request/zk.rs +++ b/crates/events/src/enclave_event/compute_request/zk.rs @@ -32,6 +32,8 @@ pub enum ZkRequest { VerifyShareDecryptionProofs(VerifyShareDecryptionProofsRequest), /// Generate proof for public key aggregation (C5). PkAggregation(PkAggregationProofRequest), + /// Generate proof(s) for threshold share decryption (C6). + ThresholdShareDecryption(ThresholdShareDecryptionProofRequest), } /// Request to generate a proof for public key aggregation (C5). @@ -199,6 +201,8 @@ pub enum ZkResponse { VerifyShareDecryptionProofs(VerifyShareDecryptionProofsResponse), /// Proof for public key aggregation (C5). PkAggregation(PkAggregationProofResponse), + /// Proof(s) for threshold share decryption (C6). + ThresholdShareDecryption(ThresholdShareDecryptionProofResponse), } /// Response containing a generated proof for public key aggregation (C5). @@ -207,6 +211,32 @@ pub struct PkAggregationProofResponse { pub proof: Proof, } +/// Request to generate proof(s) of correct threshold share decryption (C6). +/// One proof is generated per ciphertext index. +#[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derivative(Debug)] +pub struct ThresholdShareDecryptionProofRequest { + /// Serialized ciphertext bytes, one per output index. + pub ciphertext_bytes: Vec, + /// Serialized aggregated PublicKey bytes. + pub aggregated_pk_bytes: ArcBytes, + /// Aggregated secret key polynomial (encrypted at rest). + pub sk_poly_sum: SensitiveBytes, + /// Aggregated smudging error polynomials (encrypted at rest), one per output index. + pub es_poly_sum: Vec, + /// Computed decryption share polynomials, one per output index. + pub d_share_bytes: Vec, + /// BFV preset for parameter resolution. + pub params_preset: BfvPreset, +} + +/// Response containing generated proofs for threshold share decryption (C6). +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ThresholdShareDecryptionProofResponse { + /// One C6 proof per ciphertext index. + pub proofs: Vec, +} + /// Response containing a generated share computation proof. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ShareComputationProofResponse { diff --git a/crates/events/src/enclave_event/decryptionshare_created.rs b/crates/events/src/enclave_event/decryptionshare_created.rs index 1916db7068..da443dbdb9 100644 --- a/crates/events/src/enclave_event/decryptionshare_created.rs +++ b/crates/events/src/enclave_event/decryptionshare_created.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::E3id; +use crate::{E3id, Proof}; use actix::Message; use e3_utils::utility_types::ArcBytes; use serde::{Deserialize, Serialize}; @@ -18,6 +18,8 @@ pub struct DecryptionshareCreated { // ciphertext pub e3_id: E3id, pub node: String, + /// C6 proofs: one proof of correct decryption per ciphertext index. + pub decryption_proofs: Vec, } impl Display for DecryptionshareCreated { diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 5be5aadebb..310146a441 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -16,10 +16,12 @@ use e3_events::{ EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCollectionFailed, EncryptionKeyCreated, EncryptionKeyPending, EventContext, FailureReason, KeyshareCreated, PartyId, PartyProofsToVerify, PartyShareDecryptionProofsToVerify, PkGenerationProofRequest, - PkGenerationProofSigned, ProofType, Sequenced, ShareComputationProofRequest, + PkGenerationProofSigned, Proof, ProofType, Sequenced, ShareComputationProofRequest, ShareEncryptionProofRequest, ShareVerificationComplete, ShareVerificationDispatched, SignedProofPayload, ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated, - ThresholdSharePending, TypedEvent, VerificationKind, + ThresholdShareDecryptionProofRequest, ThresholdSharePending, TypedEvent, VerificationKind, + 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}; @@ -155,6 +157,19 @@ pub struct Decrypting { pk_share: ArcBytes, sk_poly_sum: SensitiveBytes, es_poly_sum: Vec, + /// Ciphertext bytes from CiphertextOutputPublished, needed for C6 proof generation. + ciphertext_output: Vec, + signed_pk_generation_proof: Option, + signed_sk_share_computation_proof: Option, + signed_e_sm_share_computation_proof: Option, + signed_sk_share_encryption_proofs: Vec, + signed_e_sm_share_encryption_proofs: Vec, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GeneratingDecryptionProof { + pk_share: ArcBytes, + decryption_share: Vec, signed_pk_generation_proof: Option, signed_sk_share_computation_proof: Option, signed_e_sm_share_computation_proof: Option, @@ -176,6 +191,8 @@ pub enum KeyshareState { ReadyForDecryption(ReadyForDecryption), // Decrypting something Decrypting(Decrypting), + // Generating C6 proof of correct decryption + GeneratingDecryptionProof(GeneratingDecryptionProof), // Finished Completed, } @@ -195,7 +212,8 @@ impl KeyshareState { (K::GeneratingThresholdShare(_), K::AggregatingDecryptionKey(_)) => true, (K::AggregatingDecryptionKey(_), K::ReadyForDecryption(_)) => true, (K::ReadyForDecryption(_), K::Decrypting(_)) => true, - (K::Decrypting(_), K::Completed) => true, + (K::Decrypting(_), K::GeneratingDecryptionProof(_)) => true, + (K::GeneratingDecryptionProof(_), K::Completed) => true, _ => false, } } @@ -219,6 +237,7 @@ impl KeyshareState { Self::AggregatingDecryptionKey(_) => "AggregatingDecryptionKey", Self::ReadyForDecryption(_) => "ReadyForDecryption", Self::Decrypting(_) => "Decrypting", + Self::GeneratingDecryptionProof(_) => "GeneratingDecryptionProof", Self::Completed => "Completed", } } @@ -347,6 +366,16 @@ impl TryInto for ThresholdKeyshareState { } } +impl TryInto for ThresholdKeyshareState { + type Error = anyhow::Error; + fn try_into(self) -> std::result::Result { + match self.state { + KeyshareState::GeneratingDecryptionProof(s) => Ok(s), + _ => Err(anyhow!("Invalid state: expected GeneratingDecryptionProof")), + } + } +} + pub struct ThresholdKeyshareParams { pub bus: BusHandle, pub cipher: Arc, @@ -372,8 +401,10 @@ pub struct ThresholdKeyshare { )>, /// Honest party IDs determined by C2/C3 verification, narrowed by C4. honest_parties: Option>, - /// DecryptionKeyShared events arriving before ReadyForDecryption. - early_decryption_key_shares: HashMap, + /// Temporarily stores DecryptionKeyShared while C4 verification is in flight. + pending_c4_verification_shares: Option>, + /// Aggregated public key bytes, captured from PublicKeyAggregated event for C6 proof. + aggregated_pk: Option, } impl ThresholdKeyshare { @@ -389,7 +420,8 @@ impl ThresholdKeyshare { pending_shares: Vec::new(), pending_share_decryption_data: None, honest_parties: None, - early_decryption_key_shares: HashMap::new(), + pending_c4_verification_shares: None, + aggregated_pk: None, } } } @@ -608,9 +640,17 @@ impl ThresholdKeyshare { } _ => Ok(()), }, - // ZK responses (C4 proofs, share/decryption verification) are now - // handled by ProofRequestActor and ShareVerificationActor respectively. - _ => 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) + } + ZkResponse::ThresholdShareDecryption(_) => self.handle_c6_proof_response(msg), + _ => Ok(()), + }, } } @@ -1879,7 +1919,9 @@ impl ThresholdKeyshare { msg: TypedEvent, ) -> Result<()> { let (msg, ec) = msg.into_components(); - // Set state to decrypting + let ciphertext_output = msg.ciphertext_output; + + // Set state to decrypting, storing ciphertext for later C6 proof generation self.state.try_mutate(&ec, |s| { use KeyshareState as K; @@ -1889,6 +1931,7 @@ impl ThresholdKeyshare { pk_share: current.pk_share, sk_poly_sum: current.sk_poly_sum, es_poly_sum: current.es_poly_sum, + ciphertext_output: ciphertext_output.clone(), 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, @@ -1899,7 +1942,6 @@ impl ThresholdKeyshare { s.new_state(next) })?; - let ciphertext_output = msg.ciphertext_output; let state = self.state.try_get()?; let e3_id = state.get_e3_id(); let decrypting: Decrypting = state.clone().try_into()?; @@ -1922,34 +1964,109 @@ impl ThresholdKeyshare { Ok(()) } - /// CalculateDecryptionShareResponse + /// CalculateDecryptionShareResponse — dispatch C6 proof generation pub fn handle_calculate_decryption_share_response( &mut self, res: TypedEvent, ) -> Result<()> { let (res, ec) = res.into_components(); let msg: CalculateDecryptionShareResponse = res.try_into()?; + let state = self.state.try_get()?; + let e3_id = state.e3_id.clone(); + let decrypting: Decrypting = state.clone().try_into()?; + let d_share_poly = msg.d_share_poly; + + let aggregated_pk_bytes = self + .aggregated_pk + .clone() + .ok_or_else(|| anyhow!("Aggregated public key not available for C6 proof"))?; + + let threshold_preset = self + .share_enc_preset + .clone() + .threshold_counterpart() + .ok_or_else(|| { + anyhow!( + "No threshold counterpart for preset {:?}", + self.share_enc_preset + ) + })?; + + info!("Dispatching C6 proof generation (threshold share decryption)..."); + + // Transition to GeneratingDecryptionProof state + self.state.try_mutate(&ec, |s| { + use KeyshareState as K; + s.new_state(K::GeneratingDecryptionProof(GeneratingDecryptionProof { + pk_share: decrypting.pk_share.clone(), + decryption_share: d_share_poly.clone(), + signed_pk_generation_proof: decrypting.signed_pk_generation_proof.clone(), + signed_sk_share_computation_proof: decrypting + .signed_sk_share_computation_proof + .clone(), + signed_e_sm_share_computation_proof: decrypting + .signed_e_sm_share_computation_proof + .clone(), + signed_sk_share_encryption_proofs: decrypting + .signed_sk_share_encryption_proofs + .clone(), + signed_e_sm_share_encryption_proofs: decrypting + .signed_e_sm_share_encryption_proofs + .clone(), + })) + })?; + + // Dispatch C6 proof request + let request = ComputeRequest::zk( + ZkRequest::ThresholdShareDecryption(ThresholdShareDecryptionProofRequest { + ciphertext_bytes: decrypting.ciphertext_output, + aggregated_pk_bytes, + sk_poly_sum: decrypting.sk_poly_sum, + es_poly_sum: decrypting.es_poly_sum, + d_share_bytes: d_share_poly, + params_preset: threshold_preset, + }), + CorrelationId::new(), + e3_id, + ); + self.bus.publish(request, ec)?; + Ok(()) + } + + /// Handle C6 proof response — publish DecryptionshareCreated with proofs + pub fn handle_c6_proof_response(&mut self, res: TypedEvent) -> Result<()> { + let (res, ec) = res.into_components(); + let zk_response = res.try_into_zk()?; + let proofs = match zk_response { + ZkResponse::ThresholdShareDecryption(data) => data.proofs, + _ => bail!("Expected ThresholdShareDecryptionProofResponse"), + }; + let state = self.state.try_get()?; let party_id = state.party_id; - let node = state.address; - let e3_id = state.e3_id; - let decryption_share = msg.d_share_poly; + let node = state.address.clone(); + let e3_id = state.e3_id.clone(); + let gen_proof: GeneratingDecryptionProof = state.clone().try_into()?; + + info!( + "C6 proof generated ({} proofs), publishing DecryptionshareCreated", + proofs.len() + ); let event = DecryptionshareCreated { party_id, node, e3_id, - decryption_share, + decryption_share: gen_proof.decryption_share, + decryption_proofs: proofs, }; - // send the decryption share self.bus.publish(event, ec.clone())?; // mark as complete self.state.try_mutate(&ec, |s| { use KeyshareState as K; info!("Decryption share sending process is complete"); - s.new_state(K::Completed) })?; @@ -1969,6 +2086,9 @@ impl Handler for ThresholdKeyshare { EnclaveEventData::CiphertextOutputPublished(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::PublicKeyAggregated(data) => { + self.aggregated_pk = Some(ArcBytes::from_bytes(&data.pubkey)); + } EnclaveEventData::ThresholdShareCreated(data) => { let _ = self.handle_threshold_share_created(TypedEvent::new(data, ec), ctx.address()); diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index c89fd1a91e..16caa2ff10 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -29,7 +29,8 @@ use e3_events::{ PkAggregationProofRequest, PkAggregationProofResponse, PkBfvProofRequest, PkBfvProofResponse, PkGenerationProofRequest, PkGenerationProofResponse, ShareComputationProofRequest, ShareComputationProofResponse, ShareEncryptionProofRequest, ShareEncryptionProofResponse, - TypedEvent, VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, + ThresholdShareDecryptionProofRequest, ThresholdShareDecryptionProofResponse, TypedEvent, + VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, VerifyShareProofsRequest, VerifyShareProofsResponse, ZkError as ZkEventError, ZkRequest, ZkResponse, }; @@ -325,6 +326,87 @@ fn handle_pk_aggregation_proof( )) } +fn handle_threshold_share_decryption_proof( + prover: &ZkProver, + cipher: &Cipher, + req: ThresholdShareDecryptionProofRequest, + request: ComputeRequest, +) -> Result { + // 1. Build threshold BFV parameters from preset + let (threshold_params, _dkg_params) = build_pair_for_preset(req.params_preset.clone()) + .map_err(|e| make_zk_error(&request, format!("build_pair_for_preset: {}", e)))?; + + // 2. Deserialize aggregated PublicKey + let public_key = PublicKey::from_bytes(&req.aggregated_pk_bytes, &threshold_params) + .map_err(|e| make_zk_error(&request, format!("aggregated_pk deserialize: {:?}", e)))?; + + // 3. Decrypt sk_poly_sum → Poly → CrtPolynomial (s) + let sk_poly = e3_trbfv::helpers::try_poly_from_sensitive_bytes( + req.sk_poly_sum, + threshold_params.clone(), + cipher, + ) + .map_err(|e| make_zk_error(&request, format!("sk_poly_sum decrypt: {}", e)))?; + let s = CrtPolynomial::from_fhe_polynomial(&sk_poly); + + // 4. For each index, build circuit data and generate proof + let num_indices = req.ciphertext_bytes.len(); + let mut proofs = Vec::with_capacity(num_indices); + + for i in 0..num_indices { + // Deserialize ciphertext + let ciphertext = Ciphertext::from_bytes(&req.ciphertext_bytes[i], &threshold_params) + .map_err(|e| { + make_zk_error(&request, format!("ciphertext[{}] deserialize: {:?}", i, e)) + })?; + + // Decrypt es_poly_sum[i] → Poly → CrtPolynomial (e) + let e_poly = e3_trbfv::helpers::try_poly_from_sensitive_bytes( + req.es_poly_sum[i].clone(), + threshold_params.clone(), + cipher, + ) + .map_err(|e| make_zk_error(&request, format!("es_poly_sum[{}] decrypt: {}", i, e)))?; + let e = CrtPolynomial::from_fhe_polynomial(&e_poly); + + // Deserialize d_share → Poly → CrtPolynomial + let d_share_poly = try_poly_from_bytes(&req.d_share_bytes[i], &threshold_params) + .map_err(|e| make_zk_error(&request, format!("d_share[{}] deserialize: {}", i, e)))?; + let d_share = CrtPolynomial::from_fhe_polynomial(&d_share_poly); + + // Build circuit data + let circuit_data = e3_zk_helpers::threshold::share_decryption::ShareDecryptionCircuitData { + ciphertext, + public_key: public_key.clone(), + s: s.clone(), + e, + d_share, + }; + + // Generate proof + let circuit = e3_zk_helpers::threshold::share_decryption::ShareDecryptionCircuit; + let e3_id_str = request.e3_id.to_string(); + let proof = circuit + .prove(prover, &req.params_preset, &circuit_data, &e3_id_str) + .map_err(|e| { + ComputeRequestError::new( + ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(format!( + "C6 proof[{}]: {}", + i, e + ))), + request.clone(), + ) + })?; + proofs.push(proof); + } + + Ok(ComputeResponse::zk( + ZkResponse::ThresholdShareDecryption(ThresholdShareDecryptionProofResponse { proofs }), + request.correlation_id, + request.e3_id, + )) +} + fn timefunc( name: &str, id: u8, @@ -501,6 +583,11 @@ fn handle_zk_request( ZkRequest::PkAggregation(req) => timefunc("zk_pk_aggregation", id, || { handle_pk_aggregation_proof(&prover, req, request.clone()) }), + ZkRequest::ThresholdShareDecryption(req) => { + timefunc("zk_threshold_share_decryption", id, || { + handle_threshold_share_decryption_proof(&prover, &cipher, req, request.clone()) + }) + } } } From ad7ad71ff908a84e9985f562cf998b6314563027 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Fri, 27 Feb 2026 23:09:03 +0000 Subject: [PATCH 3/9] feat: result shares aggregation integration --- Cargo.lock | 2 + crates/aggregator/src/ext.rs | 10 +- .../src/threshold_plaintext_aggregator.rs | 369 ++++++++++++++---- .../src/ciphernode_builder.rs | 5 +- .../src/enclave_event/compute_request/mod.rs | 2 + .../src/enclave_event/compute_request/zk.rs | 67 ++++ .../src/enclave_event/plaintext_aggregated.rs | 4 +- .../src/decryption_key_shared_collector.rs | 2 +- crates/keyshare/src/threshold_keyshare.rs | 26 +- .../keyshare/src/threshold_share_collector.rs | 2 +- crates/multithread/Cargo.toml | 2 + crates/multithread/src/multithread.rs | 150 ++++++- crates/tests/tests/integration.rs | 60 ++- templates/default/deployed_contracts.json | 26 +- templates/default/tests/integration.spec.ts | 2 +- 15 files changed, 618 insertions(+), 111 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5da183ccd..ac888f026e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3501,6 +3501,7 @@ dependencies = [ "actix", "anyhow", "bincode 1.3.3", + "e3-bfv-client", "e3-crypto", "e3-data", "e3-events", @@ -3511,6 +3512,7 @@ dependencies = [ "e3-zk-helpers", "e3-zk-prover", "fhe", + "fhe-math", "fhe-traits", "ndarray", "num-bigint", diff --git a/crates/aggregator/src/ext.rs b/crates/aggregator/src/ext.rs index 263fe5b55c..90c3ee9657 100644 --- a/crates/aggregator/src/ext.rs +++ b/crates/aggregator/src/ext.rs @@ -147,13 +147,19 @@ fn create_publickey_aggregator( pub struct ThresholdPlaintextAggregatorExtension { bus: BusHandle, sortition: Addr, + params_preset: BfvPreset, } impl ThresholdPlaintextAggregatorExtension { - pub fn create(bus: &BusHandle, sortition: &Addr) -> Box { + pub fn create( + bus: &BusHandle, + sortition: &Addr, + params_preset: BfvPreset, + ) -> Box { Box::new(Self { bus: bus.clone(), sortition: sortition.clone(), + params_preset, }) } } @@ -194,6 +200,7 @@ impl E3Extension for ThresholdPlaintextAggregatorExtension { bus: self.bus.clone(), sortition: self.sortition.clone(), e3_id: e3_id.clone(), + params_preset: self.params_preset.clone(), }, sync_state, ) @@ -222,6 +229,7 @@ impl E3Extension for ThresholdPlaintextAggregatorExtension { bus: self.bus.clone(), sortition: self.sortition.clone(), e3_id: ctx.e3_id.clone(), + params_preset: self.params_preset.clone(), }, sync_state, ) diff --git a/crates/aggregator/src/threshold_plaintext_aggregator.rs b/crates/aggregator/src/threshold_plaintext_aggregator.rs index 13aad3b4db..063ef3c07d 100644 --- a/crates/aggregator/src/threshold_plaintext_aggregator.rs +++ b/crates/aggregator/src/threshold_plaintext_aggregator.rs @@ -11,9 +11,12 @@ use anyhow::{anyhow, bail, ensure, Result}; use e3_data::Persistable; use e3_events::{ prelude::*, trap, BusHandle, ComputeRequest, ComputeResponse, ComputeResponseKind, - CorrelationId, DecryptionshareCreated, Die, E3id, EType, EnclaveEvent, EnclaveEventData, - EventContext, PlaintextAggregated, Seed, Sequenced, TypedEvent, + CorrelationId, DecryptedSharesAggregationProofRequest, DecryptedSharesAggregationProofResponse, + DecryptionshareCreated, Die, E3id, EType, EnclaveEvent, EnclaveEventData, EventContext, + PartyC6ProofsToVerify, PlaintextAggregated, Proof, Seed, Sequenced, TypedEvent, + VerifyC6ProofsRequest, VerifyC6ProofsResponse, ZkRequest, ZkResponse, }; +use e3_fhe_params::BfvPreset; use e3_sortition::{E3CommitteeContainsRequest, E3CommitteeContainsResponse, Sortition}; use e3_trbfv::{ calculate_threshold_decryption::CalculateThresholdDecryptionRequest, TrBFVConfig, TrBFVRequest, @@ -21,18 +24,28 @@ use e3_trbfv::{ }; use e3_utils::NotifySync; use e3_utils::{utility_types::ArcBytes, MAILBOX_LIMIT}; -use tracing::{debug, info, trace}; +use tracing::{debug, info, trace, warn}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Collecting { threshold_m: u64, threshold_n: u64, shares: HashMap>, + c6_proofs: HashMap>, seed: Seed, ciphertext_output: Vec, params: ArcBytes, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct VerifyingC6 { + threshold_m: u64, + threshold_n: u64, + shares: HashMap>, + ciphertext_output: Vec, + params: ArcBytes, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Computing { threshold_m: u64, @@ -42,6 +55,14 @@ pub struct Computing { params: ArcBytes, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GeneratingC7Proof { + threshold_m: u64, + threshold_n: u64, + shares: Vec<(u64, Vec)>, + plaintext: Vec, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Complete { decrypted: Vec, @@ -51,7 +72,9 @@ pub struct Complete { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum ThresholdPlaintextAggregatorState { Collecting(Collecting), + VerifyingC6(VerifyingC6), Computing(Computing), + GeneratingC7Proof(GeneratingC7Proof), Complete(Complete), } @@ -67,6 +90,18 @@ impl TryFrom for Collecting { } } +impl TryFrom for VerifyingC6 { + type Error = anyhow::Error; + fn try_from( + value: ThresholdPlaintextAggregatorState, + ) -> std::result::Result { + match value { + ThresholdPlaintextAggregatorState::VerifyingC6(s) => Ok(s), + _ => bail!("Inconsistent state: expected VerifyingC6"), + } + } +} + impl TryFrom for Computing { type Error = anyhow::Error; fn try_from( @@ -74,7 +109,19 @@ impl TryFrom for Computing { ) -> std::result::Result { match value { ThresholdPlaintextAggregatorState::Computing(s) => Ok(s), - _ => bail!("Inconsistent state"), + _ => bail!("Inconsistent state: expected Computing"), + } + } +} + +impl TryFrom for GeneratingC7Proof { + type Error = anyhow::Error; + fn try_from( + value: ThresholdPlaintextAggregatorState, + ) -> std::result::Result { + match value { + ThresholdPlaintextAggregatorState::GeneratingC7Proof(s) => Ok(s), + _ => bail!("Inconsistent state: expected GeneratingC7Proof"), } } } @@ -86,7 +133,7 @@ impl TryFrom for Complete { ) -> std::result::Result { match value { ThresholdPlaintextAggregatorState::Complete(s) => Ok(s), - _ => bail!("Inconsistent state"), + _ => bail!("Inconsistent state: expected Complete"), } } } @@ -103,6 +150,7 @@ impl ThresholdPlaintextAggregatorState { threshold_m, threshold_n, shares: HashMap::new(), + c6_proofs: HashMap::new(), seed, ciphertext_output, params, @@ -123,6 +171,7 @@ pub struct ThresholdPlaintextAggregator { bus: BusHandle, sortition: Addr, e3_id: E3id, + params_preset: BfvPreset, state: Persistable, } @@ -130,6 +179,7 @@ pub struct ThresholdPlaintextAggregatorParams { pub bus: BusHandle, pub sortition: Addr, pub e3_id: E3id, + pub params_preset: BfvPreset, } impl ThresholdPlaintextAggregator { @@ -141,6 +191,7 @@ impl ThresholdPlaintextAggregator { bus: params.bus, sortition: params.sortition, e3_id: params.e3_id, + params_preset: params.params_preset, state, } } @@ -149,6 +200,7 @@ impl ThresholdPlaintextAggregator { &mut self, party_id: u64, share: Vec, + decryption_proofs: Vec, ec: &EventContext, ) -> Result<()> { self.state.try_mutate(ec, |state| { @@ -159,9 +211,11 @@ impl ThresholdPlaintextAggregator { let threshold_n = current.threshold_n; let params = current.params.clone(); let mut shares = current.shares; + let mut c6_proofs = current.c6_proofs; info!("pushing to share collection {} {:?}", party_id, share); shares.insert(party_id, share); + c6_proofs.insert(party_id, decryption_proofs); if shares.len() <= threshold_m as usize { return Ok(ThresholdPlaintextAggregatorState::Collecting(Collecting { @@ -170,39 +224,113 @@ impl ThresholdPlaintextAggregator { threshold_m, ciphertext_output, shares, + c6_proofs, seed: current.seed, })); } - info!("Changing state to computing because received enough shares..."); + info!("Changing state to VerifyingC6 because received enough shares..."); - Ok(ThresholdPlaintextAggregatorState::Computing(Computing { - shares: shares.into_iter().collect(), - ciphertext_output, - threshold_m, - threshold_n, - params, - })) + Ok(ThresholdPlaintextAggregatorState::VerifyingC6( + VerifyingC6 { + shares, + ciphertext_output, + threshold_m, + threshold_n, + params, + }, + )) }) } - pub fn set_decryption( + /// Dispatch a C6 proof verification request for all collected parties. + pub fn dispatch_c6_verification( &mut self, - decrypted: Vec, + c6_proofs: HashMap>, + ec: EventContext, + ) -> Result<()> { + let party_proofs: Vec = c6_proofs + .into_iter() + .map(|(party_id, proofs)| PartyC6ProofsToVerify { + sender_party_id: party_id, + c6_proofs: proofs, + }) + .collect(); + + let event = ComputeRequest::zk( + ZkRequest::VerifyC6Proofs(VerifyC6ProofsRequest { party_proofs }), + CorrelationId::new(), + self.e3_id.clone(), + ); + + self.bus.publish(event, ec)?; + Ok(()) + } + + /// Handle C6 verification response: filter dishonest parties, transition to Computing. + pub fn handle_c6_verification_response( + &mut self, + data: VerifyC6ProofsResponse, ec: &EventContext, ) -> Result<()> { - self.state.try_mutate(ec, |mut state| { - let ThresholdPlaintextAggregatorState::Computing(Computing { shares, .. }) = &mut state - else { - return Ok(state.clone()); - }; - let shares = shares.to_owned(); + let state: VerifyingC6 = self + .state + .get() + .ok_or(anyhow!("Could not get state"))? + .try_into()?; - Ok(ThresholdPlaintextAggregatorState::Complete(Complete { - decrypted, - shares, + // Determine honest parties + let honest_party_ids: Vec = data + .party_results + .iter() + .filter(|r| r.all_verified) + .map(|r| r.sender_party_id) + .collect(); + + let dishonest: Vec = data + .party_results + .iter() + .filter(|r| !r.all_verified) + .map(|r| r.sender_party_id) + .collect(); + + if !dishonest.is_empty() { + warn!( + "C6 verification: {} dishonest parties filtered: {:?}", + dishonest.len(), + dishonest + ); + } + + ensure!( + honest_party_ids.len() > state.threshold_m as usize, + "Not enough honest parties after C6 verification: {} honest, {} required", + honest_party_ids.len(), + state.threshold_m + 1 + ); + + // Filter shares to only honest parties + let honest_shares: Vec<(u64, Vec)> = honest_party_ids + .iter() + .filter_map(|id| state.shares.get(id).map(|s| (*id, s.clone()))) + .collect(); + + info!( + "C6 verification passed: {} honest parties, transitioning to Computing", + honest_shares.len() + ); + + self.state.try_mutate(ec, |_| { + Ok(ThresholdPlaintextAggregatorState::Computing(Computing { + shares: honest_shares.clone(), + ciphertext_output: state.ciphertext_output.clone(), + threshold_m: state.threshold_m, + threshold_n: state.threshold_n, + params: state.params.clone(), })) - }) + })?; + + Ok(()) } pub fn handle_compute_aggregate(&mut self, msg: TypedEvent) -> Result<()> { @@ -235,6 +363,65 @@ impl ThresholdPlaintextAggregator { Ok(()) } + /// Dispatch a C7 proof generation request. + pub fn dispatch_c7_proof_request( + &mut self, + shares: Vec<(u64, Vec)>, + plaintext: Vec, + threshold_m: u64, + threshold_n: u64, + ec: EventContext, + ) -> Result<()> { + let event = ComputeRequest::zk( + ZkRequest::DecryptedSharesAggregation(DecryptedSharesAggregationProofRequest { + d_share_polys: shares, + plaintext, + params_preset: self.params_preset.clone(), + threshold_m, + threshold_n, + }), + CorrelationId::new(), + self.e3_id.clone(), + ); + + self.bus.publish(event, ec)?; + Ok(()) + } + + /// Handle C7 proof response: transition to Complete and publish PlaintextAggregated. + pub fn handle_c7_proof_response( + &mut self, + data: DecryptedSharesAggregationProofResponse, + ec: &EventContext, + ) -> Result<()> { + let state: GeneratingC7Proof = self + .state + .get() + .ok_or(anyhow!("Could not get state"))? + .try_into()?; + + let plaintext = state.plaintext.clone(); + let shares = state.shares.clone(); + + self.state.try_mutate(ec, |_| { + Ok(ThresholdPlaintextAggregatorState::Complete(Complete { + decrypted: plaintext.clone(), + shares: shares.clone(), + })) + })?; + + // Dispatch the PlaintextAggregated event with C7 proofs + let event = PlaintextAggregated { + decrypted_output: plaintext, + e3_id: self.e3_id.clone(), + aggregation_proofs: data.proofs, + }; + + info!("Dispatching plaintext event with C7 proofs {:?}", event); + self.bus.publish(event, ec.clone())?; + Ok(()) + } + pub fn handle_compute_response(&mut self, msg: TypedEvent) -> Result<()> { let (msg, ec) = msg.into_components(); ensure!( @@ -242,28 +429,79 @@ impl ThresholdPlaintextAggregator { "PlaintextAggregator should never receive incorrect e3_id msgs" ); - let ComputeResponseKind::TrBFV(TrBFVResponse::CalculateThresholdDecryption(response)) = - msg.response - else { - // Must be another compute response so ignoring - return Ok(()); - }; - - info!("Received response {:?}", response); + match msg.response { + // C6 verification response + ComputeResponseKind::Zk(ZkResponse::VerifyC6Proofs(data)) => { + info!("Received C6 verification response"); + self.handle_c6_verification_response(data, &ec)?; + + // Now dispatch the TrBFV computation + let state: Computing = self + .state + .get() + .ok_or(anyhow!("Could not get state after C6 verification"))? + .try_into()?; + + let trbfv_config = + TrBFVConfig::new(state.params.clone(), state.threshold_n, state.threshold_m); + + let event = ComputeRequest::trbfv( + TrBFVRequest::CalculateThresholdDecryption( + CalculateThresholdDecryptionRequest { + ciphertexts: state.ciphertext_output.clone(), + trbfv_config, + d_share_polys: state.shares.clone(), + } + .into(), + ), + CorrelationId::new(), + self.e3_id.clone(), + ); + self.bus.publish(event, ec)?; + } - // Update the local state - let plaintext = response.plaintext; + // TrBFV threshold decryption response → transition to GeneratingC7Proof + ComputeResponseKind::TrBFV(TrBFVResponse::CalculateThresholdDecryption(response)) => { + info!("Received TrBFV threshold decryption response"); + let plaintext = response.plaintext; + + let state: Computing = self + .state + .get() + .ok_or(anyhow!("Could not get state"))? + .try_into()?; + + let shares = state.shares.clone(); + let threshold_m = state.threshold_m; + let threshold_n = state.threshold_n; + + // Transition to GeneratingC7Proof + self.state.try_mutate(&ec, |_| { + Ok(ThresholdPlaintextAggregatorState::GeneratingC7Proof( + GeneratingC7Proof { + threshold_m, + threshold_n, + shares: shares.clone(), + plaintext: plaintext.clone(), + }, + )) + })?; + + // Dispatch C7 proof request + self.dispatch_c7_proof_request(shares, plaintext, threshold_m, threshold_n, ec)?; + } - self.set_decryption(plaintext.clone(), &ec)?; + // C7 proof response → Complete + publish + ComputeResponseKind::Zk(ZkResponse::DecryptedSharesAggregation(data)) => { + info!("Received C7 proof response"); + self.handle_c7_proof_response(data, &ec)?; + } - // Dispatch the PlaintextAggregated event - let event = PlaintextAggregated { - decrypted_output: plaintext, // Extracting here for now - e3_id: self.e3_id.clone(), - }; + _ => { + // Not a response we handle — ignore + } + } - info!("Dispatching plaintext event {:?}", event); - self.bus.publish(event, ec)?; Ok(()) } } @@ -324,7 +562,7 @@ impl Handler>> fn handle( &mut self, msg: E3CommitteeContainsResponse>, - ctx: &mut Self::Context, + _ctx: &mut Self::Context, ) -> Self::Result { trap( EType::PublickeyAggregation, @@ -346,34 +584,33 @@ impl Handler>> DecryptionshareCreated { party_id, decryption_share, + decryption_proofs, .. }, ec, ) = msg.into_inner().into_components(); - self.add_share(party_id, decryption_share, &ec)?; - - if let Some(ThresholdPlaintextAggregatorState::Computing(Computing { - threshold_m, - threshold_n, - shares, - ciphertext_output, - .. - })) = self.state.get() - { - self.notify_sync( - ctx, - TypedEvent::new( - ComputeAggregate { - shares: shares.clone(), - ciphertext_output: ciphertext_output.clone(), - threshold_m, - threshold_n, - }, - ec, - ), - ) + // Collect c6_proofs before add_share mutates state + let c6_proofs_snapshot = + if let Some(ThresholdPlaintextAggregatorState::Collecting(ref collecting)) = + self.state.get() + { + let mut proofs = collecting.c6_proofs.clone(); + proofs.insert(party_id, decryption_proofs.clone()); + Some(proofs) + } else { + None + }; + + self.add_share(party_id, decryption_share, decryption_proofs, &ec)?; + + // If we transitioned to VerifyingC6, dispatch C6 verification + if let Some(ThresholdPlaintextAggregatorState::VerifyingC6(_)) = self.state.get() { + if let Some(proofs) = c6_proofs_snapshot { + self.dispatch_c6_verification(proofs, ec)?; + } } + Ok(()) }, ) diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 598f65df4a..634c66172a 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -478,8 +478,11 @@ impl CiphernodeBuilder { if self.threshold_plaintext_agg { info!("Setting up ThresholdPlaintextAggregatorExtension"); let _ = self.ensure_multithread(&bus); + let aggregator_preset = BfvPreset::InsecureThreshold512; e3_builder = e3_builder.with(ThresholdPlaintextAggregatorExtension::create( - &bus, &sortition, + &bus, + &sortition, + aggregator_preset, )) } diff --git a/crates/events/src/enclave_event/compute_request/mod.rs b/crates/events/src/enclave_event/compute_request/mod.rs index 9b258fd519..db477ef452 100644 --- a/crates/events/src/enclave_event/compute_request/mod.rs +++ b/crates/events/src/enclave_event/compute_request/mod.rs @@ -90,6 +90,8 @@ impl ToString for ComputeRequest { ZkRequest::VerifyShareDecryptionProofs(_) => "ZkVerifyShareDecryptionProofs", ZkRequest::PkAggregation(_) => "ZkPkAggregation", ZkRequest::ThresholdShareDecryption(_) => "ZkThresholdShareDecryption", + ZkRequest::VerifyC6Proofs(_) => "ZkVerifyC6Proofs", + ZkRequest::DecryptedSharesAggregation(_) => "ZkDecryptedSharesAggregation", }, } .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 3bb905002d..dc593896f0 100644 --- a/crates/events/src/enclave_event/compute_request/zk.rs +++ b/crates/events/src/enclave_event/compute_request/zk.rs @@ -34,6 +34,10 @@ pub enum ZkRequest { PkAggregation(PkAggregationProofRequest), /// Generate proof(s) for threshold share decryption (C6). ThresholdShareDecryption(ThresholdShareDecryptionProofRequest), + /// Batch-verify C6 proofs from DecryptionshareCreated events. + VerifyC6Proofs(VerifyC6ProofsRequest), + /// Generate proof for decrypted shares aggregation (C7). + DecryptedSharesAggregation(DecryptedSharesAggregationProofRequest), } /// Request to generate a proof for public key aggregation (C5). @@ -203,6 +207,10 @@ pub enum ZkResponse { PkAggregation(PkAggregationProofResponse), /// Proof(s) for threshold share decryption (C6). ThresholdShareDecryption(ThresholdShareDecryptionProofResponse), + /// Batch verification results for C6 proofs. + VerifyC6Proofs(VerifyC6ProofsResponse), + /// Proof for decrypted shares aggregation (C7). + DecryptedSharesAggregation(DecryptedSharesAggregationProofResponse), } /// Response containing a generated proof for public key aggregation (C5). @@ -371,6 +379,65 @@ pub struct VerifyShareDecryptionProofsResponse { pub party_results: Vec, } +// --- C6 verification (follows VerifyC4Proofs pattern) --- + +/// Request to batch-verify C6 proofs from DecryptionshareCreated events. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VerifyC6ProofsRequest { + /// C6 proofs grouped by sender party_id. + pub party_proofs: Vec, +} + +/// C6 proofs from a single sender to verify. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PartyC6ProofsToVerify { + /// The party that generated these proofs. + pub sender_party_id: u64, + /// One C6 proof per ciphertext index. + pub c6_proofs: Vec, +} + +/// Batch verification results for C6 proofs. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct VerifyC6ProofsResponse { + /// Per-party verification results. + pub party_results: Vec, +} + +/// Verification result for C6 proofs from a single sender. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PartyC6VerificationResult { + /// The party whose C6 proofs were verified. + pub sender_party_id: u64, + /// Whether ALL C6 proofs from this party verified successfully. + pub all_verified: bool, +} + +// --- C7 proof generation --- + +/// Request to generate proof(s) for decrypted shares aggregation (C7). +#[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derivative(Debug)] +pub struct DecryptedSharesAggregationProofRequest { + /// Decryption shares per party: (party_id, shares_per_ct_index). + pub d_share_polys: Vec<(u64, Vec)>, + /// Decoded plaintext per ciphertext index. + pub plaintext: Vec, + /// BFV preset (selects BN vs Mod circuit variant). + pub params_preset: BfvPreset, + /// Threshold required for decryption. + pub threshold_m: u64, + /// Committee size. + pub threshold_n: u64, +} + +/// Response containing generated proofs for decrypted shares aggregation (C7). +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DecryptedSharesAggregationProofResponse { + /// One C7 proof per ciphertext index. + pub proofs: Vec, +} + /// ZK-specific error variants. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ZkError { diff --git a/crates/events/src/enclave_event/plaintext_aggregated.rs b/crates/events/src/enclave_event/plaintext_aggregated.rs index fc2a25cd3a..f3964173c0 100644 --- a/crates/events/src/enclave_event/plaintext_aggregated.rs +++ b/crates/events/src/enclave_event/plaintext_aggregated.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::E3id; +use crate::{E3id, Proof}; use actix::Message; use derivative::Derivative; use e3_utils::ArcBytes; @@ -17,6 +17,8 @@ use std::fmt::{self, Display}; pub struct PlaintextAggregated { pub e3_id: E3id, pub decrypted_output: Vec, + /// C7 proofs: one proof of correct aggregation per ciphertext index. + pub aggregation_proofs: Vec, } impl Display for PlaintextAggregated { diff --git a/crates/keyshare/src/decryption_key_shared_collector.rs b/crates/keyshare/src/decryption_key_shared_collector.rs index e6fc4872b3..0795b4124c 100644 --- a/crates/keyshare/src/decryption_key_shared_collector.rs +++ b/crates/keyshare/src/decryption_key_shared_collector.rs @@ -16,7 +16,7 @@ use tracing::{info, warn}; use crate::ThresholdKeyshare; -const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(600); +const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(1200); enum CollectorState { Collecting, diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 310146a441..58d094177e 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -16,12 +16,11 @@ use e3_events::{ EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCollectionFailed, EncryptionKeyCreated, EncryptionKeyPending, EventContext, FailureReason, KeyshareCreated, PartyId, PartyProofsToVerify, PartyShareDecryptionProofsToVerify, PkGenerationProofRequest, - PkGenerationProofSigned, Proof, ProofType, Sequenced, ShareComputationProofRequest, + PkGenerationProofSigned, ProofType, Sequenced, ShareComputationProofRequest, ShareEncryptionProofRequest, ShareVerificationComplete, ShareVerificationDispatched, SignedProofPayload, ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated, ThresholdShareDecryptionProofRequest, ThresholdSharePending, TypedEvent, VerificationKind, - VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, - VerifyShareProofsRequest, VerifyShareProofsResponse, ZkRequest, ZkResponse, + ZkRequest, ZkResponse, }; use e3_fhe_params::create_deterministic_crp_from_default_seed; use e3_fhe_params::{build_pair_for_preset, BfvParamSet, BfvPreset}; @@ -640,14 +639,10 @@ impl ThresholdKeyshare { } _ => Ok(()), }, + // ZK responses: C4 proofs and share verification are handled by + // ProofRequestActor and ShareVerificationActor respectively. + // Only C6 (ThresholdShareDecryption) is handled here. 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) - } ZkResponse::ThresholdShareDecryption(_) => self.handle_c6_proof_response(msg), _ => Ok(()), }, @@ -1693,7 +1688,10 @@ impl ThresholdKeyshare { .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); + let early_shares = self + .pending_c4_verification_shares + .take() + .unwrap_or_default(); // Transition to ReadyForDecryption self.state.try_mutate(&ec, |s| { @@ -1785,7 +1783,9 @@ impl ThresholdKeyshare { "Storing early DecryptionKeyShared from party {} (state: AggregatingDecryptionKey)", party_id ); - self.early_decryption_key_shares.insert(party_id, data); + self.pending_c4_verification_shares + .get_or_insert_with(HashMap::new) + .insert(party_id, data); Ok(()) } @@ -2399,7 +2399,7 @@ impl Handler for ThresholdKeyshare { self.pending_shares.clear(); self.pending_share_decryption_data = None; self.honest_parties = None; - self.early_decryption_key_shares.clear(); + self.pending_c4_verification_shares = None; self.notify_sync(ctx, Die); } } diff --git a/crates/keyshare/src/threshold_share_collector.rs b/crates/keyshare/src/threshold_share_collector.rs index 8d097d34c3..edf052b9bd 100644 --- a/crates/keyshare/src/threshold_share_collector.rs +++ b/crates/keyshare/src/threshold_share_collector.rs @@ -34,7 +34,7 @@ pub struct ReceivedShareProofs { pub signed_c3b_proofs: Vec, } -const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(600); +const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(1200); pub(crate) enum CollectorState { Collecting, diff --git a/crates/multithread/Cargo.toml b/crates/multithread/Cargo.toml index 9f414b2ab9..822de62b61 100644 --- a/crates/multithread/Cargo.toml +++ b/crates/multithread/Cargo.toml @@ -10,6 +10,7 @@ repository.workspace = true actix = { workspace = true } anyhow = { workspace = true } bincode = { workspace = true } +e3-bfv-client = { workspace = true } e3-data = { workspace = true } e3-fhe-params = { workspace = true } e3-trbfv = { workspace = true } @@ -20,6 +21,7 @@ e3-zk-helpers = { workspace = true } e3-utils = { workspace = true } e3-zk-prover = { workspace = true } fhe = { workspace = true } +fhe-math = { workspace = true } fhe-traits = { workspace = true } ndarray = { workspace = true } num-bigint = { workspace = true } diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index 16caa2ff10..c16fe63a4d 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -24,15 +24,17 @@ use e3_events::EType; use e3_events::EffectsEnabled; use e3_events::{ BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeRequestKind, - ComputeResponse, DkgShareDecryptionProofRequest, DkgShareDecryptionProofResponse, EnclaveEvent, - EnclaveEventData, EventPublisher, EventSubscriber, EventType, PartyVerificationResult, + ComputeResponse, DecryptedSharesAggregationProofRequest, + DecryptedSharesAggregationProofResponse, DkgShareDecryptionProofRequest, + DkgShareDecryptionProofResponse, EnclaveEvent, EnclaveEventData, EventPublisher, + EventSubscriber, EventType, PartyC6VerificationResult, PartyVerificationResult, PkAggregationProofRequest, PkAggregationProofResponse, PkBfvProofRequest, PkBfvProofResponse, PkGenerationProofRequest, PkGenerationProofResponse, ShareComputationProofRequest, ShareComputationProofResponse, ShareEncryptionProofRequest, ShareEncryptionProofResponse, ThresholdShareDecryptionProofRequest, ThresholdShareDecryptionProofResponse, TypedEvent, - VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, - VerifyShareProofsRequest, VerifyShareProofsResponse, ZkError as ZkEventError, ZkRequest, - ZkResponse, + VerifyC6ProofsRequest, VerifyC6ProofsResponse, VerifyShareDecryptionProofsRequest, + VerifyShareDecryptionProofsResponse, VerifyShareProofsRequest, VerifyShareProofsResponse, + ZkError as ZkEventError, ZkRequest, ZkResponse, }; use e3_fhe_params::build_pair_for_preset; use e3_fhe_params::{BfvParamSet, BfvPreset}; @@ -50,6 +52,9 @@ use e3_utils::SharedRng; use e3_utils::MAILBOX_LIMIT; use e3_zk_helpers::circuits::dkg::pk::circuit::{PkCircuit, PkCircuitData}; use e3_zk_helpers::circuits::dkg::share_computation::utils::compute_parity_matrix; +use e3_zk_helpers::circuits::threshold::decrypted_shares_aggregation::circuit::{ + DecryptedSharesAggregationCircuit, DecryptedSharesAggregationCircuitData, +}; use e3_zk_helpers::circuits::threshold::pk_generation::circuit::{ PkGenerationCircuit, PkGenerationCircuitData, }; @@ -360,9 +365,12 @@ fn handle_threshold_share_decryption_proof( make_zk_error(&request, format!("ciphertext[{}] deserialize: {:?}", i, e)) })?; - // Decrypt es_poly_sum[i] → Poly → CrtPolynomial (e) + // Decrypt es_poly_sum → Poly → CrtPolynomial (e) + // Currently there is a single smudging noise polynomial shared across all + // ciphertexts (see calculate_decryption_share.rs). + let es_idx = i % req.es_poly_sum.len(); let e_poly = e3_trbfv::helpers::try_poly_from_sensitive_bytes( - req.es_poly_sum[i].clone(), + req.es_poly_sum[es_idx].clone(), threshold_params.clone(), cipher, ) @@ -588,6 +596,14 @@ fn handle_zk_request( handle_threshold_share_decryption_proof(&prover, &cipher, req, request.clone()) }) } + ZkRequest::VerifyC6Proofs(req) => timefunc("zk_verify_c6_proofs", id, || { + handle_verify_c6_proofs(&prover, req, request.clone()) + }), + ZkRequest::DecryptedSharesAggregation(req) => { + timefunc("zk_decrypted_shares_aggregation", id, || { + handle_decrypted_shares_aggregation_proof(&prover, req, request.clone()) + }) + } } } @@ -1121,3 +1137,123 @@ fn handle_verify_share_decryption_proofs( request.e3_id, )) } + +fn handle_verify_c6_proofs( + prover: &ZkProver, + req: VerifyC6ProofsRequest, + request: ComputeRequest, +) -> Result { + let e3_id_str = request.e3_id.to_string(); + + let party_results: Vec = req + .party_proofs + .into_iter() + .map(|party| { + let sender = party.sender_party_id; + + for c6_proof in &party.c6_proofs { + let result = prover.verify_proof(c6_proof, &e3_id_str, sender); + match result { + Ok(true) => continue, + Ok(false) | Err(_) => { + return PartyC6VerificationResult { + sender_party_id: sender, + all_verified: false, + }; + } + } + } + + PartyC6VerificationResult { + sender_party_id: sender, + all_verified: true, + } + }) + .collect(); + + Ok(ComputeResponse::zk( + ZkResponse::VerifyC6Proofs(VerifyC6ProofsResponse { party_results }), + request.correlation_id, + request.e3_id, + )) +} + +fn handle_decrypted_shares_aggregation_proof( + prover: &ZkProver, + mut req: DecryptedSharesAggregationProofRequest, + request: ComputeRequest, +) -> Result { + // 1. Build threshold BFV parameters from preset + let (threshold_params, _dkg_params) = build_pair_for_preset(req.params_preset.clone()) + .map_err(|e| make_zk_error(&request, format!("build_pair_for_preset: {}", e)))?; + + // 2. Sort d_share_polys by party ID — the Noir circuit requires + // party_ids in strictly increasing order for Lagrange sign computation. + req.d_share_polys.sort_by_key(|(id, _)| *id); + + // 3. Determine dimensions + let num_indices = req.plaintext.len(); + let num_parties = req.d_share_polys.len(); + let mut proofs = Vec::with_capacity(num_indices); + + // 4. For each ciphertext index, build circuit data and generate proof + for i in 0..num_indices { + // a. Extract per-party shares for index i, deserialize to Poly + let d_share_polys: Vec = req + .d_share_polys + .iter() + .map(|(_, shares)| try_poly_from_bytes(&shares[i], &threshold_params)) + .collect::>() + .map_err(|e| { + make_zk_error(&request, format!("d_share_polys[{}] deserialize: {}", i, e)) + })?; + + // b. Get party IDs (convert 0-based to 1-based for circuit) + let reconstructing_parties: Vec = req + .d_share_polys + .iter() + .map(|(id, _)| (*id as usize) + 1) + .collect(); + + // c. Decode plaintext at index i to Vec + let message_vec = e3_bfv_client::decode_bytes_to_vec_u64(&req.plaintext[i].extract_bytes()) + .map_err(|e| make_zk_error(&request, format!("plaintext[{}] decode: {:?}", i, e)))?; + + // d. Build committee + let committee = e3_zk_helpers::CiphernodesCommittee { + n: req.threshold_n as usize, + h: num_parties, + threshold: req.threshold_m as usize, + }; + + // e. Build circuit data and generate proof + let circuit_data = DecryptedSharesAggregationCircuitData { + committee, + d_share_polys, + reconstructing_parties, + message_vec, + }; + + let circuit = DecryptedSharesAggregationCircuit; + let e3_id_str = request.e3_id.to_string(); + let proof = circuit + .prove(prover, &req.params_preset, &circuit_data, &e3_id_str) + .map_err(|e| { + ComputeRequestError::new( + ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(format!( + "C7 proof[{}]: {}", + i, e + ))), + request.clone(), + ) + })?; + proofs.push(proof); + } + + // 4. Return response + Ok(ComputeResponse::zk( + ZkResponse::DecryptedSharesAggregation(DecryptedSharesAggregationProofResponse { proofs }), + request.correlation_id, + request.e3_id, + )) +} diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index dab3cb06b9..358f59d784 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -222,8 +222,44 @@ async fn setup_test_zk_backend() -> (ZkBackend, tempfile::TempDir) { .await .unwrap(); - let backend = ZkBackend::new(BBPath::Default(bb_binary), circuits_dir, work_dir); + // Copy C6 (threshold/share_decryption) circuit for C6 verification + let threshold_share_dec_circuit_dir = + circuits_dir.join("threshold").join("share_decryption"); + tokio::fs::create_dir_all(&threshold_share_dec_circuit_dir) + .await + .unwrap(); + tokio::fs::copy( + threshold_target.join("share_decryption.json"), + threshold_share_dec_circuit_dir.join("share_decryption.json"), + ) + .await + .unwrap(); + tokio::fs::copy( + threshold_target.join("share_decryption.vk"), + threshold_share_dec_circuit_dir.join("share_decryption.vk"), + ) + .await + .unwrap(); + // Copy C7 (decrypted_shares_aggregation_mod) circuit + let dsa_circuit_dir = circuits_dir + .join("threshold") + .join("decrypted_shares_aggregation_mod"); + tokio::fs::create_dir_all(&dsa_circuit_dir).await.unwrap(); + tokio::fs::copy( + threshold_target.join("decrypted_shares_aggregation_mod.json"), + dsa_circuit_dir.join("decrypted_shares_aggregation_mod.json"), + ) + .await + .unwrap(); + tokio::fs::copy( + threshold_target.join("decrypted_shares_aggregation_mod.vk"), + dsa_circuit_dir.join("decrypted_shares_aggregation_mod.vk"), + ) + .await + .unwrap(); + + let backend = ZkBackend::new(BBPath::Default(bb_binary), circuits_dir, work_dir); (backend, temp) } else { println!("bb binary not found locally, downloading via ensure_installed()..."); @@ -719,13 +755,17 @@ async fn test_trbfv_actor() -> Result<()> { // The collector sees: // - 1 CiphertextOutputPublished (from shared bus) // - 5 DecryptionshareCreated (from simulate_libp2p, passes is_forwardable_event) - // - 1 ComputeRequest (PlaintextAggregation on node 0's own bus) - // - 1 ComputeResponse (PlaintextAggregation on node 0's own bus) - // - 1 PlaintextAggregated (from node 0's own aggregation actor) + // - 1 ComputeRequest (VerifyC6Proofs) + // - 1 ComputeResponse (VerifyC6Proofs) + // - 1 ComputeRequest (TrBFV CalculateThresholdDecryption) + // - 1 ComputeResponse (TrBFV CalculateThresholdDecryption) + // - 1 ComputeRequest (DecryptedSharesAggregation C7) + // - 1 ComputeResponse (DecryptedSharesAggregation C7) + // - 1 PlaintextAggregated (with C7 proofs) // Internal events from committee nodes (ComputeRequest/Response for CalculateDecryptionShare) // stay on their local buses. - // Total: 1 + 5 + 1 + 1 + 1 = 9 events - let expected_count = 1 + 5 + 1 + 1 + 1; + // Total: 1 + 5 + 2 + 2 + 2 + 1 = 13 events + let expected_count = 1 + 5 + 2 + 2 + 2 + 1; let h = nodes .take_history_with_timeout(0, expected_count, Duration::from_secs(1000)) @@ -738,6 +778,7 @@ async fn test_trbfv_actor() -> Result<()> { let Some(EnclaveEventData::PlaintextAggregated(PlaintextAggregated { decrypted_output: plaintext, + aggregation_proofs, .. })) = h.last().map(|e| e.get_data()) else { @@ -747,6 +788,11 @@ async fn test_trbfv_actor() -> Result<()> { ) }; + assert!( + !aggregation_proofs.is_empty(), + "C7 proofs should be present in PlaintextAggregated" + ); + let results = plaintext .into_iter() .map(|a| decode_bytes_to_vec_u64(&a.extract_bytes()).expect("error decoding bytes")) @@ -835,11 +881,13 @@ async fn test_p2p_actor_forwards_events_to_network() -> Result<()> { let evt_1 = PlaintextAggregated { e3_id: E3id::new("1235", 1), decrypted_output: vec![ArcBytes::from_bytes(&[1, 2, 3, 4])], + aggregation_proofs: vec![], }; let evt_2 = PlaintextAggregated { e3_id: E3id::new("1236", 1), decrypted_output: vec![ArcBytes::from_bytes(&[1, 2, 3, 4])], + aggregation_proofs: vec![], }; let local_evt_3 = CiphernodeSelected { diff --git a/templates/default/deployed_contracts.json b/templates/default/deployed_contracts.json index 1f39f056e7..f2324ab17f 100644 --- a/templates/default/deployed_contracts.json +++ b/templates/default/deployed_contracts.json @@ -21,21 +21,21 @@ }, "localhost": { "PoseidonT3": { - "blockNumber": 9, + "blockNumber": 17, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 10, + "blockNumber": 18, "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" }, "EnclaveToken": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 11, + "blockNumber": 20, "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" }, "EnclaveTicketToken": { @@ -44,14 +44,14 @@ "registry": "0x0000000000000000000000000000000000000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 13, + "blockNumber": 22, "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" }, "SlashingManager": { "constructorArgs": { "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 14, + "blockNumber": 23, "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" }, "CiphernodeRegistryOwnable": { @@ -88,7 +88,7 @@ "proxyAdminAddress": "0x8aCd85898458400f7Db866d53FCFF6f0D49741FF", "implementationAddress": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, - "blockNumber": 16, + "blockNumber": 25, "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" }, "Enclave": { @@ -111,7 +111,7 @@ "proxyAdminAddress": "0x8dAF17A20c9DBA35f005b6324F493785D239719d", "implementationAddress": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" }, - "blockNumber": 19, + "blockNumber": 28, "address": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" }, "E3RefundManager": { @@ -127,28 +127,28 @@ "proxyAdminAddress": "0x32467b43BFa67273FC7dDda0999Ee9A12F2AaA08", "implementationAddress": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" }, - "blockNumber": 21, + "blockNumber": 30, "address": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" }, "MockComputeProvider": { - "blockNumber": 25, + "blockNumber": 32, "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d" }, "MockDecryptionVerifier": { - "blockNumber": 26, + "blockNumber": 33, "address": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" }, "MockE3Program": { - "blockNumber": 27, + "blockNumber": 34, "address": "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E" }, "ImageID": { "address": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9", - "blockNumber": 31 + "blockNumber": 38 }, "MyProgram": { "address": "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8", - "blockNumber": 33 + "blockNumber": 40 } } } \ No newline at end of file diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index d80e68a3a7..ea704437a6 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 = 225 + const duration = 300 const inputWindow = await calculateInputWindow(publicClient, duration) const thresholdBfvParams = await sdk.getThresholdBfvParamsSet() const e3ProgramParams = encodeBfvParams(thresholdBfvParams) From 6ec98dde9c609dedcd4b577d55b27b22ceec53e3 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:36:47 +0000 Subject: [PATCH 4/9] chore: pr comments --- .../src/threshold_plaintext_aggregator.rs | 44 +++++++++-------- crates/keyshare/src/threshold_keyshare.rs | 47 ++++++++++++------- crates/multithread/src/multithread.rs | 35 ++++++++++++++ templates/default/deployed_contracts.json | 28 +++++------ templates/default/enclave.config.yaml | 14 +++--- 5 files changed, 107 insertions(+), 61 deletions(-) diff --git a/crates/aggregator/src/threshold_plaintext_aggregator.rs b/crates/aggregator/src/threshold_plaintext_aggregator.rs index 063ef3c07d..294f43e51b 100644 --- a/crates/aggregator/src/threshold_plaintext_aggregator.rs +++ b/crates/aggregator/src/threshold_plaintext_aggregator.rs @@ -42,6 +42,7 @@ pub struct VerifyingC6 { threshold_m: u64, threshold_n: u64, shares: HashMap>, + c6_proofs: HashMap>, ciphertext_output: Vec, params: ArcBytes, } @@ -234,6 +235,7 @@ impl ThresholdPlaintextAggregator { Ok(ThresholdPlaintextAggregatorState::VerifyingC6( VerifyingC6 { shares, + c6_proofs, ciphertext_output, threshold_m, threshold_n, @@ -302,19 +304,19 @@ impl ThresholdPlaintextAggregator { ); } - ensure!( - honest_party_ids.len() > state.threshold_m as usize, - "Not enough honest parties after C6 verification: {} honest, {} required", - honest_party_ids.len(), - state.threshold_m + 1 - ); - // Filter shares to only honest parties let honest_shares: Vec<(u64, Vec)> = honest_party_ids .iter() .filter_map(|id| state.shares.get(id).map(|s| (*id, s.clone()))) .collect(); + ensure!( + honest_shares.len() > state.threshold_m as usize, + "Not enough honest shares after C6 verification: {} honest shares, {} required", + honest_shares.len(), + state.threshold_m + 1 + ); + info!( "C6 verification passed: {} honest parties, transitioning to Computing", honest_shares.len() @@ -403,6 +405,13 @@ impl ThresholdPlaintextAggregator { let plaintext = state.plaintext.clone(); let shares = state.shares.clone(); + ensure!( + data.proofs.len() == plaintext.len(), + "C7 proof count mismatch: got {} proofs for {} ciphertext indices", + data.proofs.len(), + plaintext.len() + ); + self.state.try_mutate(ec, |_| { Ok(ThresholdPlaintextAggregatorState::Complete(Complete { decrypted: plaintext.clone(), @@ -590,25 +599,14 @@ impl Handler>> ec, ) = msg.into_inner().into_components(); - // Collect c6_proofs before add_share mutates state - let c6_proofs_snapshot = - if let Some(ThresholdPlaintextAggregatorState::Collecting(ref collecting)) = - self.state.get() - { - let mut proofs = collecting.c6_proofs.clone(); - proofs.insert(party_id, decryption_proofs.clone()); - Some(proofs) - } else { - None - }; - self.add_share(party_id, decryption_share, decryption_proofs, &ec)?; // If we transitioned to VerifyingC6, dispatch C6 verification - if let Some(ThresholdPlaintextAggregatorState::VerifyingC6(_)) = self.state.get() { - if let Some(proofs) = c6_proofs_snapshot { - self.dispatch_c6_verification(proofs, ec)?; - } + // using the proofs persisted in state + if let Some(ThresholdPlaintextAggregatorState::VerifyingC6(ref state)) = + self.state.get() + { + self.dispatch_c6_verification(state.c6_proofs.clone(), ec)?; } Ok(()) diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 58d094177e..bc7627fd48 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -251,6 +251,10 @@ pub struct ThresholdKeyshareState { pub threshold_m: u64, pub threshold_n: u64, pub params: ArcBytes, + /// Aggregated public key bytes, captured from PublicKeyAggregated event for C6 proof. + /// Defaults to None for backward compatibility with existing persisted state. + #[serde(default)] + pub aggregated_pk: Option, } impl ThresholdKeyshareState { @@ -271,6 +275,7 @@ impl ThresholdKeyshareState { threshold_m, threshold_n, params, + aggregated_pk: None, } } @@ -1979,6 +1984,7 @@ impl ThresholdKeyshare { let aggregated_pk_bytes = self .aggregated_pk .clone() + .or_else(|| state.aggregated_pk.clone()) .ok_or_else(|| anyhow!("Aggregated public key not available for C6 proof"))?; let threshold_preset = self @@ -1994,12 +2000,29 @@ impl ThresholdKeyshare { info!("Dispatching C6 proof generation (threshold share decryption)..."); + // Publish C6 proof request before transitioning state so a publish + // failure leaves us in Decrypting (retryable) rather than + // GeneratingDecryptionProof (no retry path). + let request = ComputeRequest::zk( + ZkRequest::ThresholdShareDecryption(ThresholdShareDecryptionProofRequest { + ciphertext_bytes: decrypting.ciphertext_output, + aggregated_pk_bytes, + sk_poly_sum: decrypting.sk_poly_sum, + es_poly_sum: decrypting.es_poly_sum, + d_share_bytes: d_share_poly.clone(), + params_preset: threshold_preset, + }), + CorrelationId::new(), + e3_id, + ); + self.bus.publish(request, ec.clone())?; + // Transition to GeneratingDecryptionProof state self.state.try_mutate(&ec, |s| { use KeyshareState as K; s.new_state(K::GeneratingDecryptionProof(GeneratingDecryptionProof { pk_share: decrypting.pk_share.clone(), - decryption_share: d_share_poly.clone(), + decryption_share: d_share_poly, signed_pk_generation_proof: decrypting.signed_pk_generation_proof.clone(), signed_sk_share_computation_proof: decrypting .signed_sk_share_computation_proof @@ -2015,21 +2038,6 @@ impl ThresholdKeyshare { .clone(), })) })?; - - // Dispatch C6 proof request - let request = ComputeRequest::zk( - ZkRequest::ThresholdShareDecryption(ThresholdShareDecryptionProofRequest { - ciphertext_bytes: decrypting.ciphertext_output, - aggregated_pk_bytes, - sk_poly_sum: decrypting.sk_poly_sum, - es_poly_sum: decrypting.es_poly_sum, - d_share_bytes: d_share_poly, - params_preset: threshold_preset, - }), - CorrelationId::new(), - e3_id, - ); - self.bus.publish(request, ec)?; Ok(()) } @@ -2087,7 +2095,12 @@ impl Handler for ThresholdKeyshare { self.notify_sync(ctx, TypedEvent::new(data, ec)) } EnclaveEventData::PublicKeyAggregated(data) => { - self.aggregated_pk = Some(ArcBytes::from_bytes(&data.pubkey)); + let pk = ArcBytes::from_bytes(&data.pubkey); + self.aggregated_pk = Some(pk.clone()); + let _ = self.state.try_mutate(&ec, |mut s| { + s.aggregated_pk = Some(pk); + Ok(s) + }); } EnclaveEventData::ThresholdShareCreated(data) => { let _ = diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index c16fe63a4d..f28cdd1b91 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -356,6 +356,19 @@ fn handle_threshold_share_decryption_proof( // 4. For each index, build circuit data and generate proof let num_indices = req.ciphertext_bytes.len(); + if req.es_poly_sum.is_empty() { + return Err(make_zk_error(&request, "empty es_poly_sum".to_string())); + } + if req.d_share_bytes.len() < num_indices { + return Err(make_zk_error( + &request, + format!( + "d_share_bytes too short: {} < {}", + req.d_share_bytes.len(), + num_indices + ), + )); + } let mut proofs = Vec::with_capacity(num_indices); for i in 0..num_indices { @@ -1151,6 +1164,13 @@ fn handle_verify_c6_proofs( .map(|party| { let sender = party.sender_party_id; + if party.c6_proofs.is_empty() { + return PartyC6VerificationResult { + sender_party_id: sender, + all_verified: false, + }; + } + for c6_proof in &party.c6_proofs { let result = prover.verify_proof(c6_proof, &e3_id_str, sender); match result { @@ -1194,6 +1214,21 @@ fn handle_decrypted_shares_aggregation_proof( // 3. Determine dimensions let num_indices = req.plaintext.len(); let num_parties = req.d_share_polys.len(); + + for (party_id, shares) in &req.d_share_polys { + if shares.len() < num_indices { + return Err(make_zk_error( + &request, + format!( + "party {} has {} shares but {} expected", + party_id, + shares.len(), + num_indices + ), + )); + } + } + let mut proofs = Vec::with_capacity(num_indices); // 4. For each ciphertext index, build circuit data and generate proof diff --git a/templates/default/deployed_contracts.json b/templates/default/deployed_contracts.json index f2324ab17f..dbb0bb6664 100644 --- a/templates/default/deployed_contracts.json +++ b/templates/default/deployed_contracts.json @@ -21,21 +21,21 @@ }, "localhost": { "PoseidonT3": { - "blockNumber": 17, + "blockNumber": 6, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 18, + "blockNumber": 7, "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" }, "EnclaveToken": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 20, + "blockNumber": 8, "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" }, "EnclaveTicketToken": { @@ -44,14 +44,14 @@ "registry": "0x0000000000000000000000000000000000000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 22, + "blockNumber": 10, "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" }, "SlashingManager": { "constructorArgs": { "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 23, + "blockNumber": 11, "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" }, "CiphernodeRegistryOwnable": { @@ -66,7 +66,7 @@ "proxyAdminAddress": "0x9bd03768a7DCc129555dE410FF8E85528A4F88b5", "implementationAddress": "0x0165878A594ca255338adfa4d48449f69242Eb8F" }, - "blockNumber": 15, + "blockNumber": 12, "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" }, "BondingRegistry": { @@ -88,7 +88,7 @@ "proxyAdminAddress": "0x8aCd85898458400f7Db866d53FCFF6f0D49741FF", "implementationAddress": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, - "blockNumber": 25, + "blockNumber": 13, "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" }, "Enclave": { @@ -111,7 +111,7 @@ "proxyAdminAddress": "0x8dAF17A20c9DBA35f005b6324F493785D239719d", "implementationAddress": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" }, - "blockNumber": 28, + "blockNumber": 16, "address": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" }, "E3RefundManager": { @@ -127,28 +127,28 @@ "proxyAdminAddress": "0x32467b43BFa67273FC7dDda0999Ee9A12F2AaA08", "implementationAddress": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" }, - "blockNumber": 30, + "blockNumber": 18, "address": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" }, "MockComputeProvider": { - "blockNumber": 32, + "blockNumber": 20, "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d" }, "MockDecryptionVerifier": { - "blockNumber": 33, + "blockNumber": 21, "address": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" }, "MockE3Program": { - "blockNumber": 34, + "blockNumber": 22, "address": "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E" }, "ImageID": { "address": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9", - "blockNumber": 38 + "blockNumber": 26 }, "MyProgram": { "address": "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8", - "blockNumber": 40 + "blockNumber": 28 } } } \ No newline at end of file diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index 05cceece53..80234fb404 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -4,19 +4,19 @@ chains: contracts: enclave: address: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" - deploy_block: 19 + deploy_block: 16 ciphernode_registry: - address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" - deploy_block: 14 - bonding_registry: address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - deploy_block: 15 + deploy_block: 12 + bonding_registry: + address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + deploy_block: 13 fee_token: address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" - deploy_block: 10 + deploy_block: 7 e3_program: address: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" - deploy_block: 33 + deploy_block: 28 program: dev: true nodes: From 4f7d97826b0f5e337e8b1f15ec1e023ec27645d1 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:17:19 +0000 Subject: [PATCH 5/9] refactor: use verification actor --- crates/aggregator/src/publickey_aggregator.rs | 151 +++--- .../src/threshold_plaintext_aggregator.rs | 246 +++++---- .../src/ciphernode_builder.rs | 12 +- .../aggregation_proof_pending.rs | 25 + .../enclave_event/aggregation_proof_signed.rs | 21 + .../src/enclave_event/compute_request/mod.rs | 1 - .../src/enclave_event/compute_request/zk.rs | 38 -- .../decryption_share_proof_signed.rs | 13 + .../enclave_event/decryptionshare_created.rs | 6 +- crates/events/src/enclave_event/mod.rs | 32 +- .../pk_aggregation_proof_pending.rs | 25 + .../pk_aggregation_proof_signed.rs | 21 + .../share_decryption_proof_pending.rs | 31 ++ .../src/enclave_event/share_verification.rs | 4 + .../events/src/enclave_event/signed_proof.rs | 4 + crates/keyshare/src/threshold_keyshare.rs | 109 ++-- crates/multithread/src/multithread.rs | 64 +-- crates/tests/tests/integration.rs | 32 +- crates/zk-prover/src/actors/proof_request.rs | 477 ++++++++++++++++-- .../src/actors/share_verification.rs | 49 +- 20 files changed, 952 insertions(+), 409 deletions(-) create mode 100644 crates/events/src/enclave_event/aggregation_proof_pending.rs create mode 100644 crates/events/src/enclave_event/aggregation_proof_signed.rs create mode 100644 crates/events/src/enclave_event/decryption_share_proof_signed.rs create mode 100644 crates/events/src/enclave_event/pk_aggregation_proof_pending.rs create mode 100644 crates/events/src/enclave_event/pk_aggregation_proof_signed.rs create mode 100644 crates/events/src/enclave_event/share_decryption_proof_pending.rs diff --git a/crates/aggregator/src/publickey_aggregator.rs b/crates/aggregator/src/publickey_aggregator.rs index dffe6bad5d..6acbbc7007 100644 --- a/crates/aggregator/src/publickey_aggregator.rs +++ b/crates/aggregator/src/publickey_aggregator.rs @@ -9,11 +9,11 @@ use anyhow::Result; use e3_bfv_client::client::compute_pk_commitment; use e3_data::Persistable; use e3_events::{ - prelude::*, BusHandle, ComputeRequest, ComputeResponse, ComputeResponseKind, CorrelationId, - Die, E3id, EnclaveEvent, EnclaveEventData, EventContext, KeyshareCreated, OrderedSet, - PartyProofsToVerify, PkAggregationProofRequest, PkAggregationProofResponse, - PublicKeyAggregated, Seed, Sequenced, SignedProofPayload, TypedEvent, VerifyShareProofsRequest, - VerifyShareProofsResponse, ZkRequest, ZkResponse, + prelude::*, BusHandle, Die, E3id, EnclaveEvent, EnclaveEventData, EventContext, + KeyshareCreated, OrderedSet, PartyProofsToVerify, PkAggregationProofPending, + PkAggregationProofRequest, PkAggregationProofSigned, PublicKeyAggregated, Seed, Sequenced, + ShareVerificationComplete, ShareVerificationDispatched, SignedProofPayload, TypedEvent, + VerificationKind, }; use e3_events::{trap, EType}; use e3_fhe::{Fhe, GetAggregatePublicKey}; @@ -195,47 +195,45 @@ impl PublicKeyAggregator { no_proof_parties.len() ); - let request = ComputeRequest::zk( - ZkRequest::VerifyShareProofs(VerifyShareProofsRequest { party_proofs }), - CorrelationId::new(), - self.e3_id.clone(), - ); - self.bus.publish(request, ec)?; + self.bus.publish( + ShareVerificationDispatched { + e3_id: self.e3_id.clone(), + kind: VerificationKind::PkGenerationProofs, + share_proofs: party_proofs, + decryption_proofs: vec![], + pre_dishonest: no_proof_parties.into_iter().collect(), + }, + ec, + )?; Ok(()) } - fn handle_c1_verification_response( + fn handle_c1_verification_complete( &mut self, - response: VerifyShareProofsResponse, - ec: EventContext, + msg: TypedEvent, ) -> Result<()> { + let (msg, ec) = msg.into_components(); + + if msg.kind != VerificationKind::PkGenerationProofs { + return Ok(()); + } + let PublicKeyAggregatorState::VerifyingC1 { keyshares, nodes, threshold_m, - no_proof_parties, + .. } = self .state .get() .ok_or_else(|| anyhow::anyhow!("Expected VerifyingC1 state"))? else { return Err(anyhow::anyhow!( - "handle_c1_verification_response called outside VerifyingC1 state" + "handle_c1_verification_complete called outside VerifyingC1 state" )); }; - // Partition honest/dishonest parties — include those that failed verification - // AND those that submitted no C1 proof at all - let mut dishonest_parties: Vec = no_proof_parties; - for result in &response.party_results { - if !result.all_verified { - warn!( - "Party {} failed C1 proof verification (failed payload: {:?})", - result.sender_party_id, result.failed_signed_payload - ); - dishonest_parties.push(result.sender_party_id); - } - } + let dishonest_parties = &msg.dishonest_parties; // Filter out dishonest parties from keyshares and nodes let keyshares_vec: Vec = keyshares.into_iter().collect(); @@ -295,31 +293,36 @@ impl PublicKeyAggregator { }) })?; - // Dispatch C5 proof request + // Dispatch C5 proof generation through ProofRequestActor let committee_h = honest_keyshares.len(); - info!("Dispatching C5 proof generation (pk aggregation)..."); - let request = ComputeRequest::zk( - ZkRequest::PkAggregation(PkAggregationProofRequest { - keyshare_bytes: honest_keyshares, - aggregated_pk_bytes: ArcBytes::from_bytes(&pubkey), - params_preset: self.params_preset.clone(), - committee_n: committee_h, // N = all parties in this aggregation - committee_h, - committee_threshold: 0, // Will be resolved from preset in handler - }), - CorrelationId::new(), - self.e3_id.clone(), - ); - self.bus.publish(request, ec)?; + info!("Publishing PkAggregationProofPending for C5 proof generation..."); + self.bus.publish( + PkAggregationProofPending { + e3_id: self.e3_id.clone(), + proof_request: PkAggregationProofRequest { + keyshare_bytes: honest_keyshares, + aggregated_pk_bytes: ArcBytes::from_bytes(&pubkey), + params_preset: self.params_preset.clone(), + committee_n: committee_h, + committee_h, + committee_threshold: 0, + }, + public_key: pubkey, + public_key_hash, + nodes: honest_nodes_set, + }, + ec, + )?; Ok(()) } - fn handle_c5_proof_response( + fn handle_pk_aggregation_proof_signed( &mut self, - response: PkAggregationProofResponse, - ec: EventContext, + msg: TypedEvent, ) -> Result<()> { + let (msg, ec) = msg.into_components(); + let PublicKeyAggregatorState::GeneratingC5Proof { public_key, public_key_hash, @@ -331,13 +334,13 @@ impl PublicKeyAggregator { .ok_or_else(|| anyhow::anyhow!("Expected GeneratingC5Proof state"))? else { return Err(anyhow::anyhow!( - "handle_c5_proof_response called outside GeneratingC5Proof state" + "handle_pk_aggregation_proof_signed called outside GeneratingC5Proof state" )); }; - info!("C5 proof generated, publishing PublicKeyAggregated..."); + info!("C5 proof signed, publishing PublicKeyAggregated..."); - let proof = response.proof; + let proof = msg.signed_proof.payload.proof; // Transition to Complete self.state.try_mutate(&ec, |_| { @@ -359,22 +362,6 @@ impl PublicKeyAggregator { self.bus.publish(event, ec)?; Ok(()) } - - fn handle_compute_response( - &mut self, - response: ComputeResponse, - ec: EventContext, - ) -> Result<()> { - match response.response { - ComputeResponseKind::Zk(ZkResponse::VerifyShareProofs(data)) => { - self.handle_c1_verification_response(data, ec) - } - ComputeResponseKind::Zk(ZkResponse::PkAggregation(data)) => { - self.handle_c5_proof_response(data, ec) - } - _ => Ok(()), // Ignore other compute responses - } - } } impl Actor for PublicKeyAggregator { @@ -392,7 +379,10 @@ impl Handler for PublicKeyAggregator { EnclaveEventData::KeyshareCreated(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } - EnclaveEventData::ComputeResponse(data) => { + EnclaveEventData::ShareVerificationComplete(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::PkAggregationProofSigned(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } EnclaveEventData::ComputeRequestError(data) => { @@ -451,18 +441,35 @@ impl Handler> for PublicKeyAggregator { } } -impl Handler> for PublicKeyAggregator { +impl Handler> for PublicKeyAggregator { type Result = (); fn handle( &mut self, - msg: TypedEvent, + msg: TypedEvent, _ctx: &mut Self::Context, ) -> Self::Result { - let (msg, ec) = msg.into_components(); - trap(EType::PublickeyAggregation, &self.bus.with_ec(&ec), || { - self.handle_compute_response(msg, ec) - }) + trap( + EType::PublickeyAggregation, + &self.bus.with_ec(msg.get_ctx()), + || self.handle_c1_verification_complete(msg), + ) + } +} + +impl Handler> for PublicKeyAggregator { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + trap( + EType::PublickeyAggregation, + &self.bus.with_ec(msg.get_ctx()), + || self.handle_pk_aggregation_proof_signed(msg), + ) } } diff --git a/crates/aggregator/src/threshold_plaintext_aggregator.rs b/crates/aggregator/src/threshold_plaintext_aggregator.rs index 294f43e51b..4575554371 100644 --- a/crates/aggregator/src/threshold_plaintext_aggregator.rs +++ b/crates/aggregator/src/threshold_plaintext_aggregator.rs @@ -4,17 +4,17 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use actix::prelude::*; use anyhow::{anyhow, bail, ensure, Result}; use e3_data::Persistable; use e3_events::{ - prelude::*, trap, BusHandle, ComputeRequest, ComputeResponse, ComputeResponseKind, - CorrelationId, DecryptedSharesAggregationProofRequest, DecryptedSharesAggregationProofResponse, + prelude::*, trap, AggregationProofPending, AggregationProofSigned, BusHandle, ComputeRequest, + ComputeResponse, ComputeResponseKind, CorrelationId, DecryptedSharesAggregationProofRequest, DecryptionshareCreated, Die, E3id, EType, EnclaveEvent, EnclaveEventData, EventContext, - PartyC6ProofsToVerify, PlaintextAggregated, Proof, Seed, Sequenced, TypedEvent, - VerifyC6ProofsRequest, VerifyC6ProofsResponse, ZkRequest, ZkResponse, + PartyProofsToVerify, PlaintextAggregated, Seed, Sequenced, ShareVerificationComplete, + ShareVerificationDispatched, SignedProofPayload, TypedEvent, VerificationKind, }; use e3_fhe_params::BfvPreset; use e3_sortition::{E3CommitteeContainsRequest, E3CommitteeContainsResponse, Sortition}; @@ -31,7 +31,7 @@ pub struct Collecting { threshold_m: u64, threshold_n: u64, shares: HashMap>, - c6_proofs: HashMap>, + c6_proofs: HashMap>, seed: Seed, ciphertext_output: Vec, params: ArcBytes, @@ -42,7 +42,7 @@ pub struct VerifyingC6 { threshold_m: u64, threshold_n: u64, shares: HashMap>, - c6_proofs: HashMap>, + c6_proofs: HashMap>, ciphertext_output: Vec, params: ArcBytes, } @@ -201,7 +201,7 @@ impl ThresholdPlaintextAggregator { &mut self, party_id: u64, share: Vec, - decryption_proofs: Vec, + decryption_proofs: Vec, ec: &EventContext, ) -> Result<()> { self.state.try_mutate(ec, |state| { @@ -245,69 +245,65 @@ impl ThresholdPlaintextAggregator { }) } - /// Dispatch a C6 proof verification request for all collected parties. + /// Dispatch C6 proof verification through ShareVerificationActor. pub fn dispatch_c6_verification( &mut self, - c6_proofs: HashMap>, + c6_proofs: HashMap>, ec: EventContext, ) -> Result<()> { - let party_proofs: Vec = c6_proofs + let party_proofs: Vec = c6_proofs .into_iter() - .map(|(party_id, proofs)| PartyC6ProofsToVerify { + .map(|(party_id, signed_proofs)| PartyProofsToVerify { sender_party_id: party_id, - c6_proofs: proofs, + signed_proofs, }) .collect(); - let event = ComputeRequest::zk( - ZkRequest::VerifyC6Proofs(VerifyC6ProofsRequest { party_proofs }), - CorrelationId::new(), - self.e3_id.clone(), - ); - - self.bus.publish(event, ec)?; + self.bus.publish( + ShareVerificationDispatched { + e3_id: self.e3_id.clone(), + kind: VerificationKind::ThresholdDecryptionProofs, + share_proofs: party_proofs, + decryption_proofs: vec![], + pre_dishonest: BTreeSet::new(), + }, + ec, + )?; Ok(()) } - /// Handle C6 verification response: filter dishonest parties, transition to Computing. - pub fn handle_c6_verification_response( + /// Handle ShareVerificationComplete for C6: filter dishonest parties, transition to Computing. + pub fn handle_c6_verification_complete( &mut self, - data: VerifyC6ProofsResponse, - ec: &EventContext, + msg: TypedEvent, ) -> Result<()> { + let (msg, ec) = msg.into_components(); + + if msg.kind != VerificationKind::ThresholdDecryptionProofs { + return Ok(()); + } + let state: VerifyingC6 = self .state .get() .ok_or(anyhow!("Could not get state"))? .try_into()?; - // Determine honest parties - let honest_party_ids: Vec = data - .party_results - .iter() - .filter(|r| r.all_verified) - .map(|r| r.sender_party_id) - .collect(); - - let dishonest: Vec = data - .party_results - .iter() - .filter(|r| !r.all_verified) - .map(|r| r.sender_party_id) - .collect(); - - if !dishonest.is_empty() { + let dishonest_parties = &msg.dishonest_parties; + if !dishonest_parties.is_empty() { warn!( "C6 verification: {} dishonest parties filtered: {:?}", - dishonest.len(), - dishonest + dishonest_parties.len(), + dishonest_parties ); } // Filter shares to only honest parties - let honest_shares: Vec<(u64, Vec)> = honest_party_ids + let honest_shares: Vec<(u64, Vec)> = state + .shares .iter() - .filter_map(|id| state.shares.get(id).map(|s| (*id, s.clone()))) + .filter(|(id, _)| !dishonest_parties.contains(id)) + .map(|(id, s)| (*id, s.clone())) .collect(); ensure!( @@ -322,7 +318,7 @@ impl ThresholdPlaintextAggregator { honest_shares.len() ); - self.state.try_mutate(ec, |_| { + self.state.try_mutate(&ec, |_| { Ok(ThresholdPlaintextAggregatorState::Computing(Computing { shares: honest_shares.clone(), ciphertext_output: state.ciphertext_output.clone(), @@ -332,6 +328,33 @@ impl ThresholdPlaintextAggregator { })) })?; + // Dispatch TrBFV computation + let computing: Computing = self + .state + .get() + .ok_or(anyhow!("Could not get state after C6 verification"))? + .try_into()?; + + let trbfv_config = TrBFVConfig::new( + computing.params.clone(), + computing.threshold_n, + computing.threshold_m, + ); + + let event = ComputeRequest::trbfv( + TrBFVRequest::CalculateThresholdDecryption( + CalculateThresholdDecryptionRequest { + ciphertexts: computing.ciphertext_output.clone(), + trbfv_config, + d_share_polys: computing.shares.clone(), + } + .into(), + ), + CorrelationId::new(), + self.e3_id.clone(), + ); + self.bus.publish(event, ec)?; + Ok(()) } @@ -365,7 +388,7 @@ impl ThresholdPlaintextAggregator { Ok(()) } - /// Dispatch a C7 proof generation request. + /// Publish AggregationProofPending for C7 proof generation through ProofRequestActor. pub fn dispatch_c7_proof_request( &mut self, shares: Vec<(u64, Vec)>, @@ -374,28 +397,30 @@ impl ThresholdPlaintextAggregator { threshold_n: u64, ec: EventContext, ) -> Result<()> { - let event = ComputeRequest::zk( - ZkRequest::DecryptedSharesAggregation(DecryptedSharesAggregationProofRequest { - d_share_polys: shares, + self.bus.publish( + AggregationProofPending { + e3_id: self.e3_id.clone(), + proof_request: DecryptedSharesAggregationProofRequest { + d_share_polys: shares.clone(), + plaintext: plaintext.clone(), + params_preset: self.params_preset.clone(), + threshold_m, + threshold_n, + }, plaintext, - params_preset: self.params_preset.clone(), - threshold_m, - threshold_n, - }), - CorrelationId::new(), - self.e3_id.clone(), - ); - - self.bus.publish(event, ec)?; + shares, + }, + ec, + )?; Ok(()) } - /// Handle C7 proof response: transition to Complete and publish PlaintextAggregated. - pub fn handle_c7_proof_response( + /// Handle AggregationProofSigned: transition to Complete and publish PlaintextAggregated. + pub fn handle_aggregation_proof_signed( &mut self, - data: DecryptedSharesAggregationProofResponse, - ec: &EventContext, + msg: TypedEvent, ) -> Result<()> { + let (msg, ec) = msg.into_components(); let state: GeneratingC7Proof = self .state .get() @@ -405,14 +430,21 @@ impl ThresholdPlaintextAggregator { let plaintext = state.plaintext.clone(); let shares = state.shares.clone(); + // Extract raw proofs from signed payloads for PlaintextAggregated + let proofs: Vec<_> = msg + .signed_proofs + .iter() + .map(|sp| sp.payload.proof.clone()) + .collect(); + ensure!( - data.proofs.len() == plaintext.len(), + proofs.len() == plaintext.len(), "C7 proof count mismatch: got {} proofs for {} ciphertext indices", - data.proofs.len(), + proofs.len(), plaintext.len() ); - self.state.try_mutate(ec, |_| { + self.state.try_mutate(&ec, |_| { Ok(ThresholdPlaintextAggregatorState::Complete(Complete { decrypted: plaintext.clone(), shares: shares.clone(), @@ -423,11 +455,11 @@ impl ThresholdPlaintextAggregator { let event = PlaintextAggregated { decrypted_output: plaintext, e3_id: self.e3_id.clone(), - aggregation_proofs: data.proofs, + aggregation_proofs: proofs, }; info!("Dispatching plaintext event with C7 proofs {:?}", event); - self.bus.publish(event, ec.clone())?; + self.bus.publish(event, ec)?; Ok(()) } @@ -439,37 +471,7 @@ impl ThresholdPlaintextAggregator { ); match msg.response { - // C6 verification response - ComputeResponseKind::Zk(ZkResponse::VerifyC6Proofs(data)) => { - info!("Received C6 verification response"); - self.handle_c6_verification_response(data, &ec)?; - - // Now dispatch the TrBFV computation - let state: Computing = self - .state - .get() - .ok_or(anyhow!("Could not get state after C6 verification"))? - .try_into()?; - - let trbfv_config = - TrBFVConfig::new(state.params.clone(), state.threshold_n, state.threshold_m); - - let event = ComputeRequest::trbfv( - TrBFVRequest::CalculateThresholdDecryption( - CalculateThresholdDecryptionRequest { - ciphertexts: state.ciphertext_output.clone(), - trbfv_config, - d_share_polys: state.shares.clone(), - } - .into(), - ), - CorrelationId::new(), - self.e3_id.clone(), - ); - self.bus.publish(event, ec)?; - } - - // TrBFV threshold decryption response → transition to GeneratingC7Proof + // TrBFV threshold decryption response -> transition to GeneratingC7Proof ComputeResponseKind::TrBFV(TrBFVResponse::CalculateThresholdDecryption(response)) => { info!("Received TrBFV threshold decryption response"); let plaintext = response.plaintext; @@ -496,16 +498,10 @@ impl ThresholdPlaintextAggregator { )) })?; - // Dispatch C7 proof request + // Dispatch C7 proof request through ProofRequestActor self.dispatch_c7_proof_request(shares, plaintext, threshold_m, threshold_n, ec)?; } - // C7 proof response → Complete + publish - ComputeResponseKind::Zk(ZkResponse::DecryptedSharesAggregation(data)) => { - info!("Received C7 proof response"); - self.handle_c7_proof_response(data, &ec)?; - } - _ => { // Not a response we handle — ignore } @@ -532,6 +528,12 @@ impl Handler for ThresholdPlaintextAggregator { EnclaveEventData::ComputeResponse(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::ShareVerificationComplete(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::AggregationProofSigned(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } _ => (), } } @@ -593,13 +595,13 @@ impl Handler>> DecryptionshareCreated { party_id, decryption_share, - decryption_proofs, + signed_decryption_proofs, .. }, ec, ) = msg.into_inner().into_components(); - self.add_share(party_id, decryption_share, decryption_proofs, &ec)?; + self.add_share(party_id, decryption_share, signed_decryption_proofs, &ec)?; // If we transitioned to VerifyingC6, dispatch C6 verification // using the proofs persisted in state @@ -637,6 +639,36 @@ impl Handler> for ThresholdPlaintextAggregator { } } +impl Handler> for ThresholdPlaintextAggregator { + type Result = (); + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + trap( + EType::PlaintextAggregation, + &self.bus.with_ec(msg.get_ctx()), + || self.handle_c6_verification_complete(msg), + ) + } +} + +impl Handler> for ThresholdPlaintextAggregator { + type Result = (); + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + trap( + EType::PlaintextAggregation, + &self.bus.with_ec(msg.get_ctx()), + || self.handle_aggregation_proof_signed(msg), + ) + } +} + impl Handler for ThresholdPlaintextAggregator { type Result = (); fn handle(&mut self, _: Die, ctx: &mut Self::Context) -> Self::Result { diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 634c66172a..fe90124638 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -472,7 +472,17 @@ impl CiphernodeBuilder { e3_builder = e3_builder.with(PublicKeyAggregatorExtension::create( &bus, aggregator_preset, - )) + )); + + if self.keyshare.is_none() { + let backend = self + .zk_backend + .as_ref() + .ok_or_else(|| anyhow::anyhow!("ZK backend is required for aggregator"))?; + let signer = provider_cache.ensure_signer().await?; + info!("Setting up ZK actors for aggregator"); + setup_zk_actors(&bus, backend, signer); + } } if self.threshold_plaintext_agg { diff --git a/crates/events/src/enclave_event/aggregation_proof_pending.rs b/crates/events/src/enclave_event/aggregation_proof_pending.rs new file mode 100644 index 0000000000..fbceb1671e --- /dev/null +++ b/crates/events/src/enclave_event/aggregation_proof_pending.rs @@ -0,0 +1,25 @@ +// 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. + +//! Event for C7 proof generation through ProofRequestActor. +//! +//! `AggregationProofPending` is published by [`ThresholdPlaintextAggregator`] +//! after TrBFV threshold decryption completes. +//! `ProofRequestActor` generates the C7 proof(s), signs them, and publishes +//! `AggregationProofSigned`. + +use crate::{DecryptedSharesAggregationProofRequest, E3id}; +use e3_utils::utility_types::ArcBytes; +use serde::{Deserialize, Serialize}; + +/// ThresholdPlaintextAggregator -> ProofRequestActor: generate and sign C7 proofs. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct AggregationProofPending { + pub e3_id: E3id, + pub proof_request: DecryptedSharesAggregationProofRequest, + pub plaintext: Vec, + pub shares: Vec<(u64, Vec)>, +} diff --git a/crates/events/src/enclave_event/aggregation_proof_signed.rs b/crates/events/src/enclave_event/aggregation_proof_signed.rs new file mode 100644 index 0000000000..559a801a1a --- /dev/null +++ b/crates/events/src/enclave_event/aggregation_proof_signed.rs @@ -0,0 +1,21 @@ +// 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. + +//! Event for C7 proof signing completion. +//! +//! `AggregationProofSigned` is published by [`ProofRequestActor`] after +//! generating and ECDSA-signing the C7 proofs. [`ThresholdPlaintextAggregator`] +//! consumes this to transition to Complete and publish `PlaintextAggregated`. + +use crate::{E3id, SignedProofPayload}; +use serde::{Deserialize, Serialize}; + +/// ProofRequestActor -> ThresholdPlaintextAggregator: signed C7 proofs. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct AggregationProofSigned { + pub e3_id: E3id, + pub signed_proofs: Vec, +} diff --git a/crates/events/src/enclave_event/compute_request/mod.rs b/crates/events/src/enclave_event/compute_request/mod.rs index db477ef452..3d8efabc23 100644 --- a/crates/events/src/enclave_event/compute_request/mod.rs +++ b/crates/events/src/enclave_event/compute_request/mod.rs @@ -90,7 +90,6 @@ impl ToString for ComputeRequest { ZkRequest::VerifyShareDecryptionProofs(_) => "ZkVerifyShareDecryptionProofs", ZkRequest::PkAggregation(_) => "ZkPkAggregation", ZkRequest::ThresholdShareDecryption(_) => "ZkThresholdShareDecryption", - ZkRequest::VerifyC6Proofs(_) => "ZkVerifyC6Proofs", ZkRequest::DecryptedSharesAggregation(_) => "ZkDecryptedSharesAggregation", }, } diff --git a/crates/events/src/enclave_event/compute_request/zk.rs b/crates/events/src/enclave_event/compute_request/zk.rs index dc593896f0..9258b46187 100644 --- a/crates/events/src/enclave_event/compute_request/zk.rs +++ b/crates/events/src/enclave_event/compute_request/zk.rs @@ -34,8 +34,6 @@ pub enum ZkRequest { PkAggregation(PkAggregationProofRequest), /// Generate proof(s) for threshold share decryption (C6). ThresholdShareDecryption(ThresholdShareDecryptionProofRequest), - /// Batch-verify C6 proofs from DecryptionshareCreated events. - VerifyC6Proofs(VerifyC6ProofsRequest), /// Generate proof for decrypted shares aggregation (C7). DecryptedSharesAggregation(DecryptedSharesAggregationProofRequest), } @@ -207,8 +205,6 @@ pub enum ZkResponse { PkAggregation(PkAggregationProofResponse), /// Proof(s) for threshold share decryption (C6). ThresholdShareDecryption(ThresholdShareDecryptionProofResponse), - /// Batch verification results for C6 proofs. - VerifyC6Proofs(VerifyC6ProofsResponse), /// Proof for decrypted shares aggregation (C7). DecryptedSharesAggregation(DecryptedSharesAggregationProofResponse), } @@ -379,40 +375,6 @@ pub struct VerifyShareDecryptionProofsResponse { pub party_results: Vec, } -// --- C6 verification (follows VerifyC4Proofs pattern) --- - -/// Request to batch-verify C6 proofs from DecryptionshareCreated events. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct VerifyC6ProofsRequest { - /// C6 proofs grouped by sender party_id. - pub party_proofs: Vec, -} - -/// C6 proofs from a single sender to verify. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct PartyC6ProofsToVerify { - /// The party that generated these proofs. - pub sender_party_id: u64, - /// One C6 proof per ciphertext index. - pub c6_proofs: Vec, -} - -/// Batch verification results for C6 proofs. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct VerifyC6ProofsResponse { - /// Per-party verification results. - pub party_results: Vec, -} - -/// Verification result for C6 proofs from a single sender. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct PartyC6VerificationResult { - /// The party whose C6 proofs were verified. - pub sender_party_id: u64, - /// Whether ALL C6 proofs from this party verified successfully. - pub all_verified: bool, -} - // --- C7 proof generation --- /// Request to generate proof(s) for decrypted shares aggregation (C7). diff --git a/crates/events/src/enclave_event/decryption_share_proof_signed.rs b/crates/events/src/enclave_event/decryption_share_proof_signed.rs new file mode 100644 index 0000000000..5bb5331be7 --- /dev/null +++ b/crates/events/src/enclave_event/decryption_share_proof_signed.rs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::E3id; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DecryptionShareProofSigned { + pub e3_id: E3id, +} diff --git a/crates/events/src/enclave_event/decryptionshare_created.rs b/crates/events/src/enclave_event/decryptionshare_created.rs index da443dbdb9..344902ca02 100644 --- a/crates/events/src/enclave_event/decryptionshare_created.rs +++ b/crates/events/src/enclave_event/decryptionshare_created.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 e3_utils::utility_types::ArcBytes; use serde::{Deserialize, Serialize}; @@ -18,8 +18,8 @@ pub struct DecryptionshareCreated { // ciphertext pub e3_id: E3id, pub node: String, - /// C6 proofs: one proof of correct decryption per ciphertext index. - pub decryption_proofs: Vec, + /// C6 proofs: one signed proof of correct decryption per ciphertext index. + pub signed_decryption_proofs: Vec, } impl Display for DecryptionshareCreated { diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index ccaa29d5fb..e9b4e0a242 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -4,6 +4,8 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +mod aggregation_proof_pending; +mod aggregation_proof_signed; mod ciphernode_added; mod ciphernode_removed; mod ciphernode_selected; @@ -15,6 +17,7 @@ mod committee_requested; mod compute_request; mod configuration_updated; mod decryption_key_shared; +mod decryption_share_proof_signed; mod decryption_share_proofs; mod decryptionshare_created; mod die; @@ -32,6 +35,8 @@ mod keyshare_created; mod net_sync_events_received; mod operator_activation_changed; mod outgoing_sync_requested; +mod pk_aggregation_proof_pending; +mod pk_aggregation_proof_signed; mod pk_generation_proof_signed; mod plaintext_aggregated; mod plaintext_output_published; @@ -39,6 +44,7 @@ mod proof; mod publickey_aggregated; mod publish_document; mod share_computation_proof_signed; +mod share_decryption_proof_pending; mod share_verification; mod shutdown; mod signed_proof; @@ -54,6 +60,8 @@ mod ticket_generated; mod ticket_submitted; mod typed_event; +pub use aggregation_proof_pending::*; +pub use aggregation_proof_signed::*; pub use ciphernode_added::*; pub use ciphernode_removed::*; pub use ciphernode_selected::*; @@ -65,6 +73,7 @@ pub use committee_requested::*; pub use compute_request::*; pub use configuration_updated::*; pub use decryption_key_shared::*; +pub use decryption_share_proof_signed::*; pub use decryption_share_proofs::*; pub use decryptionshare_created::*; pub use die::*; @@ -83,6 +92,8 @@ pub use keyshare_created::*; pub use net_sync_events_received::*; pub use operator_activation_changed::*; pub use outgoing_sync_requested::*; +pub use pk_aggregation_proof_pending::*; +pub use pk_aggregation_proof_signed::*; pub use pk_generation_proof_signed::*; pub use plaintext_aggregated::*; pub use plaintext_output_published::*; @@ -90,6 +101,7 @@ pub use proof::*; pub use publickey_aggregated::*; pub use publish_document::*; pub use share_computation_proof_signed::*; +pub use share_decryption_proof_pending::*; pub use share_verification::*; pub use shutdown::*; pub use signed_proof::*; @@ -252,6 +264,12 @@ pub enum EnclaveEventData { SyncEffect(SyncEffect), SyncEnded(SyncEnded), EffectsEnabled(EffectsEnabled), + DecryptionShareProofSigned(DecryptionShareProofSigned), + ShareDecryptionProofPending(ShareDecryptionProofPending), + PkAggregationProofPending(PkAggregationProofPending), + PkAggregationProofSigned(PkAggregationProofSigned), + AggregationProofPending(AggregationProofPending), + AggregationProofSigned(AggregationProofSigned), /// This is a test event to use in testing TestEvent(TestEvent), } @@ -504,6 +522,12 @@ impl EnclaveEventData { 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()), + EnclaveEventData::DecryptionShareProofSigned(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::ShareDecryptionProofPending(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::PkAggregationProofPending(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::PkAggregationProofSigned(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::AggregationProofPending(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::AggregationProofSigned(ref data) => Some(data.e3_id.clone()), _ => None, } } @@ -579,7 +603,13 @@ impl_event_types!( HistoricalNetSyncStart, SyncEffect, SyncEnded, - EffectsEnabled + EffectsEnabled, + DecryptionShareProofSigned, + ShareDecryptionProofPending, + PkAggregationProofPending, + PkAggregationProofSigned, + AggregationProofPending, + AggregationProofSigned ); impl TryFrom<&EnclaveEvent> for EnclaveError { diff --git a/crates/events/src/enclave_event/pk_aggregation_proof_pending.rs b/crates/events/src/enclave_event/pk_aggregation_proof_pending.rs new file mode 100644 index 0000000000..2ee5b1afe8 --- /dev/null +++ b/crates/events/src/enclave_event/pk_aggregation_proof_pending.rs @@ -0,0 +1,25 @@ +// 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. + +//! Event for C5 proof generation through ProofRequestActor. +//! +//! `PkAggregationProofPending` is published by [`PublicKeyAggregator`] after +//! C1 verification succeeds and synchronous aggregation completes. +//! `ProofRequestActor` generates the C5 proof, signs it, and publishes +//! `PkAggregationProofSigned`. + +use crate::{E3id, OrderedSet, PkAggregationProofRequest}; +use serde::{Deserialize, Serialize}; + +/// PublicKeyAggregator -> ProofRequestActor: generate and sign C5 proof. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PkAggregationProofPending { + pub e3_id: E3id, + pub proof_request: PkAggregationProofRequest, + pub public_key: Vec, + pub public_key_hash: [u8; 32], + pub nodes: OrderedSet, +} diff --git a/crates/events/src/enclave_event/pk_aggregation_proof_signed.rs b/crates/events/src/enclave_event/pk_aggregation_proof_signed.rs new file mode 100644 index 0000000000..bf2d118392 --- /dev/null +++ b/crates/events/src/enclave_event/pk_aggregation_proof_signed.rs @@ -0,0 +1,21 @@ +// 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. + +//! Event for C5 proof signing completion. +//! +//! `PkAggregationProofSigned` is published by [`ProofRequestActor`] after +//! generating and ECDSA-signing the C5 proof. [`PublicKeyAggregator`] +//! consumes this to transition to Complete and publish `PublicKeyAggregated`. + +use crate::{E3id, SignedProofPayload}; +use serde::{Deserialize, Serialize}; + +/// ProofRequestActor -> PublicKeyAggregator: signed C5 proof. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PkAggregationProofSigned { + pub e3_id: E3id, + pub signed_proof: SignedProofPayload, +} diff --git a/crates/events/src/enclave_event/share_decryption_proof_pending.rs b/crates/events/src/enclave_event/share_decryption_proof_pending.rs new file mode 100644 index 0000000000..ec7134fb19 --- /dev/null +++ b/crates/events/src/enclave_event/share_decryption_proof_pending.rs @@ -0,0 +1,31 @@ +// 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. + +//! Event for C6 proof generation through ProofRequestActor. +//! +//! `ShareDecryptionProofPending` is published by [`ThresholdKeyshare`] when it +//! has computed the decryption share and needs C6 proofs generated and signed. +//! `ProofRequestActor` generates the proofs, signs them, and publishes +//! `DecryptionshareCreated` with signed proofs. + +use crate::{E3id, ThresholdShareDecryptionProofRequest}; +use e3_utils::utility_types::ArcBytes; +use serde::{Deserialize, Serialize}; + +/// ThresholdKeyshare -> ProofRequestActor: generate and sign C6 proofs. +/// +/// Carries the proof generation inputs and the protocol data so that +/// ProofRequestActor can publish `DecryptionshareCreated` directly. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ShareDecryptionProofPending { + pub e3_id: E3id, + pub party_id: u64, + pub node: String, + /// Computed decryption shares, one per ciphertext index. + pub decryption_share: Vec, + /// C6 proof generation request. + pub proof_request: ThresholdShareDecryptionProofRequest, +} diff --git a/crates/events/src/enclave_event/share_verification.rs b/crates/events/src/enclave_event/share_verification.rs index 2ca3c37243..b529c4257e 100644 --- a/crates/events/src/enclave_event/share_verification.rs +++ b/crates/events/src/enclave_event/share_verification.rs @@ -24,6 +24,10 @@ pub enum VerificationKind { ShareProofs, /// C4 share decryption proof verification (after AllDecryptionKeySharesCollected). DecryptionProofs, + /// C6 threshold decryption proof verification (after all DecryptionshareCreated collected). + ThresholdDecryptionProofs, + /// C1 PK generation proof verification (after all KeyshareCreated collected). + PkGenerationProofs, } /// ThresholdKeyshare → ShareVerificationActor: verify party proofs. diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index 0d2234ac30..4bdc1b54fd 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -47,6 +47,8 @@ pub enum ProofType { T5ShareDecryption = 7, /// T6 — Decrypted shares aggregation proof (Proof 7). T6DecryptedSharesAggregation = 8, + /// C5 — Public key aggregation proof (Proof 5). + C5PkAggregation = 9, } impl ProofType { @@ -65,6 +67,7 @@ impl ProofType { CircuitName::DecryptedSharesAggregationBn, CircuitName::DecryptedSharesAggregationMod, ], + ProofType::C5PkAggregation => vec![CircuitName::PkAggregation], } } @@ -80,6 +83,7 @@ impl ProofType { | ProofType::T2DkgShareDecryption => "E3_BAD_DKG_PROOF", ProofType::T5ShareDecryption => "E3_BAD_DECRYPTION_PROOF", ProofType::T6DecryptedSharesAggregation => "E3_BAD_AGGREGATION_PROOF", + ProofType::C5PkAggregation => "E3_BAD_PK_AGGREGATION_PROOF", } } } diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index bc7627fd48..8ce8c3cb30 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -11,16 +11,16 @@ use e3_data::Persistable; use e3_events::{ prelude::*, trap, BusHandle, CiphernodeSelected, CiphertextOutputPublished, ComputeRequest, ComputeResponse, ComputeResponseKind, CorrelationId, DecryptionKeyShared, - DecryptionShareProofsPending, DecryptionshareCreated, Die, DkgProofSigned, + DecryptionShareProofSigned, DecryptionShareProofsPending, Die, DkgProofSigned, DkgShareDecryptionProofRequest, E3Failed, E3RequestComplete, E3Stage, E3id, EType, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCollectionFailed, EncryptionKeyCreated, EncryptionKeyPending, EventContext, FailureReason, KeyshareCreated, PartyId, PartyProofsToVerify, PartyShareDecryptionProofsToVerify, PkGenerationProofRequest, PkGenerationProofSigned, ProofType, Sequenced, ShareComputationProofRequest, - ShareEncryptionProofRequest, ShareVerificationComplete, ShareVerificationDispatched, - SignedProofPayload, ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated, - ThresholdShareDecryptionProofRequest, ThresholdSharePending, TypedEvent, VerificationKind, - ZkRequest, ZkResponse, + ShareDecryptionProofPending, ShareEncryptionProofRequest, ShareVerificationComplete, + ShareVerificationDispatched, SignedProofPayload, ThresholdShare, + ThresholdShareCollectionFailed, ThresholdShareCreated, ThresholdShareDecryptionProofRequest, + ThresholdSharePending, TypedEvent, VerificationKind, }; use e3_fhe_params::create_deterministic_crp_from_default_seed; use e3_fhe_params::{build_pair_for_preset, BfvParamSet, BfvPreset}; @@ -644,13 +644,9 @@ impl ThresholdKeyshare { } _ => Ok(()), }, - // ZK responses: C4 proofs and share verification are handled by + // ZK responses: proofs and verification are handled by // ProofRequestActor and ShareVerificationActor respectively. - // Only C6 (ThresholdShareDecryption) is handled here. - ComputeResponseKind::Zk(zk) => match zk { - ZkResponse::ThresholdShareDecryption(_) => self.handle_c6_proof_response(msg), - _ => Ok(()), - }, + ComputeResponseKind::Zk(_) => Ok(()), } } @@ -1382,6 +1378,7 @@ impl ThresholdKeyshare { self.publish_keyshare_created(ec) } + _ => Ok(()), } } @@ -1969,7 +1966,8 @@ impl ThresholdKeyshare { Ok(()) } - /// CalculateDecryptionShareResponse — dispatch C6 proof generation + /// CalculateDecryptionShareResponse — publish ShareDecryptionProofPending + /// so ProofRequestActor generates and signs C6 proofs. pub fn handle_calculate_decryption_share_response( &mut self, res: TypedEvent, @@ -1998,24 +1996,28 @@ impl ThresholdKeyshare { ) })?; - info!("Dispatching C6 proof generation (threshold share decryption)..."); + info!("Publishing ShareDecryptionProofPending for C6 proof generation..."); - // Publish C6 proof request before transitioning state so a publish + // Publish pending event before transitioning state so a publish // failure leaves us in Decrypting (retryable) rather than // GeneratingDecryptionProof (no retry path). - let request = ComputeRequest::zk( - ZkRequest::ThresholdShareDecryption(ThresholdShareDecryptionProofRequest { - ciphertext_bytes: decrypting.ciphertext_output, - aggregated_pk_bytes, - sk_poly_sum: decrypting.sk_poly_sum, - es_poly_sum: decrypting.es_poly_sum, - d_share_bytes: d_share_poly.clone(), - params_preset: threshold_preset, - }), - CorrelationId::new(), - e3_id, - ); - self.bus.publish(request, ec.clone())?; + self.bus.publish( + ShareDecryptionProofPending { + e3_id: e3_id.clone(), + party_id: state.party_id, + node: state.address.clone(), + decryption_share: d_share_poly.clone(), + proof_request: ThresholdShareDecryptionProofRequest { + ciphertext_bytes: decrypting.ciphertext_output, + aggregated_pk_bytes, + sk_poly_sum: decrypting.sk_poly_sum, + es_poly_sum: decrypting.es_poly_sum, + d_share_bytes: d_share_poly.clone(), + params_preset: threshold_preset, + }, + }, + ec.clone(), + )?; // Transition to GeneratingDecryptionProof state self.state.try_mutate(&ec, |s| { @@ -2041,37 +2043,12 @@ impl ThresholdKeyshare { Ok(()) } - /// Handle C6 proof response — publish DecryptionshareCreated with proofs - pub fn handle_c6_proof_response(&mut self, res: TypedEvent) -> Result<()> { - let (res, ec) = res.into_components(); - let zk_response = res.try_into_zk()?; - let proofs = match zk_response { - ZkResponse::ThresholdShareDecryption(data) => data.proofs, - _ => bail!("Expected ThresholdShareDecryptionProofResponse"), - }; - - let state = self.state.try_get()?; - let party_id = state.party_id; - let node = state.address.clone(); - let e3_id = state.e3_id.clone(); - let gen_proof: GeneratingDecryptionProof = state.clone().try_into()?; - - info!( - "C6 proof generated ({} proofs), publishing DecryptionshareCreated", - proofs.len() - ); - - let event = DecryptionshareCreated { - party_id, - node, - e3_id, - decryption_share: gen_proof.decryption_share, - decryption_proofs: proofs, - }; - - self.bus.publish(event, ec.clone())?; + pub fn handle_decryption_share_proof_signed( + &mut self, + msg: TypedEvent, + ) -> Result<()> { + let (_msg, ec) = msg.into_components(); - // mark as complete self.state.try_mutate(&ec, |s| { use KeyshareState as K; info!("Decryption share sending process is complete"); @@ -2199,6 +2176,9 @@ impl Handler for ThresholdKeyshare { } } } + EnclaveEventData::DecryptionShareProofSigned(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } EnclaveEventData::ShareVerificationComplete(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } @@ -2210,6 +2190,21 @@ impl Handler for ThresholdKeyshare { } } +impl Handler> for ThresholdKeyshare { + type 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_decryption_share_proof_signed(msg), + ) + } +} + impl Handler> for ThresholdKeyshare { type Result = (); fn handle( diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index f28cdd1b91..0d9759ea47 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -27,14 +27,14 @@ use e3_events::{ ComputeResponse, DecryptedSharesAggregationProofRequest, DecryptedSharesAggregationProofResponse, DkgShareDecryptionProofRequest, DkgShareDecryptionProofResponse, EnclaveEvent, EnclaveEventData, EventPublisher, - EventSubscriber, EventType, PartyC6VerificationResult, PartyVerificationResult, - PkAggregationProofRequest, PkAggregationProofResponse, PkBfvProofRequest, PkBfvProofResponse, - PkGenerationProofRequest, PkGenerationProofResponse, ShareComputationProofRequest, - ShareComputationProofResponse, ShareEncryptionProofRequest, ShareEncryptionProofResponse, + EventSubscriber, EventType, PartyVerificationResult, PkAggregationProofRequest, + PkAggregationProofResponse, PkBfvProofRequest, PkBfvProofResponse, PkGenerationProofRequest, + PkGenerationProofResponse, ShareComputationProofRequest, ShareComputationProofResponse, + ShareEncryptionProofRequest, ShareEncryptionProofResponse, ThresholdShareDecryptionProofRequest, ThresholdShareDecryptionProofResponse, TypedEvent, - VerifyC6ProofsRequest, VerifyC6ProofsResponse, VerifyShareDecryptionProofsRequest, - VerifyShareDecryptionProofsResponse, VerifyShareProofsRequest, VerifyShareProofsResponse, - ZkError as ZkEventError, ZkRequest, ZkResponse, + VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, + VerifyShareProofsRequest, VerifyShareProofsResponse, ZkError as ZkEventError, ZkRequest, + ZkResponse, }; use e3_fhe_params::build_pair_for_preset; use e3_fhe_params::{BfvParamSet, BfvPreset}; @@ -609,9 +609,6 @@ fn handle_zk_request( handle_threshold_share_decryption_proof(&prover, &cipher, req, request.clone()) }) } - ZkRequest::VerifyC6Proofs(req) => timefunc("zk_verify_c6_proofs", id, || { - handle_verify_c6_proofs(&prover, req, request.clone()) - }), ZkRequest::DecryptedSharesAggregation(req) => { timefunc("zk_decrypted_shares_aggregation", id, || { handle_decrypted_shares_aggregation_proof(&prover, req, request.clone()) @@ -1151,53 +1148,6 @@ fn handle_verify_share_decryption_proofs( )) } -fn handle_verify_c6_proofs( - prover: &ZkProver, - req: VerifyC6ProofsRequest, - 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; - - if party.c6_proofs.is_empty() { - return PartyC6VerificationResult { - sender_party_id: sender, - all_verified: false, - }; - } - - for c6_proof in &party.c6_proofs { - let result = prover.verify_proof(c6_proof, &e3_id_str, sender); - match result { - Ok(true) => continue, - Ok(false) | Err(_) => { - return PartyC6VerificationResult { - sender_party_id: sender, - all_verified: false, - }; - } - } - } - - PartyC6VerificationResult { - sender_party_id: sender, - all_verified: true, - } - }) - .collect(); - - Ok(ComputeResponse::zk( - ZkResponse::VerifyC6Proofs(VerifyC6ProofsResponse { party_results }), - request.correlation_id, - request.e3_id, - )) -} - fn handle_decrypted_shares_aggregation_proof( prover: &ZkProver, mut req: DecryptedSharesAggregationProofRequest, diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 358f59d784..5fa0fa62a1 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -663,10 +663,14 @@ async fn test_trbfv_actor() -> Result<()> { // Wait for KeyshareCreated + C1 verification + C5 proof + PublicKeyAggregated // - KeyshareCreated × 5 (forwarded from committee nodes) - // - ComputeRequest (C1 proof verification dispatched by PublicKeyAggregator) - // - ComputeResponse (C1 proof verification result) - // - ComputeRequest (C5 PkAggregation proof dispatched by PublicKeyAggregator) - // - ComputeResponse (C5 PkAggregation proof result) + // - ShareVerificationDispatched (C1 proof verification dispatched by PublicKeyAggregator) + // - ComputeRequest (C1 ZK verification) + // - ComputeResponse (C1 ZK verification result) + // - ShareVerificationComplete (C1 verification done) + // - PkAggregationProofPending (C5 proof requested by PublicKeyAggregator) + // - ComputeRequest (C5 proof generation) + // - ComputeResponse (C5 proof result) + // - PkAggregationProofSigned (C5 proof signed by ProofRequestActor) // - PublicKeyAggregated × 1 let shares_to_pubkey_agg_timer = Instant::now(); let expected = vec![ @@ -675,10 +679,14 @@ async fn test_trbfv_actor() -> Result<()> { "KeyshareCreated", "KeyshareCreated", "KeyshareCreated", + "ShareVerificationDispatched", "ComputeRequest", "ComputeResponse", + "ShareVerificationComplete", + "PkAggregationProofPending", "ComputeRequest", "ComputeResponse", + "PkAggregationProofSigned", "PublicKeyAggregated", ]; let h = nodes @@ -755,17 +763,21 @@ async fn test_trbfv_actor() -> Result<()> { // The collector sees: // - 1 CiphertextOutputPublished (from shared bus) // - 5 DecryptionshareCreated (from simulate_libp2p, passes is_forwardable_event) - // - 1 ComputeRequest (VerifyC6Proofs) - // - 1 ComputeResponse (VerifyC6Proofs) + // - 1 ShareVerificationDispatched (C6 verification dispatched by ThresholdPlaintextAggregator) + // - 1 ComputeRequest (C6 ZK verification) + // - 1 ComputeResponse (C6 ZK verification result) + // - 1 ShareVerificationComplete (C6 verification done) // - 1 ComputeRequest (TrBFV CalculateThresholdDecryption) // - 1 ComputeResponse (TrBFV CalculateThresholdDecryption) - // - 1 ComputeRequest (DecryptedSharesAggregation C7) - // - 1 ComputeResponse (DecryptedSharesAggregation C7) + // - 1 AggregationProofPending (C7 proof requested by ThresholdPlaintextAggregator) + // - 1 ComputeRequest (C7 proof generation) + // - 1 ComputeResponse (C7 proof result) + // - 1 AggregationProofSigned (C7 proof signed by ProofRequestActor) // - 1 PlaintextAggregated (with C7 proofs) // Internal events from committee nodes (ComputeRequest/Response for CalculateDecryptionShare) // stay on their local buses. - // Total: 1 + 5 + 2 + 2 + 2 + 1 = 13 events - let expected_count = 1 + 5 + 2 + 2 + 2 + 1; + // Total: 1 + 5 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 1 = 17 events + let expected_count = 1 + 5 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 1; let h = nodes .take_history_with_timeout(0, expected_count, Duration::from_secs(1000)) diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index 4b9e07a003..232fb3d7b4 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -10,13 +10,15 @@ use std::sync::Arc; use actix::{Actor, Addr, Context, Handler}; use alloy::signers::local::PrivateKeySigner; use e3_events::{ - BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeResponse, - 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, + AggregationProofPending, AggregationProofSigned, BusHandle, ComputeRequest, + ComputeRequestError, ComputeRequestErrorKind, ComputeResponse, ComputeResponseKind, + CorrelationId, DecryptionKeyShared, DecryptionShareProofSigned, DecryptionShareProofsPending, + DecryptionshareCreated, DkgProofSigned, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, + EncryptionKeyCreated, EncryptionKeyPending, EventContext, EventPublisher, EventSubscriber, + EventType, PkAggregationProofPending, PkAggregationProofSigned, PkBfvProofRequest, + PkGenerationProofSigned, Proof, ProofPayload, ProofType, Sequenced, + ShareDecryptionProofPending, SignedProofPayload, ThresholdShare, ThresholdShareCreated, + ThresholdSharePending, TypedEvent, ZkRequest, ZkResponse, }; use e3_utils::utility_types::ArcBytes; use e3_utils::NotifySync; @@ -159,6 +161,27 @@ impl PendingDecryptionProofs { } } +/// Pending C5 (PkAggregation) proof generation state. +#[derive(Clone, Debug)] +struct PendingPkAggregationProof { + ec: EventContext, +} + +/// Pending C6 (ShareDecryptionProof) proof generation state. +#[derive(Clone, Debug)] +struct PendingShareDecryptionProof { + party_id: u64, + node: String, + decryption_share: Vec, + ec: EventContext, +} + +/// Pending C7 (DecryptedSharesAggregation) proof generation state. +#[derive(Clone, Debug)] +struct PendingAggregationProof { + ec: EventContext, +} + /// Core actor that handles encryption key proof requests. /// /// Proofs are always wrapped in a [`SignedProofPayload`] before being published, @@ -170,10 +193,22 @@ pub struct ProofRequestActor { pending: HashMap, threshold_correlation: HashMap, pending_threshold: HashMap, - /// C4 proof staging: correlation → (e3_id, kind) + /// C4 proof staging: correlation -> (e3_id, kind) decryption_correlation: HashMap, /// C4 pending proofs per E3 pending_decryption: HashMap, + /// C6 proof staging: correlation -> e3_id + share_decryption_correlation: HashMap, + /// C6 pending proofs per E3 + pending_share_decryption: HashMap, + /// C5 proof staging: correlation -> e3_id + pk_aggregation_correlation: HashMap, + /// C5 pending proofs per E3 + pending_pk_aggregation: HashMap, + /// C7 proof staging: correlation -> e3_id + aggregation_correlation: HashMap, + /// C7 pending proofs per E3 + pending_aggregation: HashMap, } impl ProofRequestActor { @@ -186,6 +221,12 @@ impl ProofRequestActor { threshold_correlation: HashMap::new(), decryption_correlation: HashMap::new(), pending_decryption: HashMap::new(), + share_decryption_correlation: HashMap::new(), + pending_share_decryption: HashMap::new(), + pk_aggregation_correlation: HashMap::new(), + pending_pk_aggregation: HashMap::new(), + aggregation_correlation: HashMap::new(), + pending_aggregation: HashMap::new(), } } @@ -196,6 +237,9 @@ impl ProofRequestActor { bus.subscribe(EventType::ComputeRequestError, addr.clone().into()); bus.subscribe(EventType::ThresholdSharePending, addr.clone().into()); bus.subscribe(EventType::DecryptionShareProofsPending, addr.clone().into()); + bus.subscribe(EventType::ShareDecryptionProofPending, addr.clone().into()); + bus.subscribe(EventType::PkAggregationProofPending, addr.clone().into()); + bus.subscribe(EventType::AggregationProofPending, addr.clone().into()); addr } @@ -393,6 +437,18 @@ impl ProofRequestActor { self.handle_threshold_proof_response(&msg.correlation_id, resp.proof.clone()); } } + ComputeResponseKind::Zk(ZkResponse::ThresholdShareDecryption(resp)) => { + self.handle_share_decryption_proof_response( + &msg.correlation_id, + resp.proofs.clone(), + ); + } + ComputeResponseKind::Zk(ZkResponse::PkAggregation(resp)) => { + self.handle_pk_aggregation_proof_response(&msg.correlation_id, resp.proof.clone()); + } + ComputeResponseKind::Zk(ZkResponse::DecryptedSharesAggregation(resp)) => { + self.handle_aggregation_proof_response(&msg.correlation_id, resp.proofs.clone()); + } _ => {} } } @@ -579,6 +635,273 @@ impl ProofRequestActor { } } + /// Handle ShareDecryptionProofPending: dispatch C6 proof generation. + fn handle_share_decryption_proof_pending( + &mut self, + msg: TypedEvent, + ) { + let (msg, ec) = msg.into_components(); + let e3_id = msg.e3_id.clone(); + + if self.pending_share_decryption.contains_key(&e3_id) { + warn!( + "Duplicate ShareDecryptionProofPending for E3 {} — ignoring", + e3_id + ); + return; + } + + self.pending_share_decryption.insert( + e3_id.clone(), + PendingShareDecryptionProof { + party_id: msg.party_id, + node: msg.node, + decryption_share: msg.decryption_share, + ec: ec.clone(), + }, + ); + + let correlation_id = CorrelationId::new(); + self.share_decryption_correlation + .insert(correlation_id, e3_id.clone()); + + info!( + "Requesting C6 ThresholdShareDecryption proof for E3 {}", + e3_id + ); + if let Err(err) = self.bus.publish( + ComputeRequest::zk( + ZkRequest::ThresholdShareDecryption(msg.proof_request), + correlation_id, + e3_id.clone(), + ), + ec, + ) { + error!("Failed to publish C6 proof request: {err}"); + self.share_decryption_correlation.remove(&correlation_id); + self.pending_share_decryption.remove(&e3_id); + } + } + + /// Handle C6 proof response — sign proofs and publish DecryptionshareCreated. + fn handle_share_decryption_proof_response( + &mut self, + correlation_id: &CorrelationId, + proofs: Vec, + ) { + let Some(e3_id) = self.share_decryption_correlation.remove(correlation_id) else { + return; + }; + + let Some(pending) = self.pending_share_decryption.remove(&e3_id) else { + error!( + "No pending share decryption proof for E3 {} — orphan correlation", + e3_id + ); + return; + }; + + // Sign each C6 proof + let mut signed_proofs = Vec::with_capacity(proofs.len()); + for proof in proofs { + let Some(signed) = self.sign_proof(&e3_id, ProofType::T5ShareDecryption, proof) else { + error!("Failed to sign C6 proof — DecryptionshareCreated will not be published"); + return; + }; + signed_proofs.push(signed); + } + + info!( + "All C6 proofs signed for E3 {} party {} (signer: {})", + e3_id, + pending.party_id, + self.signer.address() + ); + + let ec = pending.ec; + if let Err(err) = self.bus.publish( + DecryptionshareCreated { + party_id: pending.party_id, + node: pending.node, + e3_id: e3_id.clone(), + decryption_share: pending.decryption_share, + signed_decryption_proofs: signed_proofs, + }, + ec.clone(), + ) { + error!("Failed to publish DecryptionshareCreated: {err}"); + } + + if let Err(err) = self.bus.publish( + DecryptionShareProofSigned { + e3_id: e3_id.clone(), + }, + ec, + ) { + error!("Failed to publish DecryptionShareProofSigned: {err}"); + } + } + + /// Handle PkAggregationProofPending: dispatch C5 proof generation. + fn handle_pk_aggregation_proof_pending(&mut self, msg: TypedEvent) { + let (msg, ec) = msg.into_components(); + let e3_id = msg.e3_id.clone(); + + if self.pending_pk_aggregation.contains_key(&e3_id) { + warn!( + "Duplicate PkAggregationProofPending for E3 {} — ignoring", + e3_id + ); + return; + } + + self.pending_pk_aggregation + .insert(e3_id.clone(), PendingPkAggregationProof { ec: ec.clone() }); + + let correlation_id = CorrelationId::new(); + self.pk_aggregation_correlation + .insert(correlation_id, e3_id.clone()); + + info!("Requesting C5 PkAggregation proof for E3 {}", e3_id); + if let Err(err) = self.bus.publish( + ComputeRequest::zk( + ZkRequest::PkAggregation(msg.proof_request), + correlation_id, + e3_id.clone(), + ), + ec, + ) { + error!("Failed to publish C5 proof request: {err}"); + self.pk_aggregation_correlation.remove(&correlation_id); + self.pending_pk_aggregation.remove(&e3_id); + } + } + + /// Handle C5 proof response — sign proof and publish PkAggregationProofSigned. + fn handle_pk_aggregation_proof_response( + &mut self, + correlation_id: &CorrelationId, + proof: Proof, + ) { + let Some(e3_id) = self.pk_aggregation_correlation.remove(correlation_id) else { + return; + }; + + let Some(pending) = self.pending_pk_aggregation.remove(&e3_id) else { + error!( + "No pending pk aggregation proof for E3 {} — orphan correlation", + e3_id + ); + return; + }; + + let Some(signed) = self.sign_proof(&e3_id, ProofType::C5PkAggregation, proof) else { + error!("Failed to sign C5 proof — PkAggregationProofSigned will not be published"); + return; + }; + + info!( + "C5 proof signed for E3 {} (signer: {})", + e3_id, + self.signer.address() + ); + + if let Err(err) = self.bus.publish( + PkAggregationProofSigned { + e3_id: e3_id.clone(), + signed_proof: signed, + }, + pending.ec, + ) { + error!("Failed to publish PkAggregationProofSigned: {err}"); + } + } + + /// Handle AggregationProofPending: dispatch C7 proof generation. + fn handle_aggregation_proof_pending(&mut self, msg: TypedEvent) { + let (msg, ec) = msg.into_components(); + let e3_id = msg.e3_id.clone(); + + if self.pending_aggregation.contains_key(&e3_id) { + warn!( + "Duplicate AggregationProofPending for E3 {} — ignoring", + e3_id + ); + return; + } + + self.pending_aggregation + .insert(e3_id.clone(), PendingAggregationProof { ec: ec.clone() }); + + let correlation_id = CorrelationId::new(); + self.aggregation_correlation + .insert(correlation_id, e3_id.clone()); + + info!( + "Requesting C7 DecryptedSharesAggregation proof for E3 {}", + e3_id + ); + if let Err(err) = self.bus.publish( + ComputeRequest::zk( + ZkRequest::DecryptedSharesAggregation(msg.proof_request), + correlation_id, + e3_id.clone(), + ), + ec, + ) { + error!("Failed to publish C7 proof request: {err}"); + self.aggregation_correlation.remove(&correlation_id); + self.pending_aggregation.remove(&e3_id); + } + } + + /// Handle C7 proof response — sign proofs and publish AggregationProofSigned. + fn handle_aggregation_proof_response( + &mut self, + correlation_id: &CorrelationId, + proofs: Vec, + ) { + let Some(e3_id) = self.aggregation_correlation.remove(correlation_id) else { + return; + }; + + let Some(pending) = self.pending_aggregation.remove(&e3_id) else { + error!( + "No pending aggregation proof for E3 {} — orphan correlation", + e3_id + ); + return; + }; + + // Sign each C7 proof + let mut signed_proofs = Vec::with_capacity(proofs.len()); + for proof in proofs { + let Some(signed) = + self.sign_proof(&e3_id, ProofType::T6DecryptedSharesAggregation, proof) + else { + error!("Failed to sign C7 proof — AggregationProofSigned will not be published"); + return; + }; + signed_proofs.push(signed); + } + + info!( + "All C7 proofs signed for E3 {} (signer: {})", + e3_id, + self.signer.address() + ); + + if let Err(err) = self.bus.publish( + AggregationProofSigned { + e3_id: e3_id.clone(), + signed_proofs, + }, + pending.ec, + ) { + error!("Failed to publish AggregationProofSigned: {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; @@ -632,6 +955,20 @@ impl ProofRequestActor { } } + fn sign_and_group_proofs( + &self, + e3_id: &E3id, + proof_type: ProofType, + proofs: impl Iterator, + ) -> Option>> { + let mut map: BTreeMap> = BTreeMap::new(); + for (recipient, proof) in proofs { + let signed = self.sign_proof(e3_id, proof_type, proof)?; + map.entry(recipient).or_default().push(signed); + } + Some(map) + } + fn publish_threshold_share_with_proofs(&mut self, pending: PendingThresholdProofs) { let e3_id = &pending.e3_id; let party_id = pending.full_share.party_id; @@ -667,37 +1004,29 @@ impl ProofRequestActor { return; }; - // Sign C3a proofs (SkShareEncryption) — keyed by (recipient, row) - let mut signed_c3a_map: BTreeMap> = BTreeMap::new(); - for ((_recipient, _row), proof) in &pending.sk_share_encryption_proofs { - if let Some(signed) = - self.sign_proof(e3_id, ProofType::C3aSkShareEncryption, proof.clone()) - { - signed_c3a_map.entry(*_recipient).or_default().push(signed); - } else { - error!( - "Failed to sign C3a proof for recipient {} — shares will not be published", - _recipient - ); - return; - } - } + let Some(signed_c3a_map) = self.sign_and_group_proofs( + e3_id, + ProofType::C3aSkShareEncryption, + pending + .sk_share_encryption_proofs + .iter() + .map(|((recipient, _row), proof)| (*recipient, proof.clone())), + ) else { + error!("Failed to sign C3a proofs — shares will not be published"); + return; + }; - // Sign C3b proofs (ESmShareEncryption) — keyed by (esi_index, recipient, row) - let mut signed_c3b_map: BTreeMap> = BTreeMap::new(); - for ((_esi, _recipient, _row), proof) in &pending.e_sm_share_encryption_proofs { - if let Some(signed) = - self.sign_proof(e3_id, ProofType::C3bESmShareEncryption, proof.clone()) - { - signed_c3b_map.entry(*_recipient).or_default().push(signed); - } else { - error!( - "Failed to sign C3b proof for recipient {} — shares will not be published", - _recipient - ); - return; - } - } + let Some(signed_c3b_map) = self.sign_and_group_proofs( + e3_id, + ProofType::C3bESmShareEncryption, + pending + .e_sm_share_encryption_proofs + .iter() + .map(|((_esi, recipient, _row), proof)| (*recipient, proof.clone())), + ) else { + error!("Failed to sign C3b proofs — shares will not be published"); + return; + }; info!( "All proofs signed for E3 {} party {} (signer: {})", @@ -898,6 +1227,33 @@ impl ProofRequestActor { .retain(|_, (eid, _)| *eid != e3_id); self.pending_decryption.remove(&e3_id); } + + if let Some(e3_id) = self + .share_decryption_correlation + .remove(msg.correlation_id()) + { + error!( + "C6 proof request failed for E3 {}: {err} — DecryptionshareCreated will not be published", + e3_id + ); + self.pending_share_decryption.remove(&e3_id); + } + + if let Some(e3_id) = self.pk_aggregation_correlation.remove(msg.correlation_id()) { + error!( + "C5 proof request failed for E3 {}: {err} — PkAggregationProofSigned will not be published", + e3_id + ); + self.pending_pk_aggregation.remove(&e3_id); + } + + if let Some(e3_id) = self.aggregation_correlation.remove(msg.correlation_id()) { + error!( + "C7 proof request failed for E3 {}: {err} — AggregationProofSigned will not be published", + e3_id + ); + self.pending_aggregation.remove(&e3_id); + } } } @@ -927,6 +1283,15 @@ impl Handler for ProofRequestActor { EnclaveEventData::DecryptionShareProofsPending(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::ShareDecryptionProofPending(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::PkAggregationProofPending(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::AggregationProofPending(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } _ => (), } } @@ -991,3 +1356,39 @@ impl Handler> for ProofRequestActor { self.handle_decryption_share_proofs_pending(msg) } } + +impl Handler> for ProofRequestActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_share_decryption_proof_pending(msg) + } +} + +impl Handler> for ProofRequestActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_pk_aggregation_proof_pending(msg) + } +} + +impl Handler> for ProofRequestActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_aggregation_proof_pending(msg) + } +} diff --git a/crates/zk-prover/src/actors/share_verification.rs b/crates/zk-prover/src/actors/share_verification.rs index 439122f6ef..2e005df4df 100644 --- a/crates/zk-prover/src/actors/share_verification.rs +++ b/crates/zk-prover/src/actors/share_verification.rs @@ -38,7 +38,6 @@ 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
)>, @@ -55,9 +54,6 @@ struct PendingVerification { 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>, /// Recovered address for each party (from ECDSA step). party_addresses: HashMap, } @@ -97,9 +93,16 @@ impl ShareVerificationActor { let (msg, ec) = msg.into_components(); let e3_id = msg.e3_id.clone(); + info!( + "handling ShareVerificationDispatched {:?}, {:?}", + e3_id, msg.kind + ); + match msg.kind { - VerificationKind::ShareProofs => { - self.verify_share_proofs(e3_id, msg.share_proofs, msg.pre_dishonest, ec); + VerificationKind::ShareProofs + | VerificationKind::ThresholdDecryptionProofs + | VerificationKind::PkGenerationProofs => { + self.verify_share_proofs(e3_id, msg.kind, msg.share_proofs, msg.pre_dishonest, ec); } VerificationKind::DecryptionProofs => { self.verify_decryption_proofs(e3_id, msg.decryption_proofs, msg.pre_dishonest, ec); @@ -107,18 +110,24 @@ impl ShareVerificationActor { } } - /// C2/C3 verification: ECDSA check on each party, then dispatch ZK. + /// C2/C3/C6/C1 verification: ECDSA check on each party, then dispatch ZK. fn verify_share_proofs( &mut self, e3_id: E3id, + kind: VerificationKind, party_proofs: Vec, pre_dishonest: BTreeSet, ec: EventContext, ) { let e3_id_str = e3_id.to_string(); + let label = match &kind { + VerificationKind::ShareProofs => "C2/C3", + VerificationKind::ThresholdDecryptionProofs => "C6", + VerificationKind::PkGenerationProofs => "C1", + VerificationKind::DecryptionProofs => "C4", + }; 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 { @@ -126,9 +135,8 @@ impl ShareVerificationActor { party.sender_party_id, &party.signed_proofs, &e3_id_str, - "C2/C3", + label, ); - party_signed_payloads.insert(party.sender_party_id, party.signed_proofs.clone()); if result.passed { ecdsa_passed_parties.push(party.clone()); } else { @@ -154,7 +162,7 @@ impl ShareVerificationActor { // 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); + self.publish_complete(e3_id, kind, all_dishonest, ec); return; } @@ -168,12 +176,11 @@ impl ShareVerificationActor { correlation_id, PendingVerification { e3_id: e3_id.clone(), - kind: VerificationKind::ShareProofs, + kind: kind.clone(), ec: ec.clone(), ecdsa_dishonest, pre_dishonest, dispatched_party_ids, - party_signed_payloads, party_addresses, }, ); @@ -187,13 +194,13 @@ impl ShareVerificationActor { ); if let Err(err) = self.bus.publish(request, ec.clone()) { - error!("Failed to dispatch ZK verification: {err}"); + error!("Failed to dispatch {} ZK verification: {err}", label); 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); + self.publish_complete(e3_id, kind, all_dishonest, ec); } } } @@ -209,7 +216,6 @@ impl ShareVerificationActor { 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 { @@ -227,7 +233,6 @@ impl ShareVerificationActor { &e3_id_str, "C4", ); - party_signed_payloads.insert(party.sender_party_id, all_signed_cloned); if result.passed { ecdsa_passed_parties.push(party.clone()); @@ -269,7 +274,6 @@ impl ShareVerificationActor { ecdsa_dishonest, pre_dishonest, dispatched_party_ids, - party_signed_payloads, party_addresses, }, ); @@ -316,7 +320,6 @@ impl ShareVerificationActor { 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)), }; @@ -333,7 +336,6 @@ impl ShareVerificationActor { label, sender_party_id ); return EcdsaPartyResult { - sender_party_id, passed: false, failed_payload: Some((signed.clone(), Some(addr))), }; @@ -348,7 +350,6 @@ impl ShareVerificationActor { label, sender_party_id, signed.payload.proof_type, e ); return EcdsaPartyResult { - sender_party_id, passed: false, failed_payload: Some((signed.clone(), expected_addr)), }; @@ -363,7 +364,6 @@ impl ShareVerificationActor { label, sender_party_id, expected_circuits, signed.payload.proof.circuit ); return EcdsaPartyResult { - sender_party_id, passed: false, failed_payload: Some((signed.clone(), expected_addr)), }; @@ -371,7 +371,6 @@ impl ShareVerificationActor { } EcdsaPartyResult { - sender_party_id, passed: true, failed_payload: None, } @@ -388,7 +387,9 @@ impl ShareVerificationActor { let zk_results: Vec = match (&pending.kind, msg.response) { ( - VerificationKind::ShareProofs, + VerificationKind::ShareProofs + | VerificationKind::ThresholdDecryptionProofs + | VerificationKind::PkGenerationProofs, ComputeResponseKind::Zk(ZkResponse::VerifyShareProofs(r)), ) => r.party_results, ( From 704b2863382e216759a2c9b7923644376a1dd78b Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:16:09 +0000 Subject: [PATCH 6/9] chore: cleanup --- crates/aggregator/src/publickey_aggregator.rs | 2 ++ crates/aggregator/src/threshold_plaintext_aggregator.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/aggregator/src/publickey_aggregator.rs b/crates/aggregator/src/publickey_aggregator.rs index 6acbbc7007..16964cee94 100644 --- a/crates/aggregator/src/publickey_aggregator.rs +++ b/crates/aggregator/src/publickey_aggregator.rs @@ -304,8 +304,10 @@ impl PublicKeyAggregator { keyshare_bytes: honest_keyshares, aggregated_pk_bytes: ArcBytes::from_bytes(&pubkey), params_preset: self.params_preset.clone(), + // this field is not really used in the circuit, we only use H committee_n: committee_h, committee_h, + // this field is not really used in the circuit, we only use H committee_threshold: 0, }, public_key: pubkey, diff --git a/crates/aggregator/src/threshold_plaintext_aggregator.rs b/crates/aggregator/src/threshold_plaintext_aggregator.rs index 4575554371..b68bda14ac 100644 --- a/crates/aggregator/src/threshold_plaintext_aggregator.rs +++ b/crates/aggregator/src/threshold_plaintext_aggregator.rs @@ -230,7 +230,7 @@ impl ThresholdPlaintextAggregator { })); } - info!("Changing state to VerifyingC6 because received enough shares..."); + info!("Changing state to VerifyingC6 because we received enough shares..."); Ok(ThresholdPlaintextAggregatorState::VerifyingC6( VerifyingC6 { From f0e513db813255b01f2a4449808017a487025f64 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:15:21 +0000 Subject: [PATCH 7/9] chore: pr comments --- crates/aggregator/src/publickey_aggregator.rs | 4 +++ .../src/threshold_plaintext_aggregator.rs | 9 +++++++ .../enclave_event/decryptionshare_created.rs | 1 + .../src/enclave_event/keyshare_created.rs | 1 + .../src/enclave_event/plaintext_aggregated.rs | 1 + .../src/enclave_event/publickey_aggregated.rs | 1 + .../events/src/enclave_event/signed_proof.rs | 6 ++--- crates/multithread/src/multithread.rs | 27 +++++++++---------- 8 files changed, 32 insertions(+), 18 deletions(-) diff --git a/crates/aggregator/src/publickey_aggregator.rs b/crates/aggregator/src/publickey_aggregator.rs index 16964cee94..add768383a 100644 --- a/crates/aggregator/src/publickey_aggregator.rs +++ b/crates/aggregator/src/publickey_aggregator.rs @@ -218,6 +218,10 @@ impl PublicKeyAggregator { return Ok(()); } + if msg.e3_id != self.e3_id { + return Ok(()); + } + let PublicKeyAggregatorState::VerifyingC1 { keyshares, nodes, diff --git a/crates/aggregator/src/threshold_plaintext_aggregator.rs b/crates/aggregator/src/threshold_plaintext_aggregator.rs index b68bda14ac..2df1bd046e 100644 --- a/crates/aggregator/src/threshold_plaintext_aggregator.rs +++ b/crates/aggregator/src/threshold_plaintext_aggregator.rs @@ -283,6 +283,10 @@ impl ThresholdPlaintextAggregator { return Ok(()); } + if msg.e3_id != self.e3_id { + return Ok(()); + } + let state: VerifyingC6 = self .state .get() @@ -421,6 +425,11 @@ impl ThresholdPlaintextAggregator { msg: TypedEvent, ) -> Result<()> { let (msg, ec) = msg.into_components(); + + if msg.e3_id != self.e3_id { + return Ok(()); + } + let state: GeneratingC7Proof = self .state .get() diff --git a/crates/events/src/enclave_event/decryptionshare_created.rs b/crates/events/src/enclave_event/decryptionshare_created.rs index 344902ca02..e34e096b8f 100644 --- a/crates/events/src/enclave_event/decryptionshare_created.rs +++ b/crates/events/src/enclave_event/decryptionshare_created.rs @@ -19,6 +19,7 @@ pub struct DecryptionshareCreated { pub e3_id: E3id, pub node: String, /// C6 proofs: one signed proof of correct decryption per ciphertext index. + #[serde(default)] pub signed_decryption_proofs: Vec, } diff --git a/crates/events/src/enclave_event/keyshare_created.rs b/crates/events/src/enclave_event/keyshare_created.rs index 2c2d614c06..19795149d0 100644 --- a/crates/events/src/enclave_event/keyshare_created.rs +++ b/crates/events/src/enclave_event/keyshare_created.rs @@ -19,6 +19,7 @@ pub struct KeyshareCreated { pub pubkey: ArcBytes, pub e3_id: E3id, pub node: String, + #[serde(default)] pub signed_pk_generation_proof: Option, } diff --git a/crates/events/src/enclave_event/plaintext_aggregated.rs b/crates/events/src/enclave_event/plaintext_aggregated.rs index f3964173c0..bda9a2f3a0 100644 --- a/crates/events/src/enclave_event/plaintext_aggregated.rs +++ b/crates/events/src/enclave_event/plaintext_aggregated.rs @@ -18,6 +18,7 @@ pub struct PlaintextAggregated { pub e3_id: E3id, pub decrypted_output: Vec, /// C7 proofs: one proof of correct aggregation per ciphertext index. + #[serde(default)] pub aggregation_proofs: Vec, } diff --git a/crates/events/src/enclave_event/publickey_aggregated.rs b/crates/events/src/enclave_event/publickey_aggregated.rs index df6f2de778..128e8f368f 100644 --- a/crates/events/src/enclave_event/publickey_aggregated.rs +++ b/crates/events/src/enclave_event/publickey_aggregated.rs @@ -20,6 +20,7 @@ pub struct PublicKeyAggregated { pub e3_id: E3id, pub nodes: OrderedSet, /// C5 proof: proof of correct pk aggregation. + #[serde(default)] pub pk_aggregation_proof: Option, } diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index 4bdc1b54fd..f7634ffc68 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -22,10 +22,8 @@ use e3_utils::utility_types::ArcBytes; use serde::{Deserialize, Serialize}; use std::fmt::{self, Display}; -/// Proof type identifier covering all node-generated proofs. -/// -/// Aggregation proofs (Proofs 5 and 7) are excluded — they are published on-chain -/// directly and verified by the contract at submission time. +/// Proof type identifier covering all node-generated proofs, including +/// aggregation proofs (C5 pk aggregation and C7 decrypted shares aggregation). #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ProofType { diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index 0d9759ea47..55503e9764 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -37,6 +37,7 @@ use e3_events::{ ZkResponse, }; use e3_fhe_params::build_pair_for_preset; +use e3_fhe_params::create_deterministic_crp_from_default_seed; use e3_fhe_params::{BfvParamSet, BfvPreset}; use e3_polynomial::CrtPolynomial; use e3_trbfv::calculate_decryption_key::calculate_decryption_key; @@ -46,6 +47,7 @@ use e3_trbfv::gen_esi_sss::gen_esi_sss; use e3_trbfv::gen_pk_share_and_sk_sss::gen_pk_share_and_sk_sss; use e3_trbfv::helpers::deserialize_secret_key; use e3_trbfv::helpers::try_poly_from_bytes; +use e3_trbfv::helpers::try_poly_from_sensitive_bytes; use e3_trbfv::shares::SharedSecret; use e3_trbfv::{TrBFVError, TrBFVRequest, TrBFVResponse}; use e3_utils::SharedRng; @@ -64,8 +66,10 @@ use e3_zk_helpers::dkg::share_decryption::{ShareDecryptionCircuit, ShareDecrypti use e3_zk_helpers::dkg::share_encryption::{ShareEncryptionCircuit, ShareEncryptionCircuitData}; use e3_zk_helpers::threshold::pk_aggregation::PkAggregationCircuit; use e3_zk_helpers::threshold::pk_aggregation::PkAggregationCircuitData; +use e3_zk_helpers::CiphernodesCommittee; use e3_zk_prover::{Provable, ZkBackend, ZkProver}; use fhe::bfv::{Ciphertext, Encoding, Plaintext, PublicKey, SecretKey}; +use fhe::mbfv::PublicKeyShare; use fhe_traits::{DeserializeParametrized, FheEncoder}; use ndarray::Array2; use num_bigint::BigInt; @@ -277,16 +281,15 @@ fn handle_pk_aggregation_proof( .map_err(|e| make_zk_error(&request, format!("build_pair_for_preset: {}", e)))?; // 2. Create deterministic CRP - let crp = e3_fhe_params::create_deterministic_crp_from_default_seed(&threshold_params); + let crp = create_deterministic_crp_from_default_seed(&threshold_params); // 3. Deserialize each keyshare as PublicKeyShare and extract pk0 let mut pk0_shares = Vec::with_capacity(req.keyshare_bytes.len()); for (i, ks_bytes) in req.keyshare_bytes.iter().enumerate() { - let pk_share = - fhe::mbfv::PublicKeyShare::deserialize(ks_bytes, &threshold_params, crp.clone()) - .map_err(|e| { - make_zk_error(&request, format!("keyshare[{}] deserialize: {:?}", i, e)) - })?; + let pk_share = PublicKeyShare::deserialize(ks_bytes, &threshold_params, crp.clone()) + .map_err(|e| { + make_zk_error(&request, format!("keyshare[{}] deserialize: {:?}", i, e)) + })?; pk0_shares.push(CrtPolynomial::from_fhe_polynomial(&pk_share.p0_share())); } @@ -298,7 +301,7 @@ fn handle_pk_aggregation_proof( let a = CrtPolynomial::from_fhe_polynomial(&crp.poly()); // 6. Build committee and circuit data - let committee = e3_zk_helpers::CiphernodesCommittee { + let committee = CiphernodesCommittee { n: req.committee_n, h: req.committee_h, threshold: req.committee_threshold, @@ -346,12 +349,8 @@ fn handle_threshold_share_decryption_proof( .map_err(|e| make_zk_error(&request, format!("aggregated_pk deserialize: {:?}", e)))?; // 3. Decrypt sk_poly_sum → Poly → CrtPolynomial (s) - let sk_poly = e3_trbfv::helpers::try_poly_from_sensitive_bytes( - req.sk_poly_sum, - threshold_params.clone(), - cipher, - ) - .map_err(|e| make_zk_error(&request, format!("sk_poly_sum decrypt: {}", e)))?; + let sk_poly = try_poly_from_sensitive_bytes(req.sk_poly_sum, threshold_params.clone(), cipher) + .map_err(|e| make_zk_error(&request, format!("sk_poly_sum decrypt: {}", e)))?; let s = CrtPolynomial::from_fhe_polynomial(&sk_poly); // 4. For each index, build circuit data and generate proof @@ -382,7 +381,7 @@ fn handle_threshold_share_decryption_proof( // Currently there is a single smudging noise polynomial shared across all // ciphertexts (see calculate_decryption_share.rs). let es_idx = i % req.es_poly_sum.len(); - let e_poly = e3_trbfv::helpers::try_poly_from_sensitive_bytes( + let e_poly = try_poly_from_sensitive_bytes( req.es_poly_sum[es_idx].clone(), threshold_params.clone(), cipher, From 488fcbbcfde2f3d0ae26aff67a37ccda852a9070 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:13:40 +0000 Subject: [PATCH 8/9] chore: pr comments --- crates/aggregator/src/publickey_aggregator.rs | 4 ++ .../src/threshold_plaintext_aggregator.rs | 57 ++---------------- crates/multithread/src/multithread.rs | 9 ++- crates/zk-prover/src/actors/proof_request.rs | 60 +++++++++++++------ templates/default/tests/integration.spec.ts | 2 +- 5 files changed, 61 insertions(+), 71 deletions(-) diff --git a/crates/aggregator/src/publickey_aggregator.rs b/crates/aggregator/src/publickey_aggregator.rs index add768383a..ee770970b3 100644 --- a/crates/aggregator/src/publickey_aggregator.rs +++ b/crates/aggregator/src/publickey_aggregator.rs @@ -329,6 +329,10 @@ impl PublicKeyAggregator { ) -> Result<()> { let (msg, ec) = msg.into_components(); + if msg.e3_id != self.e3_id { + return Ok(()); + } + let PublicKeyAggregatorState::GeneratingC5Proof { public_key, public_key_hash, diff --git a/crates/aggregator/src/threshold_plaintext_aggregator.rs b/crates/aggregator/src/threshold_plaintext_aggregator.rs index 2df1bd046e..d5589270df 100644 --- a/crates/aggregator/src/threshold_plaintext_aggregator.rs +++ b/crates/aggregator/src/threshold_plaintext_aggregator.rs @@ -159,15 +159,6 @@ impl ThresholdPlaintextAggregatorState { } } -#[derive(Message)] -#[rtype("()")] -pub struct ComputeAggregate { - pub shares: Vec<(u64, Vec)>, - pub ciphertext_output: Vec, - pub threshold_m: u64, - pub threshold_n: u64, -} - pub struct ThresholdPlaintextAggregator { bus: BusHandle, sortition: Addr, @@ -218,7 +209,7 @@ impl ThresholdPlaintextAggregator { shares.insert(party_id, share); c6_proofs.insert(party_id, decryption_proofs); - if shares.len() <= threshold_m as usize { + if (shares.len() as u64) < threshold_n { return Ok(ThresholdPlaintextAggregatorState::Collecting(Collecting { params, threshold_n, @@ -230,7 +221,10 @@ impl ThresholdPlaintextAggregator { })); } - info!("Changing state to VerifyingC6 because we received enough shares..."); + info!( + "Changing state to VerifyingC6 because received all {} shares...", + threshold_n + ); Ok(ThresholdPlaintextAggregatorState::VerifyingC6( VerifyingC6 { @@ -362,36 +356,6 @@ impl ThresholdPlaintextAggregator { Ok(()) } - pub fn handle_compute_aggregate(&mut self, msg: TypedEvent) -> Result<()> { - let (msg, ec) = msg.into_components(); - info!("create_calculate_threshold_decryption_event..."); - - let e3_id = self.e3_id.clone(); - let state: Computing = self - .state - .get() - .ok_or(anyhow!("Could not get state"))? - .try_into()?; - - let trbfv_config = - TrBFVConfig::new(state.params.clone(), state.threshold_n, state.threshold_m); - - let event = ComputeRequest::trbfv( - TrBFVRequest::CalculateThresholdDecryption( - CalculateThresholdDecryptionRequest { - ciphertexts: msg.ciphertext_output, - trbfv_config, - d_share_polys: msg.shares, - } - .into(), - ), - CorrelationId::new(), - e3_id, - ); - self.bus.publish(event, ec)?; - Ok(()) - } - /// Publish AggregationProofPending for C7 proof generation through ProofRequestActor. pub fn dispatch_c7_proof_request( &mut self, @@ -626,17 +590,6 @@ impl Handler>> } } -impl Handler> for ThresholdPlaintextAggregator { - type Result = (); - fn handle(&mut self, msg: TypedEvent, _: &mut Self::Context) -> Self::Result { - trap( - EType::PlaintextAggregation, - &self.bus.with_ec(msg.get_ctx()), - || self.handle_compute_aggregate(msg), - ) - } -} - impl Handler> for ThresholdPlaintextAggregator { type Result = (); fn handle(&mut self, msg: TypedEvent, _: &mut Self::Context) -> Self::Result { diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index 55503e9764..a27436aed2 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -1160,7 +1160,14 @@ fn handle_decrypted_shares_aggregation_proof( // party_ids in strictly increasing order for Lagrange sign computation. req.d_share_polys.sort_by_key(|(id, _)| *id); - // 3. Determine dimensions + // 3. The circuit expects exactly threshold + 1 shares for Lagrange interpolation. + // We may have more honest parties than needed, so take the first threshold + 1. + let required = req.threshold_m as usize + 1; + if req.d_share_polys.len() > required { + req.d_share_polys.truncate(required); + } + + // 4. Determine dimensions let num_indices = req.plaintext.len(); let num_parties = req.d_share_polys.len(); diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index 232fb3d7b4..bf9420180c 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -13,12 +13,12 @@ use e3_events::{ AggregationProofPending, AggregationProofSigned, BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeResponse, ComputeResponseKind, CorrelationId, DecryptionKeyShared, DecryptionShareProofSigned, DecryptionShareProofsPending, - DecryptionshareCreated, DkgProofSigned, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, - EncryptionKeyCreated, EncryptionKeyPending, EventContext, EventPublisher, EventSubscriber, - EventType, PkAggregationProofPending, PkAggregationProofSigned, PkBfvProofRequest, - PkGenerationProofSigned, Proof, ProofPayload, ProofType, Sequenced, - ShareDecryptionProofPending, SignedProofPayload, ThresholdShare, ThresholdShareCreated, - ThresholdSharePending, TypedEvent, ZkRequest, ZkResponse, + DecryptionshareCreated, DkgProofSigned, E3Failed, E3Stage, E3id, EnclaveEvent, + EnclaveEventData, EncryptionKey, EncryptionKeyCreated, EncryptionKeyPending, EventContext, + EventPublisher, EventSubscriber, EventType, FailureReason, PkAggregationProofPending, + PkAggregationProofSigned, PkBfvProofRequest, PkGenerationProofSigned, Proof, ProofPayload, + ProofType, Sequenced, ShareDecryptionProofPending, SignedProofPayload, ThresholdShare, + ThresholdShareCreated, ThresholdSharePending, TypedEvent, ZkRequest, ZkResponse, }; use e3_utils::utility_types::ArcBytes; use e3_utils::NotifySync; @@ -719,7 +719,8 @@ impl ProofRequestActor { ); let ec = pending.ec; - if let Err(err) = self.bus.publish( + + match self.bus.publish( DecryptionshareCreated { party_id: pending.party_id, node: pending.node, @@ -729,16 +730,19 @@ impl ProofRequestActor { }, ec.clone(), ) { - error!("Failed to publish DecryptionshareCreated: {err}"); - } - - if let Err(err) = self.bus.publish( - DecryptionShareProofSigned { - e3_id: e3_id.clone(), - }, - ec, - ) { - error!("Failed to publish DecryptionShareProofSigned: {err}"); + Ok(_) => { + if let Err(err) = self.bus.publish( + DecryptionShareProofSigned { + e3_id: e3_id.clone(), + }, + ec, + ) { + error!("Failed to publish DecryptionShareProofSigned: {err}"); + } + } + Err(err) => { + error!("Failed to publish DecryptionshareCreated: {err}"); + } } } @@ -1197,6 +1201,7 @@ impl ProofRequestActor { } fn handle_compute_request_error(&mut self, msg: TypedEvent) { + let (msg, ec) = msg.into_components(); let ComputeRequestErrorKind::Zk(err) = msg.get_err() else { return; }; @@ -1245,6 +1250,17 @@ impl ProofRequestActor { e3_id ); self.pending_pk_aggregation.remove(&e3_id); + if let Err(e) = self.bus.publish( + E3Failed { + e3_id, + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::DKGInvalidShares, + }, + ec, + ) { + error!("Failed to publish E3Failed for C5 error: {e}"); + } + return; } if let Some(e3_id) = self.aggregation_correlation.remove(msg.correlation_id()) { @@ -1253,6 +1269,16 @@ impl ProofRequestActor { e3_id ); self.pending_aggregation.remove(&e3_id); + if let Err(e) = self.bus.publish( + E3Failed { + e3_id, + failed_at_stage: E3Stage::CiphertextReady, + reason: FailureReason::DecryptionInvalidShares, + }, + ec, + ) { + error!("Failed to publish E3Failed for C7 error: {e}"); + } } } } diff --git a/templates/default/tests/integration.spec.ts b/templates/default/tests/integration.spec.ts index ea704437a6..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 = 300 + const duration = 225 const inputWindow = await calculateInputWindow(publicClient, duration) const thresholdBfvParams = await sdk.getThresholdBfvParamsSet() const e3ProgramParams = encodeBfvParams(thresholdBfvParams) From bf7e462754361d2c8c766a009aa3249f4aa4d556 Mon Sep 17 00:00:00 2001 From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:20:09 +0000 Subject: [PATCH 9/9] chore: ensure we throw e3 failed and fix ordering issue --- crates/aggregator/src/publickey_aggregator.rs | 66 ++++++++------- .../src/threshold_plaintext_aggregator.rs | 83 ++++++++++--------- .../src/ciphernode_builder.rs | 2 +- crates/keyshare/src/threshold_keyshare.rs | 26 +++--- crates/zk-prover/src/actors/proof_request.rs | 16 +++- 5 files changed, 107 insertions(+), 86 deletions(-) diff --git a/crates/aggregator/src/publickey_aggregator.rs b/crates/aggregator/src/publickey_aggregator.rs index ee770970b3..f4dad40569 100644 --- a/crates/aggregator/src/publickey_aggregator.rs +++ b/crates/aggregator/src/publickey_aggregator.rs @@ -259,12 +259,12 @@ impl PublicKeyAggregator { ); } - // Check remaining count >= threshold - if honest_keyshares.len() < threshold_m { + // Need at least threshold + 1 honest parties for aggregation + if honest_keyshares.len() <= threshold_m { return Err(anyhow::anyhow!( - "Not enough honest parties after C1 verification: {} < {}", + "Not enough honest parties after C1 verification: {} (need at least {})", honest_keyshares.len(), - threshold_m + threshold_m + 1 )); } @@ -287,17 +287,9 @@ impl PublicKeyAggregator { let honest_nodes_set = OrderedSet::from(honest_nodes); - // Transition to GeneratingC5Proof - self.state.try_mutate(&ec, |_| { - Ok(PublicKeyAggregatorState::GeneratingC5Proof { - public_key: pubkey.clone(), - public_key_hash, - keyshare_bytes: honest_keyshares.clone(), - nodes: honest_nodes_set.clone(), - }) - })?; - - // Dispatch C5 proof generation through ProofRequestActor + // Publish pending event before transitioning state so a publish + // failure leaves us in VerifyingC1 (retryable) rather than + // GeneratingC5Proof (no retry path). let committee_h = honest_keyshares.len(); info!("Publishing PkAggregationProofPending for C5 proof generation..."); @@ -305,7 +297,7 @@ impl PublicKeyAggregator { PkAggregationProofPending { e3_id: self.e3_id.clone(), proof_request: PkAggregationProofRequest { - keyshare_bytes: honest_keyshares, + keyshare_bytes: honest_keyshares.clone(), aggregated_pk_bytes: ArcBytes::from_bytes(&pubkey), params_preset: self.params_preset.clone(), // this field is not really used in the circuit, we only use H @@ -314,12 +306,22 @@ impl PublicKeyAggregator { // this field is not really used in the circuit, we only use H committee_threshold: 0, }, - public_key: pubkey, + public_key: pubkey.clone(), public_key_hash, - nodes: honest_nodes_set, + nodes: honest_nodes_set.clone(), }, - ec, + ec.clone(), )?; + + // Transition to GeneratingC5Proof + self.state.try_mutate(&ec, |_| { + Ok(PublicKeyAggregatorState::GeneratingC5Proof { + public_key: pubkey, + public_key_hash, + keyshare_bytes: honest_keyshares, + nodes: honest_nodes_set, + }) + })?; Ok(()) } @@ -352,24 +354,26 @@ impl PublicKeyAggregator { let proof = msg.signed_proof.payload.proof; + // Publish PublicKeyAggregated before transitioning state so a publish + // failure leaves us in GeneratingC5Proof (retryable) rather than + // Complete (no retry path). + let event = PublicKeyAggregated { + pubkey: public_key.clone(), + public_key_hash, + e3_id: self.e3_id.clone(), + nodes: nodes.clone(), + pk_aggregation_proof: Some(proof), + }; + self.bus.publish(event, ec.clone())?; + // Transition to Complete self.state.try_mutate(&ec, |_| { Ok(PublicKeyAggregatorState::Complete { - public_key: public_key.clone(), + public_key, keyshares: OrderedSet::new(), - nodes: nodes.clone(), + nodes, }) })?; - - // Publish PublicKeyAggregated with C5 proof - let event = PublicKeyAggregated { - pubkey: public_key, - public_key_hash, - e3_id: self.e3_id.clone(), - nodes, - pk_aggregation_proof: Some(proof), - }; - self.bus.publish(event, ec)?; Ok(()) } } diff --git a/crates/aggregator/src/threshold_plaintext_aggregator.rs b/crates/aggregator/src/threshold_plaintext_aggregator.rs index d5589270df..4ce3ff0d84 100644 --- a/crates/aggregator/src/threshold_plaintext_aggregator.rs +++ b/crates/aggregator/src/threshold_plaintext_aggregator.rs @@ -316,42 +316,35 @@ impl ThresholdPlaintextAggregator { honest_shares.len() ); - self.state.try_mutate(&ec, |_| { - Ok(ThresholdPlaintextAggregatorState::Computing(Computing { - shares: honest_shares.clone(), - ciphertext_output: state.ciphertext_output.clone(), - threshold_m: state.threshold_m, - threshold_n: state.threshold_n, - params: state.params.clone(), - })) - })?; - - // Dispatch TrBFV computation - let computing: Computing = self - .state - .get() - .ok_or(anyhow!("Could not get state after C6 verification"))? - .try_into()?; - - let trbfv_config = TrBFVConfig::new( - computing.params.clone(), - computing.threshold_n, - computing.threshold_m, - ); + // Publish ComputeRequest before transitioning state so a publish + // failure leaves us in VerifyingC6 (retryable) rather than + // Computing (no retry path). + let trbfv_config = + TrBFVConfig::new(state.params.clone(), state.threshold_n, state.threshold_m); let event = ComputeRequest::trbfv( TrBFVRequest::CalculateThresholdDecryption( CalculateThresholdDecryptionRequest { - ciphertexts: computing.ciphertext_output.clone(), + ciphertexts: state.ciphertext_output.clone(), trbfv_config, - d_share_polys: computing.shares.clone(), + d_share_polys: honest_shares.clone(), } .into(), ), CorrelationId::new(), self.e3_id.clone(), ); - self.bus.publish(event, ec)?; + self.bus.publish(event, ec.clone())?; + + self.state.try_mutate(&ec, |_| { + Ok(ThresholdPlaintextAggregatorState::Computing(Computing { + shares: honest_shares, + ciphertext_output: state.ciphertext_output, + threshold_m: state.threshold_m, + threshold_n: state.threshold_n, + params: state.params, + })) + })?; Ok(()) } @@ -417,22 +410,24 @@ impl ThresholdPlaintextAggregator { plaintext.len() ); - self.state.try_mutate(&ec, |_| { - Ok(ThresholdPlaintextAggregatorState::Complete(Complete { - decrypted: plaintext.clone(), - shares: shares.clone(), - })) - })?; - - // Dispatch the PlaintextAggregated event with C7 proofs + // Publish PlaintextAggregated before transitioning state so a publish + // failure leaves us in GeneratingC7Proof (retryable) rather than + // Complete (no retry path). let event = PlaintextAggregated { - decrypted_output: plaintext, + decrypted_output: plaintext.clone(), e3_id: self.e3_id.clone(), aggregation_proofs: proofs, }; info!("Dispatching plaintext event with C7 proofs {:?}", event); - self.bus.publish(event, ec)?; + self.bus.publish(event, ec.clone())?; + + self.state.try_mutate(&ec, |_| { + Ok(ThresholdPlaintextAggregatorState::Complete(Complete { + decrypted: plaintext, + shares, + })) + })?; Ok(()) } @@ -459,20 +454,28 @@ impl ThresholdPlaintextAggregator { let threshold_m = state.threshold_m; let threshold_n = state.threshold_n; + // Publish pending event before transitioning state so a publish + // failure leaves us in Computing (retryable) rather than + // GeneratingC7Proof (no retry path). + self.dispatch_c7_proof_request( + shares.clone(), + plaintext.clone(), + threshold_m, + threshold_n, + ec.clone(), + )?; + // Transition to GeneratingC7Proof self.state.try_mutate(&ec, |_| { Ok(ThresholdPlaintextAggregatorState::GeneratingC7Proof( GeneratingC7Proof { threshold_m, threshold_n, - shares: shares.clone(), - plaintext: plaintext.clone(), + shares, + plaintext, }, )) })?; - - // Dispatch C7 proof request through ProofRequestActor - self.dispatch_c7_proof_request(shares, plaintext, threshold_m, threshold_n, ec)?; } _ => { diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index fe90124638..1bd273cede 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -488,7 +488,7 @@ impl CiphernodeBuilder { if self.threshold_plaintext_agg { info!("Setting up ThresholdPlaintextAggregatorExtension"); let _ = self.ensure_multithread(&bus); - let aggregator_preset = BfvPreset::InsecureThreshold512; + let aggregator_preset = DEFAULT_BFV_PRESET; e3_builder = e3_builder.with(ThresholdPlaintextAggregatorExtension::create( &bus, &sortition, diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 8ce8c3cb30..86d308ba55 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -1239,10 +1239,10 @@ impl ThresholdKeyshare { let dishonest_count = (pre_dishonest.len() as u64).min(total); let honest_count = total - dishonest_count; - if honest_count < threshold { + 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 + "Too few honest parties for E3 {} ({} honest, need at least {}) after C2/C3 pre-dishonest filtering — cannot proceed", + e3_id, honest_count, threshold + 1 ); self.pending_shares.clear(); self.bus.publish( @@ -1305,10 +1305,10 @@ impl ThresholdKeyshare { let dishonest_count = (msg.dishonest_parties.len() as u64).min(total); let honest_count = total - dishonest_count; - if honest_count < threshold { + if honest_count <= threshold { warn!( - "Too few honest parties for E3 {} ({} honest < {} threshold) — cannot proceed", - e3_id, honest_count, threshold + "Too few honest parties for E3 {} ({} honest, need at least {}) — cannot proceed", + e3_id, honest_count, threshold + 1 ); // Clear pending shares self.pending_shares.clear(); @@ -1347,10 +1347,10 @@ impl ThresholdKeyshare { .map(|h| h.len() as u64) .unwrap_or(0); - if honest_count < threshold { + if honest_count <= threshold { warn!( - "Too few honest parties after C4 for E3 {} ({} honest < {} threshold)", - e3_id, honest_count, threshold + "Too few honest parties after C4 for E3 {} ({} honest, need at least {})", + e3_id, honest_count, threshold + 1 ); self.bus.publish( E3Failed { @@ -1522,7 +1522,7 @@ impl ThresholdKeyshare { ); // Re-check threshold after exclusion let threshold = state.threshold_m; - if (honest_shares.len() as u64) < threshold { + if (honest_shares.len() as u64) <= threshold { self.pending_shares.clear(); self.bus.publish( E3Failed { @@ -1851,10 +1851,10 @@ impl ThresholdKeyshare { .map(|h| h.len() as u64) .unwrap_or(0); - if honest_count < threshold { + if honest_count <= threshold { warn!( - "Too few honest parties after C4 pre-filtering for E3 {} ({} honest < {} threshold)", - e3_id, honest_count, threshold + "Too few honest parties after C4 pre-filtering for E3 {} ({} honest, need at least {})", + e3_id, honest_count, threshold + 1 ); self.bus.publish( E3Failed { diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index bf9420180c..acaf63b3c1 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -1211,6 +1211,7 @@ impl ProofRequestActor { "T0 proof request failed for E3 {}: {err} — key will not be published without proof", pending.e3_id ); + return; } if let Some((e3_id, kind)) = self.threshold_correlation.remove(msg.correlation_id()) { @@ -1221,6 +1222,7 @@ impl ProofRequestActor { self.threshold_correlation .retain(|_, (eid, _)| *eid != e3_id); self.pending_threshold.remove(&e3_id); + return; } if let Some((e3_id, kind)) = self.decryption_correlation.remove(msg.correlation_id()) { @@ -1231,6 +1233,7 @@ impl ProofRequestActor { self.decryption_correlation .retain(|_, (eid, _)| *eid != e3_id); self.pending_decryption.remove(&e3_id); + return; } if let Some(e3_id) = self @@ -1242,6 +1245,17 @@ impl ProofRequestActor { e3_id ); self.pending_share_decryption.remove(&e3_id); + if let Err(e) = self.bus.publish( + E3Failed { + e3_id, + failed_at_stage: E3Stage::CiphertextReady, + reason: FailureReason::DecryptionInvalidShares, + }, + ec.clone(), + ) { + error!("Failed to publish E3Failed for C6 error: {e}"); + } + return; } if let Some(e3_id) = self.pk_aggregation_correlation.remove(msg.correlation_id()) { @@ -1256,7 +1270,7 @@ impl ProofRequestActor { failed_at_stage: E3Stage::CommitteeFinalized, reason: FailureReason::DKGInvalidShares, }, - ec, + ec.clone(), ) { error!("Failed to publish E3Failed for C5 error: {e}"); }