diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index b44edddbbf..b7e936fba0 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -435,6 +435,15 @@ impl CiphernodeBuilder { // Currently hardcoded to InsecureDkg512 for DKG operations. // Production deployments should use BfvPreset::SecureDkg8192. let share_enc_preset = BfvPreset::InsecureDkg512; + + let backend = self + .zk_backend + .as_ref() + .ok_or_else(|| anyhow::anyhow!("ZK backend is required for threshold keyshare"))?; + + // Ensure signer is available before setting up extensions that need it + let signer = provider_cache.ensure_signer().await?; + info!("Setting up ThresholdKeyshareExtension"); e3_builder = e3_builder.with(ThresholdKeyshareExtension::create( &bus, @@ -443,12 +452,7 @@ impl CiphernodeBuilder { share_enc_preset, )); - let backend = self - .zk_backend - .as_ref() - .ok_or_else(|| anyhow::anyhow!("ZK backend is required for threshold keyshare"))?; info!("Setting up ZK actors"); - let signer = provider_cache.ensure_signer().await?; setup_zk_actors(&bus, backend, signer); } diff --git a/crates/ciphernode-builder/src/eventbus_factory.rs b/crates/ciphernode-builder/src/eventbus_factory.rs index 268678ea2a..8842010150 100644 --- a/crates/ciphernode-builder/src/eventbus_factory.rs +++ b/crates/ciphernode-builder/src/eventbus_factory.rs @@ -6,11 +6,8 @@ use actix::Actor; use actix::Addr; -use e3_config::AppConfig; -use e3_data::Repositories; use e3_events::Disabled; use e3_events::EventType; -use e3_evm::EthPrivateKeyRepositoryFactory; use once_cell::sync::Lazy; use std::any::Any; use std::any::TypeId; diff --git a/crates/ciphernode-builder/src/evm_system.rs b/crates/ciphernode-builder/src/evm_system.rs index 5d88eb4c90..0609b7b15f 100644 --- a/crates/ciphernode-builder/src/evm_system.rs +++ b/crates/ciphernode-builder/src/evm_system.rs @@ -8,14 +8,11 @@ use std::mem::replace; use actix::Actor; use alloy::{primitives::Address, providers::Provider}; -use e3_events::{ - run_once, BusHandle, EventExtractor, EventSubscriber, EventType, HistoricalEvmSyncStart, -}; +use e3_events::{run_once, BusHandle, EventSubscriber, EventType, HistoricalEvmSyncStart}; use e3_evm::{ EthProvider, EvmChainGateway, EvmEventProcessor, EvmReadInterface, EvmRouter, Filters, - FixHistoricalOrder, SyncStartExtractor, + FixHistoricalOrder, }; -use e3_utils::actix::oneshot_runner::OneShotRunner; pub trait RouteFn: FnOnce(EvmEventProcessor) -> EvmEventProcessor + Send {} impl RouteFn for F where F: FnOnce(EvmEventProcessor) -> EvmEventProcessor + Send {} diff --git a/crates/cli/src/ciphernode/setup.rs b/crates/cli/src/ciphernode/setup.rs index f351ac1cac..e95a910807 100644 --- a/crates/cli/src/ciphernode/setup.rs +++ b/crates/cli/src/ciphernode/setup.rs @@ -9,12 +9,11 @@ use anyhow::Result; use dialoguer::{theme::ColorfulTheme, Input}; use e3_config::AppConfig; use e3_entrypoint::config::setup; -use e3_utils::{colorize, eth_address_from_private_key, Color}; +use e3_utils::{colorize, Color}; use std::path::PathBuf; use tracing::instrument; use zeroize::Zeroizing; -use crate::password_set; use crate::password_set::ask_for_password; use crate::wallet_set::ask_for_private_key; diff --git a/crates/cli/src/start.rs b/crates/cli/src/start.rs index 2eada86c1e..94d3e59410 100644 --- a/crates/cli/src/start.rs +++ b/crates/cli/src/start.rs @@ -5,7 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use crate::owo; -use anyhow::{anyhow, Result}; +use anyhow::Result; use e3_config::{AppConfig, NodeRole}; use e3_entrypoint::helpers::listen_for_shutdown; use tracing::{info, instrument}; diff --git a/crates/entrypoint/src/start/aggregator_start.rs b/crates/entrypoint/src/start/aggregator_start.rs index 1ad2275633..912c396b40 100644 --- a/crates/entrypoint/src/start/aggregator_start.rs +++ b/crates/entrypoint/src/start/aggregator_start.rs @@ -4,7 +4,6 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use alloy::primitives::Address; use anyhow::Result; use e3_ciphernode_builder::{CiphernodeBuilder, CiphernodeHandle}; use e3_config::AppConfig; diff --git a/crates/events/src/enclave_event/compute_request/zk.rs b/crates/events/src/enclave_event/compute_request/zk.rs index 9772e49ab4..bfe7be131b 100644 --- a/crates/events/src/enclave_event/compute_request/zk.rs +++ b/crates/events/src/enclave_event/compute_request/zk.rs @@ -4,7 +4,8 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::{Proof, ProofType, SignedProofPayload}; +use crate::{Proof, SignedProofPayload}; +use alloy::primitives::Address; use derivative::Derivative; use e3_crypto::SensitiveBytes; use e3_fhe_params::BfvPreset; @@ -271,16 +272,18 @@ pub struct VerifyShareProofsResponse { } /// Verification result for all proofs from a single sender. +/// +/// Used for both C2/C3 and C4 verification results. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct PartyVerificationResult { /// The party whose proofs were verified. pub sender_party_id: u64, /// Whether ALL proofs from this party verified successfully. pub all_verified: bool, - /// If any proof failed: the proof type that failed. - pub failed_proof_type: Option, /// If any proof failed: the signed payload for fault attribution. pub failed_signed_payload: Option, + /// ECDSA-recovered address of the signer (set during verification). + pub recovered_address: Option
, } /// Request to batch-verify C4 proofs from DecryptionKeyShared events. @@ -297,26 +300,17 @@ pub struct VerifyShareDecryptionProofsRequest { pub struct PartyShareDecryptionProofsToVerify { /// The party that generated these proofs. pub sender_party_id: u64, - /// C4a proof (SecretKey decryption). - pub sk_decryption_proof: Proof, - /// C4b proofs (SmudgingNoise decryption), one per smudging noise index. - pub esm_decryption_proofs: Vec, + /// Signed C4a proof (SecretKey decryption). + pub signed_sk_decryption_proof: SignedProofPayload, + /// Signed C4b proofs (SmudgingNoise decryption), one per smudging noise index. + pub signed_esm_decryption_proofs: Vec, } /// Batch verification results for C4 proofs. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct VerifyShareDecryptionProofsResponse { /// Per-party verification results. - pub party_results: Vec, -} - -/// Verification result for C4 proofs from a single sender. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct PartyShareDecryptionVerificationResult { - /// The party whose C4 proofs were verified. - pub sender_party_id: u64, - /// Whether ALL C4 proofs from this party verified successfully. - pub all_verified: bool, + pub party_results: Vec, } /// ZK-specific error variants. diff --git a/crates/events/src/enclave_event/decryption_key_shared.rs b/crates/events/src/enclave_event/decryption_key_shared.rs index 88f1c15217..27a173588d 100644 --- a/crates/events/src/enclave_event/decryption_key_shared.rs +++ b/crates/events/src/enclave_event/decryption_key_shared.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::{E3id, Proof}; +use crate::{E3id, SignedProofPayload}; use actix::Message; use derivative::Derivative; use e3_utils::utility_types::ArcBytes; @@ -27,10 +27,10 @@ pub struct DecryptionKeyShared { pub sk_poly_sum: ArcBytes, /// Lagrange-interpolated aggregated E_SM polynomials (serialized), one per smudging noise. pub es_poly_sum: Vec, - /// C4a proof (SecretKey decryption). - pub sk_decryption_proof: Proof, - /// C4b proofs (SmudgingNoise decryption), one per smudging noise index. - pub esm_decryption_proofs: Vec, + /// ECDSA-signed C4a proof (SecretKey decryption) for verification and fault attribution. + pub signed_sk_decryption_proof: SignedProofPayload, + /// ECDSA-signed C4b proofs (SmudgingNoise decryption), one per smudging noise index. + pub signed_esm_decryption_proofs: Vec, /// Whether this was received from the network. pub external: bool, } diff --git a/crates/events/src/enclave_event/decryption_share_proofs.rs b/crates/events/src/enclave_event/decryption_share_proofs.rs new file mode 100644 index 0000000000..eebb227bb7 --- /dev/null +++ b/crates/events/src/enclave_event/decryption_share_proofs.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! Events for C4 proof generation and signing flow. +//! +//! `DecryptionShareProofsPending` is published by [`ThresholdKeyshare`] when it +//! has computed the decryption data and needs C4 proofs generated and signed. +//! `ProofRequestActor` generates the proofs, signs them, and publishes +//! `DecryptionKeyShared` (Exchange #3) directly. + +use crate::{DkgShareDecryptionProofRequest, E3id}; +use e3_utils::utility_types::ArcBytes; +use serde::{Deserialize, Serialize}; + +/// ThresholdKeyshare → ProofRequestActor: generate and sign C4 proofs. +/// +/// Carries both the proof generation inputs (sk_request, esm_requests) +/// and the protocol data (sk_poly_sum, es_poly_sum, node) so that +/// ProofRequestActor can publish `DecryptionKeyShared` directly. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DecryptionShareProofsPending { + pub e3_id: E3id, + pub party_id: u64, + pub node: String, + /// Decrypted SK polynomial sum (for Exchange #3). + pub sk_poly_sum: ArcBytes, + /// Decrypted ES polynomial sums (for Exchange #3). + pub es_poly_sum: Vec, + /// C4a proof request (SecretKey decryption). + pub sk_request: DkgShareDecryptionProofRequest, + /// C4b proof requests (SmudgingNoise decryption), one per ESI index. + pub esm_requests: Vec, +} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 5276e37409..ccaa29d5fb 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -15,6 +15,7 @@ mod committee_requested; mod compute_request; mod configuration_updated; mod decryption_key_shared; +mod decryption_share_proofs; mod decryptionshare_created; mod die; mod e3_failed; @@ -38,6 +39,7 @@ mod proof; mod publickey_aggregated; mod publish_document; mod share_computation_proof_signed; +mod share_verification; mod shutdown; mod signed_proof; mod sync_effect; @@ -63,6 +65,7 @@ pub use committee_requested::*; pub use compute_request::*; pub use configuration_updated::*; pub use decryption_key_shared::*; +pub use decryption_share_proofs::*; pub use decryptionshare_created::*; pub use die::*; pub use e3_failed::*; @@ -87,6 +90,7 @@ pub use proof::*; pub use publickey_aggregated::*; pub use publish_document::*; pub use share_computation_proof_signed::*; +pub use share_verification::*; pub use shutdown::*; pub use signed_proof::*; use strum::IntoStaticStr; @@ -238,6 +242,9 @@ pub enum EnclaveEventData { ComputeResponse(ComputeResponse), // ComputeResponseReceived ComputeRequestError(ComputeRequestError), // ComputeRequestFailed SignedProofFailed(SignedProofFailed), + DecryptionShareProofsPending(DecryptionShareProofsPending), + ShareVerificationDispatched(ShareVerificationDispatched), + ShareVerificationComplete(ShareVerificationComplete), OutgoingSyncRequested(OutgoingSyncRequested), NetSyncEventsReceived(NetSyncEventsReceived), HistoricalEvmSyncStart(HistoricalEvmSyncStart), @@ -492,6 +499,9 @@ impl EnclaveEventData { EnclaveEventData::ComputeResponse(ref data) => Some(data.e3_id.clone()), EnclaveEventData::TestEvent(ref data) => data.e3_id.clone(), EnclaveEventData::SignedProofFailed(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::DecryptionShareProofsPending(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::ShareVerificationDispatched(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::ShareVerificationComplete(ref data) => Some(data.e3_id.clone()), EnclaveEventData::E3Failed(ref data) => Some(data.e3_id.clone()), EnclaveEventData::E3StageChanged(ref data) => Some(data.e3_id.clone()), _ => None, @@ -560,6 +570,9 @@ impl_event_types!( ComputeResponse, ComputeRequestError, SignedProofFailed, + DecryptionShareProofsPending, + ShareVerificationDispatched, + ShareVerificationComplete, OutgoingSyncRequested, NetSyncEventsReceived, HistoricalEvmSyncStart, diff --git a/crates/events/src/enclave_event/proof.rs b/crates/events/src/enclave_event/proof.rs index 9541f17f7c..f10abfd10d 100644 --- a/crates/events/src/enclave_event/proof.rs +++ b/crates/events/src/enclave_event/proof.rs @@ -96,6 +96,6 @@ impl CircuitName { impl fmt::Display for CircuitName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.as_str()) + write!(f, "{}", self.dir_path()) } } diff --git a/crates/events/src/enclave_event/share_verification.rs b/crates/events/src/enclave_event/share_verification.rs new file mode 100644 index 0000000000..2ca3c37243 --- /dev/null +++ b/crates/events/src/enclave_event/share_verification.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! Events for C2/C3/C4 share proof verification flow. +//! +//! `ShareVerificationDispatched` is published by [`ThresholdKeyshare`] when +//! proof verification is needed. [`ShareVerificationActor`] subscribes and +//! orchestrates ECDSA validation + ZK verification via multithread. +//! +//! `ShareVerificationComplete` is published by [`ShareVerificationActor`] +//! when verification finishes, carrying the set of dishonest party IDs. + +use crate::{E3id, PartyProofsToVerify, PartyShareDecryptionProofsToVerify}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; + +/// Which verification phase this request/result refers to. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum VerificationKind { + /// C2/C3 share proof verification (after AllThresholdSharesCollected). + ShareProofs, + /// C4 share decryption proof verification (after AllDecryptionKeySharesCollected). + DecryptionProofs, +} + +/// ThresholdKeyshare → ShareVerificationActor: verify party proofs. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ShareVerificationDispatched { + pub e3_id: E3id, + pub kind: VerificationKind, + /// C2/C3 party proofs (when kind == ShareProofs). + pub share_proofs: Vec, + /// C4 party proofs (when kind == DecryptionProofs). + pub decryption_proofs: Vec, + /// Parties already identified as dishonest before verification + /// (e.g., missing/incomplete proofs). Merged into the final result. + pub pre_dishonest: BTreeSet, +} + +/// ShareVerificationActor → ThresholdKeyshare: verification results. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ShareVerificationComplete { + pub e3_id: E3id, + pub kind: VerificationKind, + /// All dishonest parties (pre-dishonest + ECDSA-failed + ZK-failed). + pub dishonest_parties: BTreeSet, +} diff --git a/crates/events/src/eventstore_router.rs b/crates/events/src/eventstore_router.rs index 3f3da61f3b..3679957b50 100644 --- a/crates/events/src/eventstore_router.rs +++ b/crates/events/src/eventstore_router.rs @@ -78,7 +78,7 @@ impl Handler for QueryAggregator { impl Handler for QueryAggregator { type Result = (); - fn handle(&mut self, msg: Die, ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, _msg: Die, ctx: &mut Self::Context) -> Self::Result { ctx.stop() } } diff --git a/crates/events/src/snapshot_buffer/batch.rs b/crates/events/src/snapshot_buffer/batch.rs index acbb517557..722cb069c5 100644 --- a/crates/events/src/snapshot_buffer/batch.rs +++ b/crates/events/src/snapshot_buffer/batch.rs @@ -7,7 +7,6 @@ use std::mem::replace; use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Message, Recipient}; use e3_utils::MAILBOX_LIMIT; -use tracing::debug; use crate::{trap, Die, EType, Insert, InsertBatch, PanicDispatcher}; diff --git a/crates/events/src/snapshot_buffer/batch_router.rs b/crates/events/src/snapshot_buffer/batch_router.rs index 71b88f3147..a7882a25da 100644 --- a/crates/events/src/snapshot_buffer/batch_router.rs +++ b/crates/events/src/snapshot_buffer/batch_router.rs @@ -16,7 +16,7 @@ use actix::{Actor, Addr, Handler, Message, Recipient}; use anyhow::Context; use e3_utils::MAILBOX_LIMIT; use std::{collections::HashMap, sync::Arc, time::Duration}; -use tracing::{debug, info, trace, warn}; +use tracing::debug; type Seq = u64; diff --git a/crates/events/src/snapshot_buffer/snapshot_buffer.rs b/crates/events/src/snapshot_buffer/snapshot_buffer.rs index bf73313580..b28ddb14ea 100644 --- a/crates/events/src/snapshot_buffer/snapshot_buffer.rs +++ b/crates/events/src/snapshot_buffer/snapshot_buffer.rs @@ -140,7 +140,7 @@ impl Handler for SnapshotBuffer { impl Handler for SnapshotBuffer { type Result = (); - fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: EnclaveEvent, _ctx: &mut Self::Context) -> Self::Result { trap(EType::IO, &PanicDispatcher::new(), || { if let Some(ref router) = self.router { router.try_send(msg)?; diff --git a/crates/evm/src/evm_parser.rs b/crates/evm/src/evm_parser.rs index e4549fd8ff..473e67629c 100644 --- a/crates/evm/src/evm_parser.rs +++ b/crates/evm/src/evm_parser.rs @@ -7,7 +7,7 @@ use actix::{Actor, Handler}; use e3_events::{hlc::HlcTimestamp, EnclaveEventData}; use e3_utils::MAILBOX_LIMIT; -use tracing::{debug, info}; +use tracing::debug; use crate::{ events::{EnclaveEvmEvent, EvmEventProcessor, EvmLog}, diff --git a/crates/evm/src/evm_read_interface.rs b/crates/evm/src/evm_read_interface.rs index ef832fd4c9..44093726b7 100644 --- a/crates/evm/src/evm_read_interface.rs +++ b/crates/evm/src/evm_read_interface.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::events::{EnclaveEvmEvent, EvmEventProcessor, EvmLog}; +use crate::events::{EnclaveEvmEvent, EvmEventProcessor}; use crate::helpers::EthProvider; use crate::log_fetcher::{backfill_to_head, fetch_logs_chunked, process_log, TimestampTracker}; use crate::HistoricalSyncComplete; @@ -16,14 +16,14 @@ use alloy::providers::Provider; use alloy::rpc::types::Filter; use alloy_primitives::Address; use anyhow::anyhow; -use e3_events::{BusHandle, CorrelationId, ErrorDispatcher, Event, EventSubscriber, EventType}; +use e3_events::{BusHandle, ErrorDispatcher, Event, EventSubscriber, EventType}; use e3_events::{EType, EnclaveEvent, EnclaveEventData, EventId}; use e3_utils::MAILBOX_LIMIT; use futures_util::stream::StreamExt; use std::collections::{HashMap, HashSet}; use tokio::select; use tokio::sync::oneshot; -use tracing::{debug, error, info, instrument, warn}; +use tracing::{error, info, instrument, warn}; const MAX_RECONNECT_DELAY_SECS: u64 = 60; diff --git a/crates/evm/src/fix_historical_order.rs b/crates/evm/src/fix_historical_order.rs index 1ff824290f..ec4746fb7b 100644 --- a/crates/evm/src/fix_historical_order.rs +++ b/crates/evm/src/fix_historical_order.rs @@ -9,7 +9,7 @@ use actix::{Actor, Addr, Handler}; use bloom::{BloomFilter, ASMS}; use e3_events::CorrelationId; use e3_utils::MAILBOX_LIMIT; -use tracing::{debug, info}; +use tracing::debug; pub struct FixHistoricalOrder { dest: EvmEventProcessor, diff --git a/crates/indexer/tests/integration.rs b/crates/indexer/tests/integration.rs index dcb0641d25..52f553ad21 100644 --- a/crates/indexer/tests/integration.rs +++ b/crates/indexer/tests/integration.rs @@ -6,7 +6,7 @@ mod helpers; use alloy::{ - primitives::{Bytes, FixedBytes, Uint}, + primitives::{Bytes, Uint}, sol, }; use e3_bfv_client::compute_pk_commitment; diff --git a/crates/keyshare/src/decryption_key_shared_collector.rs b/crates/keyshare/src/decryption_key_shared_collector.rs index 9ef48dfcc8..e6fc4872b3 100644 --- a/crates/keyshare/src/decryption_key_shared_collector.rs +++ b/crates/keyshare/src/decryption_key_shared_collector.rs @@ -141,6 +141,7 @@ impl Handler> for DecryptionKeySharedCollector { ec, ); self.parent.do_send(event); + ctx.stop(); } info!( diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 2121e7b1eb..5be5aadebb 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -11,16 +11,15 @@ use e3_data::Persistable; use e3_events::{ prelude::*, trap, BusHandle, CiphernodeSelected, CiphertextOutputPublished, ComputeRequest, ComputeResponse, ComputeResponseKind, CorrelationId, DecryptionKeyShared, - DecryptionshareCreated, Die, DkgProofSigned, DkgShareDecryptionProofRequest, - DkgShareDecryptionProofResponse, E3Failed, E3RequestComplete, E3Stage, E3id, EType, + DecryptionShareProofsPending, DecryptionshareCreated, Die, DkgProofSigned, + DkgShareDecryptionProofRequest, E3Failed, E3RequestComplete, E3Stage, E3id, EType, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCollectionFailed, EncryptionKeyCreated, EncryptionKeyPending, EventContext, FailureReason, KeyshareCreated, PartyId, PartyProofsToVerify, PartyShareDecryptionProofsToVerify, PkGenerationProofRequest, - PkGenerationProofSigned, Proof, ProofType, Sequenced, ShareComputationProofRequest, - ShareEncryptionProofRequest, SignedProofPayload, ThresholdShare, - ThresholdShareCollectionFailed, ThresholdShareCreated, ThresholdSharePending, TypedEvent, - VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, - VerifyShareProofsRequest, VerifyShareProofsResponse, ZkRequest, ZkResponse, + PkGenerationProofSigned, ProofType, Sequenced, ShareComputationProofRequest, + ShareEncryptionProofRequest, ShareVerificationComplete, ShareVerificationDispatched, + SignedProofPayload, ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated, + ThresholdSharePending, TypedEvent, VerificationKind, }; use e3_fhe_params::create_deterministic_crp_from_default_seed; use e3_fhe_params::{build_pair_for_preset, BfvParamSet, BfvPreset}; @@ -43,7 +42,7 @@ use fhe::bfv::{PublicKey, SecretKey}; use fhe_traits::{DeserializeParametrized, Serialize}; use rand::rngs::OsRng; use std::{ - collections::{HashMap, HashSet}, + collections::{BTreeSet, HashMap, HashSet}, mem, sync::Arc, }; @@ -360,26 +359,21 @@ pub struct ThresholdKeyshare { cipher: Arc, decryption_key_collector: Option>, encryption_key_collector: Option>, - /// Collector for incoming DecryptionKeyShared events (Exchange #3). decryption_key_shared_collector: Option>, state: Persistable, share_enc_preset: BfvPreset, - /// Temporarily holds shares + proofs while C2/C3 proof verification is in flight. - pending_verification_shares: Option>>, - /// C4a proof (SecretKey decryption) — stored after generation, used in Exchange #3. - sk_decryption_proof: Option, - /// C4b proofs (SmudgingNoise decryption) — keyed by esi_idx for deterministic ordering. - esm_decryption_proofs: HashMap, - /// Expected number of C4b proofs (one per smudging noise index). - expected_esm_decryption_count: usize, - /// Maps correlation IDs to esi_idx for C4b proof ordering. - esm_decryption_correlation_map: HashMap, - /// Parties that provided no C2/C3 proofs (treated as dishonest when others did provide proofs). - no_proof_dishonest_parties: Option>, - /// Party IDs sent for C2/C3 verification — used to detect missing results in the response. - expected_verification_parties: Option>, - /// Honest party IDs after C2/C3 verification — used by DecryptionKeySharedCollector and C4. + /// Transient coordination data bridging async gaps — not persisted. + /// Shares pending C2/C3 verification, consumed in `proceed_with_decryption_key_calculation`. + pending_shares: Vec>, + /// Share decryption proof data built during aggregation, consumed after CalculateDecryptionKey. + pending_share_decryption_data: Option<( + DkgShareDecryptionProofRequest, + Vec, + )>, + /// Honest party IDs determined by C2/C3 verification, narrowed by C4. honest_parties: Option>, + /// DecryptionKeyShared events arriving before ReadyForDecryption. + early_decryption_key_shares: HashMap, } impl ThresholdKeyshare { @@ -392,14 +386,10 @@ impl ThresholdKeyshare { decryption_key_shared_collector: None, state: params.state, share_enc_preset: params.share_enc_preset, - pending_verification_shares: None, - sk_decryption_proof: None, - esm_decryption_proofs: HashMap::new(), - expected_esm_decryption_count: 0, - esm_decryption_correlation_map: HashMap::new(), - no_proof_dishonest_parties: None, - expected_verification_parties: None, + pending_shares: Vec::new(), + pending_share_decryption_data: None, honest_parties: None, + early_decryption_key_shares: HashMap::new(), } } } @@ -407,7 +397,7 @@ impl ThresholdKeyshare { impl Actor for ThresholdKeyshare { type Context = actix::Context; fn started(&mut self, ctx: &mut Self::Context) { - ctx.set_mailbox_capacity(MAILBOX_LIMIT) + ctx.set_mailbox_capacity(MAILBOX_LIMIT); } } @@ -452,22 +442,23 @@ impl ThresholdKeyshare { Ok(addr.clone()) } + /// Create or return the DecryptionKeySharedCollector. + /// Uses honest_parties from the struct. pub fn ensure_decryption_key_shared_collector( &mut self, self_addr: Addr, ) -> Result> { - let Some(state) = self.state.get() else { - bail!("State not found on threshold keyshare."); - }; + let state = self.state.try_get()?; + let my_party_id = state.party_id; let honest = self .honest_parties .as_ref() - .ok_or_else(|| anyhow!("Honest parties not yet defined"))?; + .ok_or_else(|| anyhow!("honest_parties not set when creating collector"))?; let expected: HashSet = honest .iter() - .filter(|&&pid| pid != state.party_id) + .filter(|&&pid| pid != my_party_id) .copied() .collect(); @@ -598,7 +589,11 @@ impl ThresholdKeyshare { Ok(()) } - pub fn handle_compute_response(&mut self, msg: TypedEvent) -> Result<()> { + pub fn handle_compute_response( + &mut self, + msg: TypedEvent, + self_addr: Addr, + ) -> Result<()> { match &msg.response { ComputeResponseKind::TrBFV(trbfv) => match trbfv { TrBFVResponse::GenEsiSss(_) => self.handle_gen_esi_sss_response(msg), @@ -606,23 +601,16 @@ impl ThresholdKeyshare { self.handle_gen_pk_share_and_sk_sss_response(msg) } TrBFVResponse::CalculateDecryptionKey(_) => { - self.handle_calculate_decryption_key_response(msg) + self.handle_calculate_decryption_key_response(msg, self_addr) } TrBFVResponse::CalculateDecryptionShare(_) => { self.handle_calculate_decryption_share_response(msg) } _ => Ok(()), }, - ComputeResponseKind::Zk(zk) => match zk { - ZkResponse::VerifyShareProofs(_) => self.handle_verify_share_proofs_response(msg), - ZkResponse::DkgShareDecryption(_) => { - self.handle_share_decryption_proof_response(msg) - } - ZkResponse::VerifyShareDecryptionProofs(_) => { - self.handle_verify_share_decryption_proofs_response(msg) - } - _ => Ok(()), - }, + // ZK responses (C4 proofs, share/decryption verification) are now + // handled by ProofRequestActor and ShareVerificationActor respectively. + _ => Ok(()), } } @@ -1175,8 +1163,8 @@ impl ThresholdKeyshare { }); } - // Store shares for use after verification completes - self.pending_verification_shares = Some(msg.shares); + // Store shares on the actor for use after verification completes (keep Arc to avoid deep clone) + self.pending_shares = msg.shares.iter().cloned().collect(); // Backward compat: only when ALL non-self parties have zero proofs // AND none have incomplete proofs (incomplete proofs are always dishonest) @@ -1195,165 +1183,187 @@ impl ThresholdKeyshare { return self.proceed_with_decryption_key_calculation(None, ec); } - // Merge no-proof and incomplete-proof parties — both are dishonest - let mut unverified_dishonest: HashSet = incomplete_proof_parties; - unverified_dishonest.extend(no_proof_parties); - if !unverified_dishonest.is_empty() { + // Merge no-proof and incomplete-proof parties — both are pre-dishonest + let mut pre_dishonest: BTreeSet = BTreeSet::new(); + pre_dishonest.extend(incomplete_proof_parties); + pre_dishonest.extend(no_proof_parties); + if !pre_dishonest.is_empty() { warn!( - "{} parties have missing/incomplete C2/C3 proofs for E3 {} — marking as dishonest: {:?}", - unverified_dishonest.len(), + "{} parties have missing/incomplete C2/C3 proofs for E3 {} — marking as pre-dishonest: {:?}", + pre_dishonest.len(), e3_id, - unverified_dishonest + pre_dishonest ); } if party_proofs_to_verify.is_empty() { // All non-self parties are dishonest (missing or incomplete proofs), none to verify - return self.proceed_with_decryption_key_calculation(Some(unverified_dishonest), ec); - } + let threshold = state.threshold_m; + let total = state.threshold_n; + let dishonest_count = (pre_dishonest.len() as u64).min(total); + let honest_count = total - dishonest_count; - // Store dishonest parties so we can merge them with verification failures later - self.no_proof_dishonest_parties = Some(unverified_dishonest); + if honest_count < threshold { + warn!( + "Too few honest parties for E3 {} ({} honest < {} threshold) after C2/C3 pre-dishonest filtering — cannot proceed", + e3_id, honest_count, threshold + ); + self.pending_shares.clear(); + self.bus.publish( + E3Failed { + e3_id: e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }, + ec, + )?; + return Ok(()); + } - // Track which party IDs we're sending for verification so we can detect missing results - self.expected_verification_parties = Some( - party_proofs_to_verify - .iter() - .map(|p| p.sender_party_id) - .collect(), - ); + let dishonest_set: HashSet = pre_dishonest.into_iter().collect(); + return self.proceed_with_decryption_key_calculation(Some(dishonest_set), ec); + } info!( - "Dispatching C2/C3 proof verification for E3 {} ({} parties)", + "Dispatching C2/C3 share verification for E3 {} ({} parties, {} pre-dishonest)", e3_id, - party_proofs_to_verify.len() + party_proofs_to_verify.len(), + pre_dishonest.len() ); - let event = ComputeRequest::zk( - ZkRequest::VerifyShareProofs(VerifyShareProofsRequest { - party_proofs: party_proofs_to_verify, - }), - CorrelationId::new(), - e3_id.clone(), - ); - self.bus.publish(event, ec)?; + self.bus.publish( + ShareVerificationDispatched { + e3_id: e3_id.clone(), + kind: VerificationKind::ShareProofs, + share_proofs: party_proofs_to_verify, + decryption_proofs: Vec::new(), + pre_dishonest, + }, + ec, + )?; Ok(()) } - /// Handle C2/C3 proof verification results — define honest set H and proceed. - pub fn handle_verify_share_proofs_response( + /// Handle ShareVerificationComplete from ShareVerificationActor. + /// Dispatched for both C2/C3 and C4 verification. + pub fn handle_share_verification_complete( &mut self, - msg: TypedEvent, + msg: TypedEvent, ) -> Result<()> { let (msg, ec) = msg.into_components(); - let resp: VerifyShareProofsResponse = match msg.response { - ComputeResponseKind::Zk(ZkResponse::VerifyShareProofs(r)) => r, - _ => bail!("Expected VerifyShareProofs response"), - }; - let state = self.state.try_get()?; let e3_id = state.get_e3_id(); - // Partition into honest and dishonest based on proof verification results - let mut dishonest_parties: HashSet = HashSet::new(); - - // Merge in parties that provided no proofs (already identified as dishonest) - if let Some(no_proof) = self.no_proof_dishonest_parties.take() { - dishonest_parties.extend(no_proof); - } - - let expected_parties = self.expected_verification_parties.take(); - let mut seen_parties: HashSet = HashSet::new(); - - for result in &resp.party_results { - seen_parties.insert(result.sender_party_id); - if result.all_verified { - info!( - "Party {} passed C2/C3 verification for E3 {}", - result.sender_party_id, e3_id - ); - } else { - warn!( - "Party {} FAILED C2/C3 verification for E3 {} (proof type: {:?})", - result.sender_party_id, e3_id, result.failed_proof_type - ); - dishonest_parties.insert(result.sender_party_id); - } - } + match msg.kind { + VerificationKind::ShareProofs => { + // C2/C3 verification complete + if msg.dishonest_parties.is_empty() { + info!( + "All parties passed C2/C3 verification for E3 {} — proceeding", + e3_id + ); + self.proceed_with_decryption_key_calculation(None, ec) + } else { + let threshold = state.threshold_m; + let total = state.threshold_n; + let dishonest_count = (msg.dishonest_parties.len() as u64).min(total); + let honest_count = total - dishonest_count; + + if honest_count < threshold { + warn!( + "Too few honest parties for E3 {} ({} honest < {} threshold) — cannot proceed", + e3_id, honest_count, threshold + ); + // Clear pending shares + self.pending_shares.clear(); + self.bus.publish( + E3Failed { + e3_id: e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }, + ec, + )?; + return Ok(()); + } - // Any party we sent for verification but got no result back is treated as dishonest - if let Some(expected) = expected_parties { - for party_id in &expected { - if !seen_parties.contains(party_id) { - warn!( - "Party {} missing from C2/C3 verification results for E3 {} — treating as dishonest", - party_id, e3_id + let dishonest_set: HashSet = msg.dishonest_parties.into_iter().collect(); + info!( + "Proceeding with {} honest parties for E3 {} ({} dishonest excluded)", + honest_count, + e3_id, + dishonest_set.len() ); - dishonest_parties.insert(*party_id); + self.proceed_with_decryption_key_calculation(Some(dishonest_set), ec) } } - } + VerificationKind::DecryptionProofs => { + // C4 verification complete — update honest set and publish KeyshareCreated + if !msg.dishonest_parties.is_empty() { + if let Some(ref mut honest) = self.honest_parties { + honest.retain(|pid| !msg.dishonest_parties.contains(pid)); + } - if dishonest_parties.is_empty() { - info!( - "All parties passed C2/C3 verification for E3 {} — proceeding", - e3_id - ); - self.proceed_with_decryption_key_calculation(None, ec) - } else { - let threshold = state.threshold_m; - let total = state.threshold_n; - let honest_count = total - dishonest_parties.len() as u64; + let threshold = state.threshold_m; + let honest_count = self + .honest_parties + .as_ref() + .map(|h| h.len() as u64) + .unwrap_or(0); + + if honest_count < threshold { + warn!( + "Too few honest parties after C4 for E3 {} ({} honest < {} threshold)", + e3_id, honest_count, threshold + ); + self.bus.publish( + E3Failed { + e3_id: e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }, + ec, + )?; + return Ok(()); + } - if honest_count < threshold { - warn!( - "Too few honest parties for E3 {} ({} honest < {} threshold) — cannot proceed", - e3_id, honest_count, threshold - ); - if let Err(err) = self.bus.publish( - E3Failed { - e3_id: msg.e3_id, - failed_at_stage: E3Stage::CommitteeFinalized, - reason: FailureReason::InsufficientCommitteeMembers, - }, - ec, - ) { - error!("Failed to publish E3Failed: {err}"); + info!( + "Updated honest set after C4 for E3 {}: {} honest ({} removed)", + e3_id, + honest_count, + msg.dishonest_parties.len() + ); + } else { + info!( + "All parties passed C4 verification for E3 {} — publishing KeyshareCreated", + e3_id + ); } - self.pending_verification_shares = None; - return Ok(()); - } - info!( - "Proceeding with {} honest parties for E3 {} ({} dishonest excluded)", - honest_count, - e3_id, - dishonest_parties.len() - ); - self.proceed_with_decryption_key_calculation(Some(dishonest_parties), ec) + self.publish_keyshare_created(ec) + } } } - /// After verification, decrypt shares from honest parties, compute decryption key, - /// and dispatch C4 proof generation. + /// After verification, decrypt shares from honest parties and compute decryption key. + /// C4 proof generation is deferred to ProofRequestActor via DecryptionShareProofsPending. fn proceed_with_decryption_key_calculation( &mut self, dishonest_parties: Option>, ec: EventContext, ) -> Result<()> { - let shares = self - .pending_verification_shares - .take() - .ok_or_else(|| anyhow!("No pending verification shares"))?; - let cipher = self.cipher.clone(); let state = self.state.try_get()?; let e3_id = state.get_e3_id(); let party_id = state.party_id as usize; let trbfv_config = state.get_trbfv_config(); - // Get our BFV secret key from state + // Get our BFV secret key from state, pending shares from the actor let current: AggregatingDecryptionKey = state.clone().try_into()?; + let shares = std::mem::take(&mut self.pending_shares); + if shares.is_empty() { + bail!("No pending verification shares"); + } let sk_bytes = current.sk_bfv.access(&cipher)?; let params = BfvParamSet::from(self.share_enc_preset.clone()).build_arc(); let sk_bfv = deserialize_secret_key(&sk_bytes, ¶ms)?; @@ -1369,9 +1379,128 @@ impl ThresholdKeyshare { }) .collect(); - // Store honest party IDs for later use by DecryptionKeySharedCollector + // Derive expected dimensions from our own share (trusted source). + // All parties use the same on-chain BFV params, so dimensions must be identical. + let own_share = honest_shares + .iter() + .find(|ts| ts.party_id == state.party_id as u64) + .ok_or_else(|| anyhow!("Own share not found in honest shares"))?; + + let expected_num_esi = own_share.esi_sss.len(); + let own_sk_share = own_share + .sk_sss + .clone_share(if own_share.sk_sss.len() == 1 { + 0 + } else { + party_id + }) + .ok_or(anyhow!("No own sk_sss share"))?; + let expected_num_moduli_sk = own_sk_share.num_moduli(); + let expected_num_moduli_esi = if expected_num_esi > 0 { + own_share.esi_sss[0] + .clone_share(if own_share.esi_sss[0].len() == 1 { + 0 + } else { + party_id + }) + .map(|s| s.num_moduli()) + .unwrap_or(0) + } else { + 0 + }; + + // Validate per-party dimensions and exclude mismatched parties. + // This prevents a malicious party with wrong-sized shares from + // causing a panic or opaque error in downstream matrix building. + let mut dimension_excluded: Vec = Vec::new(); + let honest_shares: Vec<_> = honest_shares + .into_iter() + .filter(|ts| { + // Own share is always valid + if ts.party_id == state.party_id as u64 { + return true; + } + // Check esi count + if ts.esi_sss.len() != expected_num_esi { + warn!( + "Party {} has wrong esi_sss count ({} vs expected {}) — excluding from honest set", + ts.party_id, ts.esi_sss.len(), expected_num_esi + ); + dimension_excluded.push(ts.party_id); + return false; + } + // Check sk share exists and moduli count + let idx = if ts.sk_sss.len() == 1 { 0 } else { party_id }; + match ts.sk_sss.clone_share(idx) { + Some(share) if share.num_moduli() != expected_num_moduli_sk => { + warn!( + "Party {} has wrong sk num_moduli ({} vs expected {}) — excluding from honest set", + ts.party_id, share.num_moduli(), expected_num_moduli_sk + ); + dimension_excluded.push(ts.party_id); + return false; + } + None => { + warn!( + "Party {} has no sk_sss share at index {} — excluding from honest set", + ts.party_id, idx + ); + dimension_excluded.push(ts.party_id); + return false; + } + _ => {} + } + // Check esi shares exist and moduli counts + for (esi_idx, esi_shares) in ts.esi_sss.iter().enumerate() { + let idx = if esi_shares.len() == 1 { 0 } else { party_id }; + match esi_shares.clone_share(idx) { + Some(share) if share.num_moduli() != expected_num_moduli_esi => { + warn!( + "Party {} has wrong esi num_moduli at index {} ({} vs expected {}) — excluding from honest set", + ts.party_id, esi_idx, share.num_moduli(), expected_num_moduli_esi + ); + dimension_excluded.push(ts.party_id); + return false; + } + None => { + warn!( + "Party {} has no esi_sss share at index {} (esi {}) — excluding from honest set", + ts.party_id, idx, esi_idx + ); + dimension_excluded.push(ts.party_id); + return false; + } + _ => {} + } + } + true + }) + .collect(); + + if !dimension_excluded.is_empty() { + warn!( + "Excluded {} parties with dimension mismatches: {:?}", + dimension_excluded.len(), + dimension_excluded + ); + // Re-check threshold after exclusion + let threshold = state.threshold_m; + if (honest_shares.len() as u64) < threshold { + self.pending_shares.clear(); + self.bus.publish( + E3Failed { + e3_id: e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }, + ec, + )?; + return Ok(()); + } + } + + // Store honest party IDs in state (after dimension exclusion) let honest_party_ids: HashSet = honest_shares.iter().map(|s| s.party_id).collect(); - self.honest_parties = Some(honest_party_ids); let num_honest = honest_shares.len(); info!( @@ -1379,37 +1508,32 @@ impl ThresholdKeyshare { num_honest, e3_id ); - // Collect ciphertext bytes for C4 proof generation BEFORE decrypting + // Collect ciphertext bytes for C4 proof requests (built here, sent after CalculateDecryptionKey) + // Dimensions are validated per-party above, so all shares are consistent. // C4a: sk_sss ciphertexts from honest parties [H * L] + let num_moduli_sk = expected_num_moduli_sk; let mut sk_ciphertexts_raw = Vec::new(); - let mut num_moduli_sk = 0; for ts in &honest_shares { let idx = if ts.sk_sss.len() == 1 { 0 } else { party_id }; let share = ts .sk_sss .clone_share(idx) .ok_or(anyhow!("No sk_sss share at index {}", idx))?; - num_moduli_sk = share.num_moduli(); for ct_bytes in share.ciphertext_bytes() { sk_ciphertexts_raw.push(ct_bytes.clone()); } } // C4b: esi_sss ciphertexts from honest parties — one set per smudging noise - // Layout per esi index: [H * L] ciphertexts - let num_esi = honest_shares - .first() - .map(|ts| ts.esi_sss.len()) - .unwrap_or(0); + let num_esi = expected_num_esi; + let num_moduli_esi = expected_num_moduli_esi; let mut esi_ciphertexts_raw: Vec> = vec![Vec::new(); num_esi]; - let mut num_moduli_esi = 0; for ts in &honest_shares { for (esi_idx, esi_shares) in ts.esi_sss.iter().enumerate() { let idx = if esi_shares.len() == 1 { 0 } else { party_id }; let share = esi_shares .clone_share(idx) .ok_or(anyhow!("No esi_sss share at index {}", idx))?; - num_moduli_esi = share.num_moduli(); for ct_bytes in share.ciphertext_bytes() { esi_ciphertexts_raw[esi_idx].push(ct_bytes.clone()); } @@ -1429,8 +1553,8 @@ impl ThresholdKeyshare { }) .collect::>()?; - // Similarly decrypt esi_sss for each ciphertext - let esi_sss_collected: Vec> = honest_shares + // Decrypt per-party ESI shares: shape [party][esm_idx] + let per_party_esi: Vec> = honest_shares .iter() .map(|ts| { ts.esi_sss @@ -1446,6 +1570,16 @@ impl ThresholdKeyshare { }) .collect::>()?; + // Transpose to [esm_idx][party] — CalculateDecryptionKey aggregates per smudging noise + let esi_sss_collected: Vec> = (0..num_esi) + .map(|esm_idx| { + per_party_esi + .iter() + .map(|party_esi| party_esi[esm_idx].clone()) + .collect() + }) + .collect(); + // Publish CalculateDecryptionKey request let request = CalculateDecryptionKeyRequest { trbfv_config, @@ -1463,151 +1597,92 @@ impl ThresholdKeyshare { ); self.bus.publish(event, ec.clone())?; - // Reset share decryption proof storage and set expected count - self.sk_decryption_proof = None; - self.esm_decryption_proofs.clear(); - self.esm_decryption_correlation_map.clear(); - self.expected_esm_decryption_count = num_esi; - - // Resolve threshold preset for C4 proof requests + // Build C4 proof requests — stored for sending after CalculateDecryptionKey completes let threshold_preset = self .share_enc_preset .threshold_counterpart() .ok_or_else(|| anyhow!("No threshold counterpart for {:?}", self.share_enc_preset))?; - // Dispatch C4a proof generation (SecretKey decryption) - info!( - "Dispatching C4a DkgShareDecryption proof (SecretKey) for E3 {} ({} honest, {} moduli)", - e3_id, num_honest, num_moduli_sk - ); - let sk_decryption_request = ComputeRequest::zk( - ZkRequest::DkgShareDecryption(DkgShareDecryptionProofRequest { + let sk_request = DkgShareDecryptionProofRequest { + sk_bfv: current.sk_bfv.clone(), + honest_ciphertexts_raw: sk_ciphertexts_raw, + num_honest_parties: num_honest, + num_moduli: num_moduli_sk, + dkg_input_type: DkgInputType::SecretKey, + params_preset: threshold_preset, + }; + + let esm_requests: Vec = esi_ciphertexts_raw + .into_iter() + .map(|esi_cts| DkgShareDecryptionProofRequest { sk_bfv: current.sk_bfv.clone(), - honest_ciphertexts_raw: sk_ciphertexts_raw, + honest_ciphertexts_raw: esi_cts, num_honest_parties: num_honest, - num_moduli: num_moduli_sk, - dkg_input_type: DkgInputType::SecretKey, + num_moduli: num_moduli_esi, + dkg_input_type: DkgInputType::SmudgingNoise, params_preset: threshold_preset, - }), - CorrelationId::new(), - e3_id.clone(), - ); - self.bus.publish(sk_decryption_request, ec.clone())?; + }) + .collect(); - // Dispatch C4b proof generation for each smudging noise index - for (esi_idx, esi_cts) in esi_ciphertexts_raw.into_iter().enumerate() { - info!( - "Dispatching C4b DkgShareDecryption proof (SmudgingNoise[{}]) for E3 {} ({} honest, {} moduli)", - esi_idx, e3_id, num_honest, num_moduli_esi - ); - let correlation_id = CorrelationId::new(); - self.esm_decryption_correlation_map - .insert(correlation_id, esi_idx); - let esm_decryption_request = ComputeRequest::zk( - ZkRequest::DkgShareDecryption(DkgShareDecryptionProofRequest { - sk_bfv: current.sk_bfv.clone(), - honest_ciphertexts_raw: esi_cts, - num_honest_parties: num_honest, - num_moduli: num_moduli_esi, - dkg_input_type: DkgInputType::SmudgingNoise, - params_preset: threshold_preset, - }), - correlation_id, - e3_id.clone(), - ); - self.bus.publish(esm_decryption_request, ec.clone())?; - } + // Store honest parties and C4 data on the actor (transient coordination) + self.honest_parties = Some(honest_party_ids); + self.pending_share_decryption_data = Some((sk_request, esm_requests)); Ok(()) } - /// Handle C4 (DkgShareDecryption) proof responses — store for Exchange #3. - fn handle_share_decryption_proof_response( + /// 5a. CalculateDecryptionKeyResponse — transition to ReadyForDecryption, + /// then publish DecryptionShareProofsPending so ProofRequestActor can + /// generate C4 proofs, sign them, and publish DecryptionKeyShared. + pub fn handle_calculate_decryption_key_response( &mut self, - msg: TypedEvent, + res: TypedEvent, + self_addr: Addr, ) -> Result<()> { - let (msg, _ec) = msg.into_components(); - let correlation_id = msg.correlation_id; - let resp: DkgShareDecryptionProofResponse = match msg.response { - ComputeResponseKind::Zk(ZkResponse::DkgShareDecryption(r)) => r, - _ => bail!("Expected DkgShareDecryption response"), - }; + let (res, ec) = res.into_components(); + let output: CalculateDecryptionKeyResponse = res + .try_into() + .context("Error extracting data from compute process")?; - let state = self.state.try_get()?; - let e3_id = state.get_e3_id(); + let (sk_poly_sum, es_poly_sum) = (output.sk_poly_sum, output.es_poly_sum); - match resp.dkg_input_type { - DkgInputType::SecretKey => { - info!("Received SK share decryption proof for E3 {}", e3_id); - self.sk_decryption_proof = Some(resp.proof); - } - DkgInputType::SmudgingNoise => { - let esi_idx = self - .esm_decryption_correlation_map - .remove(&correlation_id) - .ok_or_else(|| { - anyhow!( - "Unknown correlation ID {} for ESM share decryption proof in E3 {}", - correlation_id, - e3_id - ) - })?; - info!( - "Received ESM share decryption proof (SmudgingNoise[{}]) for E3 {} ({}/{})", - esi_idx, - e3_id, - self.esm_decryption_proofs.len() + 1, - self.expected_esm_decryption_count - ); - self.esm_decryption_proofs.insert(esi_idx, resp.proof); - } - } + // Extract C4 data from the actor (stored by proceed_with_decryption_key_calculation) + let (sk_request, esm_requests) = self + .pending_share_decryption_data + .take() + .ok_or_else(|| anyhow!("No pending share decryption data — CalculateDecryptionKey responded before proof requests were built"))?; - if self.sk_decryption_proof.is_some() - && self.esm_decryption_proofs.len() == self.expected_esm_decryption_count - { - info!( - "All share decryption proofs received for E3 {} (1 SK + {} ESM)", - e3_id, self.expected_esm_decryption_count - ); - self.try_publish_decryption_key_shared(_ec)?; - } + // Take early shares from the actor before transitioning + let early_shares = std::mem::take(&mut self.early_decryption_key_shares); - Ok(()) - } + // Transition to ReadyForDecryption + self.state.try_mutate(&ec, |s| { + use KeyshareState as K; + info!("Try store decryption key"); - /// Publish Exchange #3 (DecryptionKeyShared) when both the decryption key - /// and all C4 proofs are ready. - fn try_publish_decryption_key_shared(&mut self, ec: EventContext) -> Result<()> { - let state = self.state.try_get()?; - let e3_id = state.get_e3_id(); + let current: AggregatingDecryptionKey = s.clone().try_into()?; - // Need to be in ReadyForDecryption state (decryption key computed) - let ready: ReadyForDecryption = match state.clone().try_into() { - Ok(r) => r, - Err(_) => { - trace!("Not yet in ReadyForDecryption state — deferring Exchange #3"); - return Ok(()); - } - }; + let next = K::ReadyForDecryption(ReadyForDecryption { + pk_share: current.pk_share, + sk_poly_sum, + es_poly_sum, + signed_pk_generation_proof: current.signed_pk_generation_proof, + signed_sk_share_computation_proof: current.signed_sk_share_computation_proof, + signed_e_sm_share_computation_proof: current.signed_e_sm_share_computation_proof, + signed_sk_share_encryption_proofs: current.signed_sk_share_encryption_proofs, + signed_e_sm_share_encryption_proofs: current.signed_e_sm_share_encryption_proofs, + }); - // Need all share decryption proofs - let sk_proof = match &self.sk_decryption_proof { - Some(p) => p.clone(), - None => { - trace!("SK share decryption proof not yet received — deferring Exchange #3"); - return Ok(()); - } - }; - if self.esm_decryption_proofs.len() != self.expected_esm_decryption_count { - trace!("Not all ESM share decryption proofs received — deferring Exchange #3"); - return Ok(()); - } + s.new_state(next) + })?; + // Publish DecryptionShareProofsPending to ProofRequestActor + let state = self.state.try_get()?; + let e3_id = state.get_e3_id(); let party_id = state.party_id; let node = state.address.clone(); - // Decrypt sk_poly_sum and es_poly_sum from SensitiveBytes → ArcBytes for network transmission + let ready: ReadyForDecryption = state.clone().try_into()?; let sk_poly_sum_bytes = ready.sk_poly_sum.access(&self.cipher)?; let es_poly_sum_bytes: Vec = ready .es_poly_sum @@ -1619,201 +1694,161 @@ impl ThresholdKeyshare { .collect::>()?; info!( - "Publishing Exchange #3 (DecryptionKeyShared) for E3 {} party {}", - e3_id, party_id + "Publishing DecryptionShareProofsPending for E3 {} party {} (1 SK + {} ESM requests)", + e3_id, + party_id, + esm_requests.len() ); - // Assemble ESM decryption proofs in esi_idx order to align with es_poly_sum - let mut esm_proofs_ordered: Vec = - Vec::with_capacity(self.expected_esm_decryption_count); - for idx in 0..self.expected_esm_decryption_count { - let proof = self - .esm_decryption_proofs - .get(&idx) - .ok_or_else(|| anyhow!("Missing ESM share decryption proof for esi_idx {}", idx))? - .clone(); - esm_proofs_ordered.push(proof); - } - self.bus.publish( - DecryptionKeyShared { + DecryptionShareProofsPending { e3_id: e3_id.clone(), party_id, node, sk_poly_sum: ArcBytes::from_bytes(&sk_poly_sum_bytes), es_poly_sum: es_poly_sum_bytes, - sk_decryption_proof: sk_proof, - esm_decryption_proofs: esm_proofs_ordered, - external: false, + sk_request, + esm_requests, }, - ec, + ec.clone(), )?; + // Create collector and replay any early-arriving DecryptionKeyShared events + let state = self.state.try_get()?; + let my_party_id = state.party_id; + let honest = self.honest_parties.as_ref().cloned().unwrap_or_default(); + let expected: HashSet = honest + .iter() + .filter(|&&pid| pid != my_party_id) + .copied() + .collect(); + + if !expected.is_empty() { + let collector = self.ensure_decryption_key_shared_collector(self_addr)?; + for (_pid, share) in early_shares { + collector.do_send(TypedEvent::new(share, ec.clone())); + } + } + Ok(()) } - /// 5a. CalculateDecryptionKeyResponse -> KeyshareCreated - pub fn handle_calculate_decryption_key_response( + /// Handle an external DecryptionKeyShared event while in AggregatingDecryptionKey state. + /// Store it for later processing when we transition to ReadyForDecryption. + fn handle_early_decryption_key_share( &mut self, - res: TypedEvent, + data: DecryptionKeyShared, + _ec: EventContext, ) -> Result<()> { - let (res, ec) = res.into_components(); - let output: CalculateDecryptionKeyResponse = res - .try_into() - .context("Error extracting data from compute process")?; - - let (sk_poly_sum, es_poly_sum) = (output.sk_poly_sum, output.es_poly_sum); - - self.state.try_mutate(&ec, |s| { - use KeyshareState as K; - info!("Try store decryption key"); - - // Get pk_share and signed proof from current state - let current: AggregatingDecryptionKey = s.clone().try_into()?; - - // Transition to ReadyForDecryption, carrying the signed proof - let next = K::ReadyForDecryption(ReadyForDecryption { - pk_share: current.pk_share, - sk_poly_sum, - es_poly_sum, - signed_pk_generation_proof: current.signed_pk_generation_proof, - signed_sk_share_computation_proof: current.signed_sk_share_computation_proof, - signed_e_sm_share_computation_proof: current.signed_e_sm_share_computation_proof, - signed_sk_share_encryption_proofs: current.signed_sk_share_encryption_proofs, - signed_e_sm_share_encryption_proofs: current.signed_e_sm_share_encryption_proofs, - }); - - s.new_state(next) - })?; - - // KeyshareCreated (Exchange #4) is deferred until after C4 proof verification - // from all honest parties. Check if C4 proofs are already ready for Exchange #3. - self.try_publish_decryption_key_shared(ec)?; - + let party_id = data.party_id; + info!( + "Storing early DecryptionKeyShared from party {} (state: AggregatingDecryptionKey)", + party_id + ); + self.early_decryption_key_shares.insert(party_id, data); Ok(()) } - /// Handle all DecryptionKeyShared events collected from honest parties. - /// Build VerifyC4ProofsRequest and dispatch for verification. - pub fn handle_all_decryption_key_shares_collected( + /// Dispatch C4 verification for all collected DecryptionKeyShared events. + /// Shares are provided by the DecryptionKeySharedCollector. + fn dispatch_c4_verification( &mut self, - msg: TypedEvent, + collected_shares: HashMap, + ec: EventContext, ) -> Result<()> { - let (msg, ec) = msg.into_components(); let state = self.state.try_get()?; let e3_id = state.get_e3_id(); + let ready: ReadyForDecryption = state.clone().try_into()?; info!( - "AllDecryptionKeySharesCollected for E3 {} ({} shares)", + "All DecryptionKeyShared collected for E3 {} ({} shares)", e3_id, - msg.shares.len() + collected_shares.len() ); - // Build share decryption proof verification requests from collected shares - let party_proofs: Vec = msg - .shares + // Validate ESM proof count — each party must provide exactly + // one C4b proof per smudging noise index. + let expected_esm = ready.es_poly_sum.len(); + let mut c4_count_dishonest: HashSet = HashSet::new(); + let party_proofs: Vec = collected_shares .iter() - .map(|(&party_id, share)| PartyShareDecryptionProofsToVerify { - sender_party_id: party_id, - sk_decryption_proof: share.sk_decryption_proof.clone(), - esm_decryption_proofs: share.esm_decryption_proofs.clone(), + .filter_map(|(&party_id, share)| { + if share.signed_esm_decryption_proofs.len() != expected_esm { + warn!( + "Party {} has wrong ESM proof count ({} vs expected {}) for E3 {} — treating as dishonest", + party_id, + share.signed_esm_decryption_proofs.len(), + expected_esm, + e3_id + ); + c4_count_dishonest.insert(party_id); + None + } else { + Some(PartyShareDecryptionProofsToVerify { + sender_party_id: party_id, + signed_sk_decryption_proof: share.signed_sk_decryption_proof.clone(), + signed_esm_decryption_proofs: share.signed_esm_decryption_proofs.clone(), + }) + } }) .collect(); - if party_proofs.is_empty() { - info!("No share decryption proofs to verify — publishing KeyshareCreated directly"); - return self.publish_keyshare_created(ec); + // Evict pre-dishonest parties (wrong ESM count) from honest set + if !c4_count_dishonest.is_empty() { + if let Some(ref mut honest) = self.honest_parties { + honest.retain(|pid| !c4_count_dishonest.contains(pid)); + } } - info!( - "Dispatching share decryption proof verification for E3 {} ({} parties)", - e3_id, - party_proofs.len() - ); - - let event = ComputeRequest::zk( - ZkRequest::VerifyShareDecryptionProofs(VerifyShareDecryptionProofsRequest { - party_proofs, - }), - CorrelationId::new(), - e3_id.clone(), - ); - self.bus.publish(event, ec)?; - Ok(()) - } - - /// Handle share decryption proof verification results — update honest set H and publish KeyshareCreated. - fn handle_verify_share_decryption_proofs_response( - &mut self, - msg: TypedEvent, - ) -> Result<()> { - let (msg, ec) = msg.into_components(); - let resp: VerifyShareDecryptionProofsResponse = match msg.response { - ComputeResponseKind::Zk(ZkResponse::VerifyShareDecryptionProofs(r)) => r, - _ => bail!("Expected VerifyShareDecryptionProofs response"), - }; - - let state = self.state.try_get()?; - let e3_id = state.get_e3_id(); + if party_proofs.is_empty() { + // Check threshold viability after removing pre-dishonest parties + let threshold = state.threshold_m; + let honest_count = self + .honest_parties + .as_ref() + .map(|h| h.len() as u64) + .unwrap_or(0); - // Partition into honest and dishonest - let mut share_decryption_dishonest: HashSet = HashSet::new(); - for result in &resp.party_results { - if result.all_verified { - info!( - "Party {} passed C4 verification for E3 {}", - result.sender_party_id, e3_id - ); - } else { + if honest_count < threshold { warn!( - "Party {} FAILED C4 verification for E3 {}", - result.sender_party_id, e3_id + "Too few honest parties after C4 pre-filtering for E3 {} ({} honest < {} threshold)", + e3_id, honest_count, threshold ); - share_decryption_dishonest.insert(result.sender_party_id); + self.bus.publish( + E3Failed { + e3_id: e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + }, + ec, + )?; + return Ok(()); } + + info!("No C4 proofs to verify — publishing KeyshareCreated directly"); + return self.publish_keyshare_created(ec); } - // Update honest parties set - if !share_decryption_dishonest.is_empty() { - if let Some(ref mut honest) = self.honest_parties { - honest.retain(|pid| !share_decryption_dishonest.contains(pid)); + let pre_dishonest: BTreeSet = c4_count_dishonest.into_iter().collect(); - let threshold = state.threshold_m; - let honest_count = honest.len() as u64; - if honest_count < threshold { - warn!( - "Too few honest parties after C4 verification for E3 {} ({} honest < {} threshold)", - e3_id, honest_count, threshold - ); - if let Err(err) = self.bus.publish( - E3Failed { - e3_id: e3_id.clone(), - failed_at_stage: E3Stage::CommitteeFinalized, - reason: FailureReason::InsufficientCommitteeMembers, - }, - ec, - ) { - error!("Failed to publish E3Failed: {err}"); - } - return Ok(()); - } - - info!( - "Updated honest set after C4 verification for E3 {}: {} honest ({} removed)", - e3_id, - honest.len(), - share_decryption_dishonest.len() - ); - } - } else { - info!( - "All parties passed C4 verification for E3 {} — publishing KeyshareCreated", - e3_id - ); - } + info!( + "Dispatching C4 share verification for E3 {} ({} parties, {} pre-dishonest)", + e3_id, + party_proofs.len(), + pre_dishonest.len() + ); - // Exchange #4: Publish KeyshareCreated - self.publish_keyshare_created(ec) + self.bus.publish( + ShareVerificationDispatched { + e3_id: e3_id.clone(), + kind: VerificationKind::DecryptionProofs, + share_proofs: Vec::new(), + decryption_proofs: party_proofs, + pre_dishonest, + }, + ec, + )?; + Ok(()) } /// Publish KeyshareCreated (Exchange #4) with pk_share and signed C1 proof. @@ -1974,16 +2009,66 @@ impl Handler for ThresholdKeyshare { } EnclaveEventData::DecryptionKeyShared(data) => { if data.external { - info!( - "Received DecryptionKeyShared from party {} for E3 {}", - data.party_id, data.e3_id - ); - match self.ensure_decryption_key_shared_collector(ctx.address()) { - Ok(collector) => collector.do_send(TypedEvent::new(data, ec)), - Err(e) => warn!("Cannot forward DecryptionKeyShared: {}", e), + // Route based on current state + if let Some(state) = self.state.get() { + let result = match &state.state { + KeyshareState::AggregatingDecryptionKey(_) => { + self.handle_early_decryption_key_share(data, ec) + } + KeyshareState::ReadyForDecryption(_) => { + // Delegate to the collector actor + if let Some(ref collector) = self.decryption_key_shared_collector { + collector.do_send(TypedEvent::new(data, ec)); + Ok(()) + } else { + warn!( + "DecryptionKeyShared from party {} dropped — no collector (sole honest party)", + data.party_id + ); + Ok(()) + } + } + other => { + trace!( + "DecryptionKeyShared from party {} in unexpected state {:?}, ignoring", + data.party_id, + other.variant_name() + ); + Ok(()) + } + }; + if let Err(err) = result { + error!("Failed to handle DecryptionKeyShared: {err}"); + } + } + } else { + // Own DecryptionKeyShared published by ProofRequestActor. + // A3 fast-path: if no other honest parties, publish KeyshareCreated directly. + if let Some(state) = self.state.get() { + if data.party_id == state.party_id { + if let KeyshareState::ReadyForDecryption(_) = state.state { + let others = self + .honest_parties + .as_ref() + .map(|h| h.iter().filter(|&&pid| pid != state.party_id).count()) + .unwrap_or(0); + if others == 0 { + info!( + "No other honest parties for E3 {} — publishing KeyshareCreated directly", + data.e3_id + ); + if let Err(err) = self.publish_keyshare_created(ec) { + error!("Failed to publish KeyshareCreated: {err}"); + } + } + } + } } } } + EnclaveEventData::ShareVerificationComplete(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } EnclaveEventData::ComputeResponse(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } @@ -1994,11 +2079,15 @@ impl Handler for ThresholdKeyshare { impl Handler> for ThresholdKeyshare { type Result = (); - fn handle(&mut self, msg: TypedEvent, _: &mut Self::Context) -> Self::Result { + fn handle( + &mut self, + msg: TypedEvent, + ctx: &mut Self::Context, + ) -> Self::Result { trap( EType::KeyGeneration, &self.bus.with_ec(msg.get_ctx()), - || self.handle_compute_response(msg), + || self.handle_compute_response(msg, ctx.address()), ) } } @@ -2033,66 +2122,36 @@ impl Handler> for ThresholdKeyshare { } } -impl Handler> for ThresholdKeyshare { +impl Handler> for ThresholdKeyshare { type Result = (); fn handle( &mut self, - msg: TypedEvent, + msg: TypedEvent, _: &mut Self::Context, ) -> Self::Result { trap( EType::KeyGeneration, &self.bus.with_ec(msg.get_ctx()), - || self.handle_all_threshold_shares_collected(msg), + || self.handle_share_verification_complete(msg), ) } } -impl Handler> for ThresholdKeyshare { +impl Handler> for ThresholdKeyshare { type Result = (); fn handle( &mut self, - msg: TypedEvent, + msg: TypedEvent, _: &mut Self::Context, ) -> Self::Result { trap( EType::KeyGeneration, &self.bus.with_ec(msg.get_ctx()), - || self.handle_all_decryption_key_shares_collected(msg), + || self.handle_all_threshold_shares_collected(msg), ) } } -impl Handler for ThresholdKeyshare { - type Result = (); - fn handle( - &mut self, - msg: DecryptionKeySharedCollectionFailed, - _: &mut Self::Context, - ) -> Self::Result { - trap(EType::KeyGeneration, &self.bus.clone(), || { - warn!( - e3_id = %msg.e3_id, - missing_parties = ?msg.missing_parties, - "DecryptionKeyShared collection failed: {}", - msg.reason - ); - - // Clear the collector reference since it's stopped - self.decryption_key_shared_collector = None; - - if let Err(err) = self.bus.publish_without_context(E3Failed { - e3_id: msg.e3_id.clone(), - failed_at_stage: E3Stage::CommitteeFinalized, - reason: FailureReason::InsufficientCommitteeMembers, - }) { - error!("Failed to publish E3Failed: {err}"); - } - Ok(()) - }) - } -} - impl Handler> for ThresholdKeyshare { type Result = (); fn handle( @@ -2163,12 +2222,64 @@ impl Handler for ThresholdKeyshare { } } +impl Handler> for ThresholdKeyshare { + type Result = (); + fn handle( + &mut self, + msg: TypedEvent, + _: &mut Self::Context, + ) -> Self::Result { + trap( + EType::KeyGeneration, + &self.bus.with_ec(msg.get_ctx()), + || { + let (msg, ec) = msg.into_components(); + self.decryption_key_shared_collector = None; + self.dispatch_c4_verification(msg.shares, ec) + }, + ) + } +} + +impl Handler for ThresholdKeyshare { + type Result = (); + fn handle( + &mut self, + msg: DecryptionKeySharedCollectionFailed, + ctx: &mut Self::Context, + ) -> Self::Result { + trap(EType::KeyGeneration, &self.bus.clone(), || { + warn!( + e3_id = %msg.e3_id, + missing_parties = ?msg.missing_parties, + "DecryptionKeyShared collection failed: {}", + msg.reason + ); + + self.decryption_key_shared_collector = None; + + self.bus.publish_without_context(E3Failed { + e3_id: msg.e3_id.clone(), + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::InsufficientCommitteeMembers, + })?; + + ctx.stop(); + Ok(()) + }) + } +} + impl Handler for ThresholdKeyshare { type Result = (); fn handle(&mut self, _: E3RequestComplete, ctx: &mut Self::Context) -> Self::Result { self.encryption_key_collector = None; self.decryption_key_collector = None; self.decryption_key_shared_collector = None; + self.pending_shares.clear(); + self.pending_share_decryption_data = None; + self.honest_parties = None; + self.early_decryption_key_shares.clear(); self.notify_sync(ctx, Die); } } diff --git a/crates/keyshare/src/threshold_share_collector.rs b/crates/keyshare/src/threshold_share_collector.rs index 31682fc21e..8d097d34c3 100644 --- a/crates/keyshare/src/threshold_share_collector.rs +++ b/crates/keyshare/src/threshold_share_collector.rs @@ -22,7 +22,7 @@ use tracing::{info, warn}; use crate::{AllThresholdSharesCollected, ThresholdKeyshare}; /// Proofs received alongside a threshold share from a sender. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct ReceivedShareProofs { /// Signed C2a proof (sk share computation) from the sender. pub signed_c2a_proof: Option, diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index 9aa284140b..0482b723b2 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -19,14 +19,14 @@ use anyhow::Result; use e3_crypto::Cipher; use e3_events::run_once; use e3_events::trap_fut; + use e3_events::EType; use e3_events::EffectsEnabled; use e3_events::{ BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeRequestKind, ComputeResponse, DkgShareDecryptionProofRequest, DkgShareDecryptionProofResponse, EnclaveEvent, - EnclaveEventData, EventPublisher, EventSubscriber, EventType, - PartyShareDecryptionVerificationResult, PartyVerificationResult, PkBfvProofRequest, - PkBfvProofResponse, PkGenerationProofRequest, PkGenerationProofResponse, + EnclaveEventData, EventPublisher, EventSubscriber, EventType, PartyVerificationResult, + PkBfvProofRequest, PkBfvProofResponse, PkGenerationProofRequest, PkGenerationProofResponse, ShareComputationProofRequest, ShareComputationProofResponse, ShareEncryptionProofRequest, ShareEncryptionProofResponse, TypedEvent, VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, VerifyShareProofsRequest, VerifyShareProofsResponse, @@ -815,38 +815,49 @@ fn handle_verify_share_proofs( ) -> Result { let e3_id_str = request.e3_id.to_string(); + // ECDSA validation (signature recovery, signer consistency, e3_id match) + // is handled by ShareVerificationActor before dispatching to multithread. + // This function performs ZK-only proof verification. let party_results: Vec = req .party_proofs .into_iter() .map(|party| { let sender = party.sender_party_id; + for signed_proof in &party.signed_proofs { + // 1. Validate CircuitName matches expected circuits for this ProofType + let expected_circuits = signed_proof.payload.proof_type.circuit_names(); + if !expected_circuits.contains(&signed_proof.payload.proof.circuit) { + info!( + "Circuit name mismatch for party {} ({:?}): expected {:?}, got {:?}", + sender, + signed_proof.payload.proof_type, + expected_circuits, + signed_proof.payload.proof.circuit + ); + return PartyVerificationResult { + sender_party_id: sender, + all_verified: false, + failed_signed_payload: Some(signed_proof.clone()), + recovered_address: None, + }; + } + + // 2. ZK proof verification let proof = &signed_proof.payload.proof; let result = prover.verify(proof, &e3_id_str, sender); match result { Ok(true) => continue, - Ok(false) => { + Ok(false) | Err(_) => { info!( - "Proof verification failed for party {} ({:?})", + "ZK proof verification failed for party {} ({:?})", sender, signed_proof.payload.proof_type ); return PartyVerificationResult { sender_party_id: sender, all_verified: false, - failed_proof_type: Some(signed_proof.payload.proof_type), - failed_signed_payload: Some(signed_proof.clone()), - }; - } - Err(e) => { - info!( - "Proof verification error for party {} ({:?}): {}", - sender, signed_proof.payload.proof_type, e - ); - return PartyVerificationResult { - sender_party_id: sender, - all_verified: false, - failed_proof_type: Some(signed_proof.payload.proof_type), failed_signed_payload: Some(signed_proof.clone()), + recovered_address: None, }; } } @@ -854,8 +865,8 @@ fn handle_verify_share_proofs( PartyVerificationResult { sender_party_id: sender, all_verified: true, - failed_proof_type: None, failed_signed_payload: None, + recovered_address: None, } }) .collect(); @@ -874,41 +885,73 @@ fn handle_verify_share_decryption_proofs( ) -> Result { let e3_id_str = request.e3_id.to_string(); - let party_results: Vec = req + // ECDSA validation (signature recovery, signer consistency, e3_id match) + // is handled by ShareVerificationActor before dispatching to multithread. + // This function performs ZK-only proof verification. + let party_results: Vec = req .party_proofs .into_iter() .map(|party| { let sender = party.sender_party_id; - // Verify SK decryption proof - let sk_result = prover.verify(&party.sk_decryption_proof, &e3_id_str, sender); - match sk_result { - Ok(true) => {} - Ok(false) | Err(_) => { - return PartyShareDecryptionVerificationResult { + // Guard: an empty esm_decryption_proofs vec would make this loop + // vacuously true. Defence-in-depth: reject any party with zero ESM proofs. + if party.signed_esm_decryption_proofs.is_empty() { + return PartyVerificationResult { + sender_party_id: sender, + all_verified: false, + failed_signed_payload: None, + recovered_address: None, + }; + } + + // Flatten all signed proofs (SK + ESMs) and verify uniformly. + let all_signed: Vec<&e3_events::SignedProofPayload> = + std::iter::once(&party.signed_sk_decryption_proof) + .chain(party.signed_esm_decryption_proofs.iter()) + .collect(); + + for signed_proof in &all_signed { + // 1. Validate CircuitName matches expected circuits for this ProofType + let expected_circuits = signed_proof.payload.proof_type.circuit_names(); + if !expected_circuits.contains(&signed_proof.payload.proof.circuit) { + info!( + "C4 circuit mismatch for party {}: expected {:?}, got {:?}", + sender, expected_circuits, signed_proof.payload.proof.circuit + ); + return PartyVerificationResult { sender_party_id: sender, all_verified: false, + failed_signed_payload: Some((*signed_proof).clone()), + recovered_address: None, }; } - } - // Verify all ESM decryption proofs - for esm_proof in &party.esm_decryption_proofs { - let result = prover.verify(esm_proof, &e3_id_str, sender); + // 2. ZK proof verification + let proof = &signed_proof.payload.proof; + let result = prover.verify(proof, &e3_id_str, sender); match result { Ok(true) => continue, Ok(false) | Err(_) => { - return PartyShareDecryptionVerificationResult { + info!( + "C4 ZK proof verification failed for party {} ({:?})", + sender, signed_proof.payload.proof_type + ); + return PartyVerificationResult { sender_party_id: sender, all_verified: false, + failed_signed_payload: Some((*signed_proof).clone()), + recovered_address: None, }; } } } - PartyShareDecryptionVerificationResult { + PartyVerificationResult { sender_party_id: sender, all_verified: true, + failed_signed_payload: None, + recovered_address: None, } }) .collect(); diff --git a/crates/sync/src/sync.rs b/crates/sync/src/sync.rs index 19d97b3dfc..adadeda54c 100644 --- a/crates/sync/src/sync.rs +++ b/crates/sync/src/sync.rs @@ -12,8 +12,7 @@ use e3_events::{ AggregateConfig, AggregateId, BusHandle, CorrelationId, EffectsEnabled, EnclaveEvent, EnclaveEventData, Event, EventContextAccessors, EventPublisher, EventStoreQueryBy, EventStoreQueryResponse, EvmEventConfig, EvmEventConfigChain, HistoricalEvmEventsReceived, - HistoricalEvmSyncStart, HistoricalNetEventsReceived, HistoricalNetSyncStart, SeqAgg, SyncEnded, - Unsequenced, + HistoricalEvmSyncStart, SeqAgg, SyncEnded, Unsequenced, }; use e3_utils::actix::channel as actix_toolbox; use std::{ diff --git a/crates/test-helpers/src/usecase_helpers.rs b/crates/test-helpers/src/usecase_helpers.rs index 3372e9ca60..467d126963 100644 --- a/crates/test-helpers/src/usecase_helpers.rs +++ b/crates/test-helpers/src/usecase_helpers.rs @@ -168,8 +168,8 @@ pub fn get_decryption_keys( }) .collect::>()?; - // Similarly decrypt esi_sss - let esi_sss_collected: Vec> = shares + // Similarly decrypt esi_sss — shape [sender][esi_idx] + let per_sender_esi: Vec> = shares .iter() .map(|ts| { ts.esi_sss @@ -184,6 +184,17 @@ pub fn get_decryption_keys( }) .collect::>()?; + // Transpose to [esi_idx][sender] — CalculateDecryptionKey aggregates per smudging noise + let num_esi = per_sender_esi.first().map_or(0, |v| v.len()); + let esi_sss_collected: Vec> = (0..num_esi) + .map(|esi_idx| { + per_sender_esi + .iter() + .map(|sender_esi| sender_esi[esi_idx].clone()) + .collect() + }) + .collect(); + let CalculateDecryptionKeyResponse { es_poly_sum, sk_poly_sum, diff --git a/crates/trbfv/src/calculate_decryption_share.rs b/crates/trbfv/src/calculate_decryption_share.rs index e2a5e5e5ec..cc7cdcb18f 100644 --- a/crates/trbfv/src/calculate_decryption_share.rs +++ b/crates/trbfv/src/calculate_decryption_share.rs @@ -120,11 +120,15 @@ pub fn calculate_decryption_share( .map(|(index, ciphertext)| { let share_manager = ShareManager::new(num_ciphernodes, threshold, params.clone()); info!("Create decryption share for ct index {}...", index); + // Currently there is a single smudging noise polynomial shared across all + // ciphertexts. When multiple per-ciphertext noises are supported, the + // index mapping here will need to change. + let es_idx = index % es_poly_sum.len(); share_manager .decryption_share( Arc::new(ciphertext), sk_poly_sum.clone(), - es_poly_sum[index].clone(), + es_poly_sum[es_idx].clone(), ) .context(format!("Could not decrypt ciphertext {}", index)) }) diff --git a/crates/zk-prover/src/actors/mod.rs b/crates/zk-prover/src/actors/mod.rs index 4a589ac764..e9cf0d71d5 100644 --- a/crates/zk-prover/src/actors/mod.rs +++ b/crates/zk-prover/src/actors/mod.rs @@ -13,6 +13,7 @@ //! ### Core Actors (Business Logic - No IO) //! - [`ProofRequestActor`]: Converts `EncryptionKeyPending` → `ComputeRequest` and handles responses //! - [`ProofVerificationActor`]: Verifies `EncryptionKeyReceived` and converts to `EncryptionKeyCreated` +//! - [`ShareVerificationActor`]: Handles ECDSA + ZK verification for C2/C3/C4 share proofs //! //! ### IO Actors (File System Operations) //! - [`ZkActor`]: Performs actual proof generation/verification using disk-based circuits and bb binary @@ -34,12 +35,14 @@ pub mod proof_request; pub mod proof_verification; +pub mod share_verification; pub mod zk_actor; pub use proof_request::ProofRequestActor; pub use proof_verification::{ ProofVerificationActor, ZkVerificationRequest, ZkVerificationResponse, }; +pub use share_verification::ShareVerificationActor; pub use zk_actor::ZkActor; use actix::{Actor, Addr}; @@ -58,11 +61,13 @@ pub fn setup_zk_actors(bus: &BusHandle, backend: &ZkBackend, signer: PrivateKeyS let proof_request = ProofRequestActor::setup(bus, signer); let proof_verification = ProofVerificationActor::setup(bus, verifier); + let share_verification = ShareVerificationActor::setup(bus); ZkActors { zk_actor, proof_request, proof_verification, + share_verification, } } @@ -71,4 +76,5 @@ pub struct ZkActors { pub zk_actor: Addr, pub proof_request: Addr, pub proof_verification: Addr, + pub share_verification: Addr, } diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index 6475c1fbea..4b9e07a003 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -11,14 +11,16 @@ use actix::{Actor, Addr, Context, Handler}; use alloy::signers::local::PrivateKeySigner; use e3_events::{ BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeResponse, - ComputeResponseKind, CorrelationId, DkgProofSigned, E3id, EnclaveEvent, EnclaveEventData, - EncryptionKey, EncryptionKeyCreated, EncryptionKeyPending, EventContext, EventPublisher, - EventSubscriber, EventType, PkBfvProofRequest, PkGenerationProofSigned, Proof, ProofPayload, - ProofType, Sequenced, SignedProofPayload, ThresholdShare, ThresholdShareCreated, - ThresholdSharePending, TypedEvent, ZkRequest, ZkResponse, + ComputeResponseKind, CorrelationId, DecryptionKeyShared, DecryptionShareProofsPending, + DkgProofSigned, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCreated, + EncryptionKeyPending, EventContext, EventPublisher, EventSubscriber, EventType, + PkBfvProofRequest, PkGenerationProofSigned, Proof, ProofPayload, ProofType, Sequenced, + SignedProofPayload, ThresholdShare, ThresholdShareCreated, ThresholdSharePending, TypedEvent, + ZkRequest, ZkResponse, }; +use e3_utils::utility_types::ArcBytes; use e3_utils::NotifySync; -use tracing::{error, info}; +use tracing::{error, info, warn}; #[derive(Clone, Debug)] enum ThresholdProofKind { @@ -130,6 +132,33 @@ impl PendingThresholdProofs { } } +#[derive(Clone, Debug)] +enum DecryptionProofKind { + SecretKey, + SmudgingNoise { esi_idx: usize }, +} + +/// Pending C4 (DkgShareDecryption) proof generation state. +#[derive(Clone, Debug)] +struct PendingDecryptionProofs { + party_id: u64, + node: String, + sk_poly_sum: ArcBytes, + es_poly_sum: Vec, + ec: EventContext, + sk_proof: Option, + esm_proofs: HashMap, + expected_esm_count: usize, +} + +impl PendingDecryptionProofs { + fn is_complete(&self) -> bool { + self.sk_proof.is_some() + && self.esm_proofs.len() == self.expected_esm_count + && (0..self.expected_esm_count).all(|i| self.esm_proofs.contains_key(&i)) + } +} + /// Core actor that handles encryption key proof requests. /// /// Proofs are always wrapped in a [`SignedProofPayload`] before being published, @@ -141,6 +170,10 @@ pub struct ProofRequestActor { pending: HashMap, threshold_correlation: HashMap, pending_threshold: HashMap, + /// C4 proof staging: correlation → (e3_id, kind) + decryption_correlation: HashMap, + /// C4 pending proofs per E3 + pending_decryption: HashMap, } impl ProofRequestActor { @@ -151,6 +184,8 @@ impl ProofRequestActor { pending: HashMap::new(), pending_threshold: HashMap::new(), threshold_correlation: HashMap::new(), + decryption_correlation: HashMap::new(), + pending_decryption: HashMap::new(), } } @@ -160,6 +195,7 @@ impl ProofRequestActor { bus.subscribe(EventType::ComputeResponse, addr.clone().into()); bus.subscribe(EventType::ComputeRequestError, addr.clone().into()); bus.subscribe(EventType::ThresholdSharePending, addr.clone().into()); + bus.subscribe(EventType::DecryptionShareProofsPending, addr.clone().into()); addr } @@ -347,12 +383,202 @@ impl ProofRequestActor { self.handle_threshold_proof_response(&msg.correlation_id, resp.proof.clone()); } ComputeResponseKind::Zk(ZkResponse::DkgShareDecryption(resp)) => { - self.handle_threshold_proof_response(&msg.correlation_id, resp.proof.clone()); + // Try C4 decryption proof first, then fall back to C1/C2/C3 threshold + if self + .decryption_correlation + .contains_key(&msg.correlation_id) + { + self.handle_decryption_proof_response(&msg.correlation_id, resp.proof.clone()); + } else { + self.handle_threshold_proof_response(&msg.correlation_id, resp.proof.clone()); + } } _ => {} } } + /// Handle DecryptionShareProofsPending: dispatch C4 proof generation. + fn handle_decryption_share_proofs_pending( + &mut self, + msg: TypedEvent, + ) { + let (msg, ec) = msg.into_components(); + let e3_id = msg.e3_id.clone(); + let esm_count = msg.esm_requests.len(); + + if self.pending_decryption.contains_key(&e3_id) { + warn!( + "Duplicate DecryptionShareProofsPending for E3 {} — ignoring", + e3_id + ); + return; + } + + self.pending_decryption.insert( + e3_id.clone(), + PendingDecryptionProofs { + party_id: msg.party_id, + node: msg.node, + sk_poly_sum: msg.sk_poly_sum, + es_poly_sum: msg.es_poly_sum, + ec: ec.clone(), + sk_proof: None, + esm_proofs: HashMap::new(), + expected_esm_count: esm_count, + }, + ); + + // C4a: SecretKey decryption proof + let sk_corr = CorrelationId::new(); + self.decryption_correlation + .insert(sk_corr, (e3_id.clone(), DecryptionProofKind::SecretKey)); + info!( + "Requesting C4a DkgShareDecryption proof (SecretKey) for E3 {}", + e3_id + ); + if let Err(err) = self.bus.publish( + ComputeRequest::zk( + ZkRequest::DkgShareDecryption(msg.sk_request), + sk_corr, + e3_id.clone(), + ), + ec.clone(), + ) { + error!("Failed to publish C4a proof request: {err}"); + self.decryption_correlation + .retain(|_, (eid, _)| *eid != e3_id); + self.pending_decryption.remove(&e3_id); + return; + } + + // C4b: SmudgingNoise decryption proofs + for (esi_idx, esm_req) in msg.esm_requests.into_iter().enumerate() { + let esm_corr = CorrelationId::new(); + self.decryption_correlation.insert( + esm_corr, + ( + e3_id.clone(), + DecryptionProofKind::SmudgingNoise { esi_idx }, + ), + ); + info!( + "Requesting C4b DkgShareDecryption proof (SmudgingNoise[{}]) for E3 {}", + esi_idx, e3_id + ); + if let Err(err) = self.bus.publish( + ComputeRequest::zk( + ZkRequest::DkgShareDecryption(esm_req), + esm_corr, + e3_id.clone(), + ), + ec.clone(), + ) { + error!("Failed to publish C4b proof request: {err}"); + self.decryption_correlation + .retain(|_, (eid, _)| *eid != e3_id); + self.pending_decryption.remove(&e3_id); + return; + } + } + } + + /// Handle a C4 proof response — store and check completeness. + fn handle_decryption_proof_response(&mut self, correlation_id: &CorrelationId, proof: Proof) { + let Some((e3_id, kind)) = self.decryption_correlation.remove(correlation_id) else { + return; + }; + + let Some(pending) = self.pending_decryption.get_mut(&e3_id) else { + error!( + "No pending decryption proofs for E3 {} — orphan correlation", + e3_id + ); + return; + }; + + match kind { + DecryptionProofKind::SecretKey => { + info!("Received C4a SK decryption proof for E3 {}", e3_id); + pending.sk_proof = Some(proof); + } + DecryptionProofKind::SmudgingNoise { esi_idx } => { + info!( + "Received C4b ESM decryption proof [{}] for E3 {}", + esi_idx, e3_id + ); + pending.esm_proofs.insert(esi_idx, proof); + } + } + + if pending.is_complete() { + info!( + "All C4 proofs complete for E3 {} — signing and publishing DecryptionKeyShared", + e3_id + ); + let pending = self.pending_decryption.remove(&e3_id).unwrap(); + self.sign_and_publish_decryption_key_shared(&e3_id, pending); + } + } + + /// Sign all C4 proofs and publish DecryptionKeyShared (Exchange #3). + fn sign_and_publish_decryption_key_shared( + &mut self, + e3_id: &E3id, + pending: PendingDecryptionProofs, + ) { + // Sign C4a (SK decryption proof) + let Some(signed_sk) = self.sign_proof( + e3_id, + ProofType::T2DkgShareDecryption, + pending.sk_proof.expect("checked in is_complete"), + ) else { + error!("Failed to sign C4a SK proof — DecryptionKeyShared will not be published"); + return; + }; + + // Sign C4b (ESM decryption proofs) in esi_idx order + let mut signed_esms = Vec::with_capacity(pending.expected_esm_count); + for idx in 0..pending.expected_esm_count { + let proof = pending + .esm_proofs + .get(&idx) + .expect("checked in is_complete") + .clone(); + let Some(signed) = self.sign_proof(e3_id, ProofType::T2DkgShareDecryption, proof) + else { + error!( + "Failed to sign C4b ESM proof [{}] — DecryptionKeyShared will not be published", + idx + ); + return; + }; + signed_esms.push(signed); + } + + info!( + "All C4 proofs signed for E3 {} party {} (signer: {})", + e3_id, + pending.party_id, + self.signer.address() + ); + + if let Err(err) = self.bus.publish( + DecryptionKeyShared { + e3_id: e3_id.clone(), + party_id: pending.party_id, + node: pending.node, + sk_poly_sum: pending.sk_poly_sum, + es_poly_sum: pending.es_poly_sum, + signed_sk_decryption_proof: signed_sk, + signed_esm_decryption_proofs: signed_esms, + external: false, + }, + pending.ec, + ) { + error!("Failed to publish DecryptionKeyShared: {err}"); + } + } + fn handle_threshold_proof_response(&mut self, correlation_id: &CorrelationId, proof: Proof) { let Some((e3_id, kind)) = self.threshold_correlation.remove(correlation_id) else { return; @@ -662,6 +888,16 @@ impl ProofRequestActor { .retain(|_, (eid, _)| *eid != e3_id); self.pending_threshold.remove(&e3_id); } + + if let Some((e3_id, kind)) = self.decryption_correlation.remove(msg.correlation_id()) { + error!( + "C4 {:?} proof request failed for E3 {}: {err} — DecryptionKeyShared will not be published", + kind, e3_id + ); + self.decryption_correlation + .retain(|_, (eid, _)| *eid != e3_id); + self.pending_decryption.remove(&e3_id); + } } } @@ -688,6 +924,9 @@ impl Handler for ProofRequestActor { EnclaveEventData::ComputeRequestError(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::DecryptionShareProofsPending(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } _ => (), } } @@ -740,3 +979,15 @@ impl Handler> for ProofRequestActor { self.handle_compute_request_error(msg) } } + +impl Handler> for ProofRequestActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_decryption_share_proofs_pending(msg) + } +} diff --git a/crates/zk-prover/src/actors/proof_verification.rs b/crates/zk-prover/src/actors/proof_verification.rs index c0fe9ae3c4..eb1ded01b7 100644 --- a/crates/zk-prover/src/actors/proof_verification.rs +++ b/crates/zk-prover/src/actors/proof_verification.rs @@ -134,6 +134,16 @@ impl ProofVerificationActor { } }; + // Validate circuit name matches expected ProofType circuits + let expected_circuits = signed.payload.proof_type.circuit_names(); + if !expected_circuits.contains(&signed.payload.proof.circuit) { + error!( + "Circuit name mismatch for key from party {}: expected {:?}, got {:?}", + msg.key.party_id, expected_circuits, signed.payload.proof.circuit + ); + return; + } + // Store the signed payload so we can reference it in the verification response self.pending.insert( (msg.e3_id.clone(), msg.key.party_id), diff --git a/crates/zk-prover/src/actors/share_verification.rs b/crates/zk-prover/src/actors/share_verification.rs new file mode 100644 index 0000000000..439122f6ef --- /dev/null +++ b/crates/zk-prover/src/actors/share_verification.rs @@ -0,0 +1,584 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! Actor for C2/C3/C4 share proof verification. +//! +//! Follows the same pattern as [`ProofVerificationActor`] (for C0/T0) — sits +//! between the raw proof data and the verified result, handling ECDSA validation +//! and ZK verification orchestration. +//! +//! ## Flow +//! +//! 1. Receives [`ShareVerificationDispatched`] from [`ThresholdKeyshare`]. +//! 2. Performs ECDSA validation (signature recovery, signer consistency, e3_id, +//! circuit name) — lightweight, no thread pool needed. +//! 3. Dispatches ZK-only verification to multithread via [`ComputeRequest`]. +//! 4. Receives [`ComputeResponse`] from multithread with pure ZK results. +//! 5. Combines ECDSA + ZK results. +//! 6. Emits [`SignedProofFailed`] for any failing proofs. +//! 7. Publishes [`ShareVerificationComplete`] with dishonest party set. + +use std::collections::{BTreeSet, HashMap, HashSet}; + +use actix::{Actor, Addr, Context, Handler}; +use alloy::primitives::Address; +use e3_events::{ + BusHandle, ComputeRequest, ComputeRequestError, ComputeResponse, ComputeResponseKind, + CorrelationId, E3id, EnclaveEvent, EnclaveEventData, EventContext, EventPublisher, + EventSubscriber, EventType, PartyProofsToVerify, PartyShareDecryptionProofsToVerify, + PartyVerificationResult, Sequenced, ShareVerificationComplete, ShareVerificationDispatched, + SignedProofFailed, SignedProofPayload, TypedEvent, VerificationKind, + VerifyShareDecryptionProofsRequest, VerifyShareProofsRequest, ZkRequest, ZkResponse, +}; +use e3_utils::NotifySync; +use tracing::{error, info, warn}; + +/// ECDSA validation result for a single party. +struct EcdsaPartyResult { + sender_party_id: u64, + passed: bool, + /// The pair (signed_payload, recovered_address) of the first failing proof, if any. + failed_payload: Option<(SignedProofPayload, Option
)>, +} + +/// Pending verification state — stored while ZK verification is in flight. +struct PendingVerification { + e3_id: E3id, + kind: VerificationKind, + ec: EventContext, + /// Parties that failed ECDSA (dishonest before ZK runs). + ecdsa_dishonest: HashSet, + /// Pre-dishonest parties from the dispatch (missing/incomplete proofs). + pre_dishonest: BTreeSet, + /// 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, +} + +/// Actor that handles C2/C3/C4 share proof verification. +/// +/// Separates ECDSA validation (lightweight, done inline) from ZK proof +/// verification (heavyweight, delegated to multithread). Emits +/// [`SignedProofFailed`] for fault attribution and [`ShareVerificationComplete`] +/// with the final dishonest party set. +pub struct ShareVerificationActor { + bus: BusHandle, + /// Tracks pending verifications by correlation ID. + pending: HashMap, +} + +impl ShareVerificationActor { + pub fn new(bus: &BusHandle) -> Self { + Self { + bus: bus.clone(), + pending: HashMap::new(), + } + } + + pub fn setup(bus: &BusHandle) -> Addr { + let addr = Self::new(bus).start(); + bus.subscribe(EventType::ShareVerificationDispatched, addr.clone().into()); + bus.subscribe(EventType::ComputeResponse, addr.clone().into()); + bus.subscribe(EventType::ComputeRequestError, addr.clone().into()); + addr + } + + fn handle_share_verification_dispatched( + &mut self, + msg: TypedEvent, + ) { + let (msg, ec) = msg.into_components(); + let e3_id = msg.e3_id.clone(); + + match msg.kind { + VerificationKind::ShareProofs => { + self.verify_share_proofs(e3_id, msg.share_proofs, msg.pre_dishonest, ec); + } + VerificationKind::DecryptionProofs => { + self.verify_decryption_proofs(e3_id, msg.decryption_proofs, msg.pre_dishonest, ec); + } + } + } + + /// C2/C3 verification: ECDSA check on each party, then dispatch ZK. + fn verify_share_proofs( + &mut self, + e3_id: E3id, + party_proofs: Vec, + pre_dishonest: BTreeSet, + ec: EventContext, + ) { + let e3_id_str = e3_id.to_string(); + let mut ecdsa_dishonest = HashSet::new(); + let mut ecdsa_passed_parties = Vec::new(); + let mut party_signed_payloads: HashMap> = HashMap::new(); + let mut party_addresses: HashMap = HashMap::new(); + + for party in &party_proofs { + let result = self.ecdsa_validate_signed_proofs( + party.sender_party_id, + &party.signed_proofs, + &e3_id_str, + "C2/C3", + ); + party_signed_payloads.insert(party.sender_party_id, party.signed_proofs.clone()); + if result.passed { + ecdsa_passed_parties.push(party.clone()); + } else { + ecdsa_dishonest.insert(party.sender_party_id); + if let Some((ref signed, addr)) = result.failed_payload { + self.emit_signed_proof_failed(&e3_id, signed, addr, &ec); + } + } + } + + // Store recovered addresses for passed parties + for party in &party_proofs { + if !ecdsa_dishonest.contains(&party.sender_party_id) { + if let Some(first_signed) = party.signed_proofs.first() { + if let Ok(addr) = first_signed.recover_address() { + party_addresses.insert(party.sender_party_id, addr); + } + } + } + } + + if ecdsa_passed_parties.is_empty() { + // All parties failed ECDSA — publish result immediately + let mut all_dishonest: BTreeSet = pre_dishonest; + all_dishonest.extend(ecdsa_dishonest); + self.publish_complete(e3_id, VerificationKind::ShareProofs, all_dishonest, ec); + return; + } + + // Dispatch ZK-only verification to multithread + let correlation_id = CorrelationId::new(); + let dispatched_party_ids: HashSet = ecdsa_passed_parties + .iter() + .map(|p| p.sender_party_id) + .collect(); + self.pending.insert( + correlation_id, + PendingVerification { + e3_id: e3_id.clone(), + kind: VerificationKind::ShareProofs, + ec: ec.clone(), + ecdsa_dishonest, + pre_dishonest, + dispatched_party_ids, + party_signed_payloads, + party_addresses, + }, + ); + + let request = ComputeRequest::zk( + ZkRequest::VerifyShareProofs(VerifyShareProofsRequest { + party_proofs: ecdsa_passed_parties, + }), + correlation_id, + e3_id.clone(), + ); + + if let Err(err) = self.bus.publish(request, ec.clone()) { + error!("Failed to dispatch ZK verification: {err}"); + 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); + } + } + } + + /// C4 verification: ECDSA check on each party, then dispatch ZK. + fn verify_decryption_proofs( + &mut self, + e3_id: E3id, + party_proofs: Vec, + pre_dishonest: BTreeSet, + ec: EventContext, + ) { + let e3_id_str = e3_id.to_string(); + let mut ecdsa_dishonest = HashSet::new(); + let mut ecdsa_passed_parties = Vec::new(); + let mut party_signed_payloads: HashMap> = HashMap::new(); + let mut party_addresses: HashMap = HashMap::new(); + + for party in &party_proofs { + // Flatten all signed proofs (SK + ESMs) + let all_signed: Vec<&SignedProofPayload> = + std::iter::once(&party.signed_sk_decryption_proof) + .chain(party.signed_esm_decryption_proofs.iter()) + .collect(); + let all_signed_cloned: Vec = + all_signed.iter().map(|s| (*s).clone()).collect(); + + let result = self.ecdsa_validate_signed_proofs( + party.sender_party_id, + &all_signed_cloned, + &e3_id_str, + "C4", + ); + party_signed_payloads.insert(party.sender_party_id, all_signed_cloned); + + if result.passed { + ecdsa_passed_parties.push(party.clone()); + } else { + ecdsa_dishonest.insert(party.sender_party_id); + if let Some((ref signed, addr)) = result.failed_payload { + self.emit_signed_proof_failed(&e3_id, signed, addr, &ec); + } + } + } + + // Store recovered addresses for passed parties + for party in &party_proofs { + if !ecdsa_dishonest.contains(&party.sender_party_id) { + if let Ok(addr) = party.signed_sk_decryption_proof.recover_address() { + party_addresses.insert(party.sender_party_id, addr); + } + } + } + + if ecdsa_passed_parties.is_empty() { + let mut all_dishonest: BTreeSet = pre_dishonest; + all_dishonest.extend(ecdsa_dishonest); + self.publish_complete(e3_id, VerificationKind::DecryptionProofs, all_dishonest, ec); + return; + } + + let correlation_id = CorrelationId::new(); + let dispatched_party_ids: HashSet = ecdsa_passed_parties + .iter() + .map(|p| p.sender_party_id) + .collect(); + self.pending.insert( + correlation_id, + PendingVerification { + e3_id: e3_id.clone(), + kind: VerificationKind::DecryptionProofs, + ec: ec.clone(), + ecdsa_dishonest, + pre_dishonest, + dispatched_party_ids, + party_signed_payloads, + party_addresses, + }, + ); + + let request = ComputeRequest::zk( + ZkRequest::VerifyShareDecryptionProofs(VerifyShareDecryptionProofsRequest { + party_proofs: ecdsa_passed_parties, + }), + correlation_id, + e3_id.clone(), + ); + + if let Err(err) = self.bus.publish(request, ec.clone()) { + error!("Failed to dispatch C4 ZK verification: {err}"); + if let Some(pending) = self.pending.remove(&correlation_id) { + let mut all_dishonest: BTreeSet = pending.pre_dishonest; + all_dishonest.extend(pending.ecdsa_dishonest); + // Dispatched parties were never ZK-verified — treat as dishonest + all_dishonest.extend(pending.dispatched_party_ids); + self.publish_complete(e3_id, VerificationKind::DecryptionProofs, all_dishonest, ec); + } + } + } + + /// Validate ECDSA properties for a set of signed proofs from one party: + /// 1. e3_id match + /// 2. Signature recovery (valid ECDSA) + /// 3. Signer consistency (all proofs from same address) + /// 4. Circuit name matches expected ProofType circuits + fn ecdsa_validate_signed_proofs( + &self, + sender_party_id: u64, + signed_proofs: &[SignedProofPayload], + e3_id_str: &str, + label: &str, + ) -> EcdsaPartyResult { + let mut expected_addr: Option
= None; + + for signed in signed_proofs { + // 1. e3_id match + if signed.payload.e3_id.to_string() != e3_id_str { + info!( + "{} proof from party {} has wrong e3_id ({} vs {})", + label, sender_party_id, signed.payload.e3_id, e3_id_str + ); + return EcdsaPartyResult { + sender_party_id, + passed: false, + failed_payload: Some((signed.clone(), expected_addr)), + }; + } + + // 2. Signature recovery + match signed.recover_address() { + Ok(addr) => { + // 3. Signer consistency + match &expected_addr { + Some(ea) if *ea != addr => { + info!( + "{} inconsistent signer for party {}", + label, sender_party_id + ); + return EcdsaPartyResult { + sender_party_id, + passed: false, + failed_payload: Some((signed.clone(), Some(addr))), + }; + } + None => expected_addr = Some(addr), + _ => {} + } + } + Err(e) => { + info!( + "{} signature recovery failed for party {} ({:?}): {}", + label, sender_party_id, signed.payload.proof_type, e + ); + return EcdsaPartyResult { + sender_party_id, + passed: false, + failed_payload: Some((signed.clone(), expected_addr)), + }; + } + } + + // 4. Circuit name validation + let expected_circuits = signed.payload.proof_type.circuit_names(); + if !expected_circuits.contains(&signed.payload.proof.circuit) { + info!( + "{} circuit mismatch for party {}: expected {:?}, got {:?}", + label, sender_party_id, expected_circuits, signed.payload.proof.circuit + ); + return EcdsaPartyResult { + sender_party_id, + passed: false, + failed_payload: Some((signed.clone(), expected_addr)), + }; + } + } + + EcdsaPartyResult { + sender_party_id, + passed: true, + failed_payload: None, + } + } + + /// Handle ZK verification response from multithread. + fn handle_compute_response(&mut self, msg: TypedEvent) { + let (msg, _ec) = msg.into_components(); + + let correlation_id = msg.correlation_id; + let Some(pending) = self.pending.remove(&correlation_id) else { + return; // Not our correlation ID + }; + + let zk_results: Vec = match (&pending.kind, msg.response) { + ( + VerificationKind::ShareProofs, + ComputeResponseKind::Zk(ZkResponse::VerifyShareProofs(r)), + ) => r.party_results, + ( + VerificationKind::DecryptionProofs, + ComputeResponseKind::Zk(ZkResponse::VerifyShareDecryptionProofs(r)), + ) => r.party_results, + _ => { + error!("Unexpected ComputeResponse kind for verification — treating all dispatched parties as dishonest"); + let mut all_dishonest: BTreeSet = pending.pre_dishonest; + all_dishonest.extend(pending.ecdsa_dishonest); + all_dishonest.extend(pending.dispatched_party_ids); + self.publish_complete(pending.e3_id, pending.kind, all_dishonest, pending.ec); + return; + } + }; + + let mut all_dishonest: BTreeSet = pending.pre_dishonest; + all_dishonest.extend(&pending.ecdsa_dishonest); + + // Cross-check: every dispatched party must appear in results. + // If any party is missing from the ZK response, treat as dishonest (defense-in-depth). + let returned_party_ids: HashSet = + zk_results.iter().map(|r| r.sender_party_id).collect(); + for &dispatched_pid in &pending.dispatched_party_ids { + if !returned_party_ids.contains(&dispatched_pid) { + warn!( + "Party {} was dispatched for ZK verification but missing from results — treating as dishonest", + dispatched_pid + ); + all_dishonest.insert(dispatched_pid); + } + } + + for result in &zk_results { + // Ignore results for parties we never dispatched (defense-in-depth) + if !pending + .dispatched_party_ids + .contains(&result.sender_party_id) + { + warn!( + "ZK result for party {} was not dispatched — ignoring", + result.sender_party_id + ); + continue; + } + if !result.all_verified { + all_dishonest.insert(result.sender_party_id); + + // Emit SignedProofFailed for ZK failure + if let Some(ref signed) = result.failed_signed_payload { + let addr = pending + .party_addresses + .get(&result.sender_party_id) + .copied(); + self.emit_signed_proof_failed(&pending.e3_id, signed, addr, &pending.ec); + } + } + } + + self.publish_complete(pending.e3_id, pending.kind, all_dishonest, pending.ec); + } + + fn emit_signed_proof_failed( + &self, + e3_id: &E3id, + signed_payload: &SignedProofPayload, + recovered_addr: Option
, + ec: &EventContext, + ) { + let faulting_node = match recovered_addr { + Some(addr) => addr, + None => match signed_payload.recover_address() { + Ok(addr) => addr, + Err(err) => { + warn!("Cannot attribute fault — signature recovery failed: {err}"); + return; + } + }, + }; + + if let Err(err) = self.bus.publish( + SignedProofFailed { + e3_id: e3_id.clone(), + faulting_node, + proof_type: signed_payload.payload.proof_type, + signed_payload: signed_payload.clone(), + }, + ec.clone(), + ) { + error!("Failed to publish SignedProofFailed: {err}"); + } + } + + /// Handle computation error from multithread — clean up pending state and + /// publish ShareVerificationComplete treating all dispatched parties as dishonest. + fn handle_compute_request_error(&mut self, msg: TypedEvent) { + let (msg, _ec) = msg.into_components(); + + let correlation_id = msg.correlation_id(); + let Some(pending) = self.pending.remove(correlation_id) else { + return; + }; + + error!( + "ZK verification computation failed for E3 {} ({:?}): {} — treating all dispatched parties as dishonest", + pending.e3_id, pending.kind, msg + ); + + let mut all_dishonest: BTreeSet = pending.pre_dishonest; + all_dishonest.extend(pending.ecdsa_dishonest); + all_dishonest.extend(pending.dispatched_party_ids); + self.publish_complete(pending.e3_id, pending.kind, all_dishonest, pending.ec); + } + + fn publish_complete( + &self, + e3_id: E3id, + kind: VerificationKind, + dishonest_parties: BTreeSet, + ec: EventContext, + ) { + if let Err(err) = self.bus.publish( + ShareVerificationComplete { + e3_id, + kind, + dishonest_parties, + }, + ec, + ) { + error!("Failed to publish ShareVerificationComplete: {err}"); + } + } +} + +impl Actor for ShareVerificationActor { + type Context = Context; +} + +impl Handler for ShareVerificationActor { + type Result = (); + + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + let (msg, ec) = msg.into_components(); + match msg { + EnclaveEventData::ShareVerificationDispatched(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::ComputeResponse(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::ComputeRequestError(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + _ => (), + } + } +} + +impl Handler> for ShareVerificationActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_share_verification_dispatched(msg) + } +} + +impl Handler> for ShareVerificationActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_compute_response(msg) + } +} + +impl Handler> for ShareVerificationActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_compute_request_error(msg) + } +} diff --git a/crates/zk-prover/src/lib.rs b/crates/zk-prover/src/lib.rs index 3958210e2b..4d03e13923 100644 --- a/crates/zk-prover/src/lib.rs +++ b/crates/zk-prover/src/lib.rs @@ -15,8 +15,8 @@ mod traits; mod witness; pub use actors::{ - setup_zk_actors, ProofRequestActor, ProofVerificationActor, ZkActors, ZkVerificationRequest, - ZkVerificationResponse, + setup_zk_actors, ProofRequestActor, ProofVerificationActor, ShareVerificationActor, ZkActors, + ZkVerificationRequest, ZkVerificationResponse, }; pub use backend::{SetupStatus, ZkBackend}; diff --git a/crates/zk-prover/tests/common/mod.rs b/crates/zk-prover/tests/common/mod.rs index 077e55df08..a16725b4f2 100644 --- a/crates/zk-prover/tests/common/mod.rs +++ b/crates/zk-prover/tests/common/mod.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -pub mod helpers; +mod helpers; pub use helpers::*; use std::path::PathBuf;