diff --git a/agent/flow-trace/00_INDEX.md b/agent/flow-trace/00_INDEX.md index ef24c5429b..9c51134355 100644 --- a/agent/flow-trace/00_INDEX.md +++ b/agent/flow-trace/00_INDEX.md @@ -84,6 +84,9 @@ 10. PROOF FAIL A committee member submits an invalid proof (C0-C7) → ProofVerificationActor / ShareVerificationActor detects → SignedProofFailed triggers AccusationManager + OR: Commitment consistency mismatch detected (cross-circuit) + → CommitmentConsistencyChecker publishes CommitmentConsistencyViolation + → Also triggers AccusationManager 11. ACCUSATION AccusationManager creates ProofFailureAccusation → Signed and broadcast via P2P gossip diff --git a/agent/flow-trace/04_DKG_AND_COMPUTATION.md b/agent/flow-trace/04_DKG_AND_COMPUTATION.md index 79fba8bfdb..f84608d22f 100644 --- a/agent/flow-trace/04_DKG_AND_COMPUTATION.md +++ b/agent/flow-trace/04_DKG_AND_COMPUTATION.md @@ -308,16 +308,34 @@ ShareVerificationActor receives ShareVerificationDispatched(kind=ShareProofs) │ │ │ └─ Emit SignedProofFailed { accused, proof_type } │ │ │ → Triggers accusation pipeline (see Part 5) │ │ │ -│ │ └─ If ECDSA passes: cache recovered address, proceed to ZK +│ │ └─ If ECDSA passes: cache recovered address, proceed │ │ -│ └─ Store PendingVerification { -│ ecdsa_dishonest, pre_dishonest, dispatched_party_ids, recovered_addresses +│ └─ Store PendingConsistencyCheck { +│ ecdsa_dishonest, pre_dishonest, dispatched_party_ids, +│ recovered_addresses, party_proofs (for ZK dispatch) │ } │ -├─ PHASE 2: Heavy ZK Verification (dispatched to multithread): +├─ PHASE 2: Commitment Consistency Check (dispatched to per-E3 checker): +│ │ +│ ├─ Publishes CommitmentConsistencyCheckRequested { +│ │ correlation_id, kind, party_proofs: [(party_id, address, proofs)] +│ │ } +│ │ +│ ├─ CommitmentConsistencyChecker (per-E3 actor) receives this: +│ │ ├─ Caches each party's (address, proof_type) → {public_signals, data_hash} +│ │ ├─ Evaluates all registered CommitmentLinks (e.g. C1→C5 pk_commitment) +│ │ ├─ On mismatch: publishes CommitmentConsistencyViolation +│ │ │ → AccusationManager initiates accusation quorum (see Part 5) +│ │ └─ Responds with CommitmentConsistencyCheckComplete { inconsistent_parties } +│ │ +│ └─ On CommitmentConsistencyCheckComplete: +│ ├─ Merge inconsistent_parties into dishonest set +│ └─ Proceed to Phase 3 with remaining honest parties +│ +├─ PHASE 3: Heavy ZK Verification (dispatched to multithread): │ │ │ ├─ Publishes ComputeRequest::zk(VerifyShareProofsRequest { -│ │ party_proofs, // all ECDSA-passing parties' ZK proof data +│ │ party_proofs, // consistency-passing parties' ZK proof data │ │ }) │ │ │ ├─ ZkActor verifies each proof via: bb verify -k vk -p proof @@ -331,7 +349,7 @@ ShareVerificationActor receives ShareVerificationDispatched(kind=ShareProofs) │ │ │ └─ Publish ShareVerificationComplete { │ kind: ShareProofs, -│ dishonest_parties: {pre_dishonest ∪ ecdsa_fails ∪ zk_fails} +│ dishonest_parties: {pre_dishonest ∪ ecdsa_fails ∪ consistency_fails ∪ zk_fails} │ } │ └─ ThresholdKeyshare receives ShareVerificationComplete: diff --git a/agent/flow-trace/05_FAILURE_REFUND_SLASHING.md b/agent/flow-trace/05_FAILURE_REFUND_SLASHING.md index 7e7ecc3834..9d88c27b10 100644 --- a/agent/flow-trace/05_FAILURE_REFUND_SLASHING.md +++ b/agent/flow-trace/05_FAILURE_REFUND_SLASHING.md @@ -244,45 +244,55 @@ LIFECYCLE: #### Step 1: Local Proof Failure Detection ``` -ProofVerificationFailed event arrives at AccusationManager +ProofVerificationFailed OR CommitmentConsistencyViolation event arrives │ -├─ 1. Resolve accused address: -│ If accused_address == 0x0: -│ Look up from committee list by party_id -│ -├─ 2. Cache verification result: -│ received_data[(accused, proof_type)] = { data_hash, passed: false } -│ -├─ 3. Dedup check: -│ If (accused, proof_type) already in accused_proofs set: -│ → Return (already accused, skip) -│ Else: insert into accused_proofs -│ -├─ 4. For C3a/C3b proofs: attach signed_payload for re-verification -│ → Other nodes need the original proof to independently verify -│ → Non-C3 proofs don't need forwarding (nodes have the data locally) -│ -├─ 5. Create and SIGN accusation: -│ ProofFailureAccusation { -│ e3_id, accuser: my_address, accused, accused_party_id, -│ proof_type, data_hash, signed_payload (C3 only), -│ signature: ecSign(accusation_digest) -│ } -│ -├─ 6. Broadcast accusation via P2P gossip -│ -├─ 7. Cast OWN VOTE (agrees = true): -│ AccusationVote { -│ e3_id, accusation_id, voter: my_address, -│ agrees: true, data_hash, -│ signature: ecSign(vote_digest) -│ } -│ → Broadcast via P2P gossip +├─ For ProofVerificationFailed: +│ ├─ 1. Resolve accused address: +│ │ If accused_address == 0x0: +│ │ Look up from committee list by party_id +│ │ +│ ├─ 2. Cache verification result: +│ │ received_data[(accused, proof_type)] = { data_hash, passed: false } +│ │ +│ ├─ 3. For C3a/C3b proofs: attach signed_payload for re-verification +│ │ → Other nodes need the original proof to independently verify +│ │ +│ └─ 4. Delegate to initiate_accusation() │ -├─ 8. Start vote timeout (300 seconds): -│ → If quorum not reached by timeout, resolve as Inconclusive +├─ For CommitmentConsistencyViolation: +│ ├─ 1. Cache verification result: +│ │ received_data[(accused, proof_type)] = { data_hash, passed: false } +│ │ +│ └─ 2. Delegate to initiate_accusation() (no forwarded payload) │ -└─ 9. Check for immediate quorum (if threshold_m == 1) +└─ initiate_accusation() — shared logic: + │ + ├─ 3. Dedup check: + │ If (accused, proof_type) already in accused_proofs set: + │ → Return (already accused, skip) + │ Else: insert into accused_proofs + │ + ├─ 4. Create and SIGN accusation: + │ ProofFailureAccusation { + │ e3_id, accuser: my_address, accused, accused_party_id, + │ proof_type, data_hash, signed_payload (C3 only), + │ signature: ecSign(accusation_digest) + │ } + │ + ├─ 5. Broadcast accusation via P2P gossip + │ + ├─ 6. Cast OWN VOTE (agrees = true): + │ AccusationVote { + │ e3_id, accusation_id, voter: my_address, + │ agrees: true, data_hash, + │ signature: ecSign(vote_digest) + │ } + │ → Broadcast via P2P gossip + │ + ├─ 7. Start vote timeout (300 seconds): + │ → If quorum not reached by timeout, resolve as Inconclusive + │ + └─ 8. Check for immediate quorum (if threshold_m == 1) ``` #### Step 2: Incoming Accusation Handling diff --git a/crates/events/src/enclave_event/commitment_consistency.rs b/crates/events/src/enclave_event/commitment_consistency.rs new file mode 100644 index 0000000000..7cdcb21650 --- /dev/null +++ b/crates/events/src/enclave_event/commitment_consistency.rs @@ -0,0 +1,83 @@ +// 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 cross-circuit commitment consistency checking. +//! +//! The [`ShareVerificationActor`] publishes [`CommitmentConsistencyCheckRequested`] +//! after ECDSA validation but **before** ZK proof verification, carrying each +//! party's public signals. The per-E3 [`CommitmentConsistencyChecker`] caches +//! the signals, evaluates all registered commitment links, and responds with +//! [`CommitmentConsistencyCheckComplete`]. Only parties that pass the consistency +//! check proceed to ZK verification. + +use crate::{CorrelationId, E3id, ProofType, VerificationKind}; +use alloy::primitives::Address; +use e3_utils::utility_types::ArcBytes; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeSet; + +/// Per-party proof data for commitment consistency checking. +/// +/// Contains the public signals extracted from the party's ECDSA-validated +/// (but not yet ZK-verified) signed proofs. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PartyProofData { + pub party_id: u64, + pub address: Address, + /// Each entry is a `(proof_type, public_signals, data_hash)` tuple from a + /// signed proof. The `data_hash` is `keccak256(abi.encode(proof.data, + /// public_signals))` — used for the accusation protocol if a consistency + /// violation is detected. + pub proofs: Vec<(ProofType, ArcBytes, [u8; 32])>, +} + +/// Published by [`ShareVerificationActor`] after ECDSA validation, before ZK. +/// +/// Tells the [`CommitmentConsistencyChecker`] to cache proof data and evaluate +/// all registered commitment links. The checker responds with +/// [`CommitmentConsistencyCheckComplete`] on the same `correlation_id`. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct CommitmentConsistencyCheckRequested { + pub e3_id: E3id, + pub kind: VerificationKind, + pub correlation_id: CorrelationId, + pub party_proofs: Vec, +} + +/// Response from [`CommitmentConsistencyChecker`]. +/// +/// If `inconsistent_parties` is empty, all parties' commitments are consistent +/// with previously cached proofs and the verification pipeline may proceed to +/// ZK verification. Otherwise, the listed parties should be treated as +/// dishonest and excluded from ZK verification. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct CommitmentConsistencyCheckComplete { + pub e3_id: E3id, + pub kind: VerificationKind, + pub correlation_id: CorrelationId, + /// Parties whose commitments are inconsistent with previously cached proofs. + pub inconsistent_parties: BTreeSet, +} + +/// Emitted by [`CommitmentConsistencyChecker`] when a party's commitment +/// values are inconsistent across circuit proofs. +/// +/// Consumed by [`AccusationManager`] to initiate the off-chain accusation +/// quorum protocol — the same flow as [`ProofVerificationFailed`] but for +/// cross-circuit commitment mismatches rather than ZK proof failures. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct CommitmentConsistencyViolation { + pub e3_id: E3id, + /// Party whose commitment is inconsistent. + pub accused_party_id: u64, + /// Recovered Ethereum address of the accused party. + pub accused_address: Address, + /// The proof type (source side) whose commitment value doesn't match. + pub proof_type: ProofType, + /// `keccak256(abi.encode(proof.data, public_signals))` of the accused party's + /// proof — matches the data_hash used by the accusation protocol. + pub data_hash: [u8; 32], +} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 0ac3145177..c931eec527 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -12,6 +12,7 @@ mod ciphernode_added; mod ciphernode_removed; mod ciphernode_selected; mod ciphertext_output_published; +mod commitment_consistency; mod committee_finalize_requested; mod committee_finalized; mod committee_published; @@ -76,6 +77,7 @@ pub use ciphernode_added::*; pub use ciphernode_removed::*; pub use ciphernode_selected::*; pub use ciphertext_output_published::*; +pub use commitment_consistency::*; pub use committee_finalize_requested::*; pub use committee_finalized::*; pub use committee_published::*; @@ -296,6 +298,9 @@ pub enum EnclaveEventData { AggregationProofSigned(AggregationProofSigned), DKGInnerProofReady(DKGInnerProofReady), DKGRecursiveAggregationComplete(DKGRecursiveAggregationComplete), + CommitmentConsistencyCheckRequested(CommitmentConsistencyCheckRequested), + CommitmentConsistencyCheckComplete(CommitmentConsistencyCheckComplete), + CommitmentConsistencyViolation(CommitmentConsistencyViolation), /// This is a test event to use in testing TestEvent(TestEvent), } @@ -571,6 +576,13 @@ impl EnclaveEventData { EnclaveEventData::AggregationProofSigned(ref data) => Some(data.e3_id.clone()), EnclaveEventData::DKGRecursiveAggregationComplete(ref data) => Some(data.e3_id.clone()), EnclaveEventData::DKGInnerProofReady(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::CommitmentConsistencyCheckRequested(ref data) => { + Some(data.e3_id.clone()) + } + EnclaveEventData::CommitmentConsistencyCheckComplete(ref data) => { + Some(data.e3_id.clone()) + } + EnclaveEventData::CommitmentConsistencyViolation(ref data) => Some(data.e3_id.clone()), _ => None, } } @@ -662,7 +674,10 @@ impl_event_types!( AggregationProofPending, AggregationProofSigned, DKGInnerProofReady, - DKGRecursiveAggregationComplete + DKGRecursiveAggregationComplete, + CommitmentConsistencyCheckRequested, + CommitmentConsistencyCheckComplete, + CommitmentConsistencyViolation ); impl TryFrom<&EnclaveEvent> for EnclaveError { diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 22962d6a01..eb153f29ee 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -774,8 +774,10 @@ async fn test_trbfv_actor() -> Result<()> { let shares_to_pubkey_agg_timer = Instant::now(); const KS3: [&str; 3] = ["KeyshareCreated"; 3]; const DKG3: [&str; 3] = ["DKGRecursiveAggregationComplete"; 3]; - const C1_C5: [&str; 11] = [ + const C1_C5: [&str; 13] = [ "ShareVerificationDispatched", + "CommitmentConsistencyCheckRequested", + "CommitmentConsistencyCheckComplete", "ComputeRequest", "ComputeResponse", "ProofVerificationPassed", @@ -886,6 +888,8 @@ async fn test_trbfv_actor() -> Result<()> { // - 1 CiphertextOutputPublished (from shared bus) // - 3 DecryptionshareCreated (from simulate_libp2p, passes is_forwardable_event) // - 1 ShareVerificationDispatched (C6 verification dispatched by ThresholdPlaintextAggregator) + // - 1 CommitmentConsistencyCheckRequested (pre-ZK consistency check) + // - 1 CommitmentConsistencyCheckComplete (consistency check result) // - 1 ComputeRequest (C6 ZK verification) // - 1 ComputeResponse (C6 ZK verification result) // - 9 ProofVerificationPassed (3 parties × 3 C6 proofs per ciphertext) @@ -910,7 +914,19 @@ async fn test_trbfv_actor() -> Result<()> { }; // Sum matches the comment above through AggregationProofSigned, then fold, then PlaintextAggregated. // E3RequestComplete is not included (arrives after; not needed for take). - let expected_count = 1 + 3 + 1 + 2 + 9 + 1 + 2 + 1 + 2 + 1 + c6_fold_events + 1; + let expected_count = 1 // CiphertextOutputPublished + + 3 // DecryptionshareCreated + + 1 // ShareVerificationDispatched + + 2 // CommitmentConsistencyCheck (Requested + Complete) + + 2 // C6 ZK verification (ComputeRequest + ComputeResponse) + + 9 // ProofVerificationPassed (3 parties × 3 proofs) + + 1 // ShareVerificationComplete + + 2 // TrBFV computation (ComputeRequest + ComputeResponse) + + 1 // AggregationProofPending + + 2 // C7 proof (ComputeRequest + ComputeResponse) + + 1 // AggregationProofSigned + + c6_fold_events // C6 fold steps + + 1; // PlaintextAggregated let h = nodes .take_history_with_timeouts( diff --git a/crates/zk-prover/src/actors/accusation_manager.rs b/crates/zk-prover/src/actors/accusation_manager.rs index 4b72bd8e3b..428dd7b28a 100644 --- a/crates/zk-prover/src/actors/accusation_manager.rs +++ b/crates/zk-prover/src/actors/accusation_manager.rs @@ -35,12 +35,12 @@ use alloy::signers::local::PrivateKeySigner; use alloy::signers::SignerSync; use alloy::sol_types::SolValue; use e3_events::{ - AccusationOutcome, AccusationQuorumReached, AccusationVote, BusHandle, ComputeRequest, - ComputeRequestError, ComputeResponse, ComputeResponseKind, CorrelationId, E3id, EnclaveEvent, - EnclaveEventData, EventContext, EventPublisher, EventSubscriber, EventType, - PartyProofsToVerify, ProofFailureAccusation, ProofType, ProofVerificationFailed, - ProofVerificationPassed, Sequenced, SignedProofPayload, SlashExecuted, TypedEvent, - VerifyShareProofsRequest, ZkRequest, ZkResponse, + AccusationOutcome, AccusationQuorumReached, AccusationVote, BusHandle, + CommitmentConsistencyViolation, ComputeRequest, ComputeRequestError, ComputeResponse, + ComputeResponseKind, CorrelationId, E3id, EnclaveEvent, EnclaveEventData, EventContext, + EventPublisher, EventSubscriber, EventType, PartyProofsToVerify, ProofFailureAccusation, + ProofType, ProofVerificationFailed, ProofVerificationPassed, Sequenced, SignedProofPayload, + SlashExecuted, TypedEvent, VerifyShareProofsRequest, ZkRequest, ZkResponse, }; use e3_utils::NotifySync; use tracing::{error, info, warn}; @@ -174,6 +174,10 @@ impl AccusationManager { bus.subscribe(EventType::ComputeResponse, addr.clone().into()); bus.subscribe(EventType::ComputeRequestError, addr.clone().into()); bus.subscribe(EventType::SlashExecuted, addr.clone().into()); + bus.subscribe( + EventType::CommitmentConsistencyViolation, + addr.clone().into(), + ); addr } @@ -309,8 +313,8 @@ impl AccusationManager { /// Called when the local node detects a proof failure. /// - /// Creates and broadcasts a `ProofFailureAccusation`, casts own vote, - /// and begins vote collection with a timeout. + /// Resolves the accused address, caches the failure, extracts C3a/C3b + /// forwarding payload, then delegates to [`initiate_accusation`]. fn on_local_proof_failure( &mut self, event: ProofVerificationFailed, @@ -335,26 +339,15 @@ impl AccusationManager { event.accused_address }; - let key = (accused_address, event.proof_type); - // Cache the failed verification result self.received_data.insert( - key, + (accused_address, event.proof_type), ReceivedProofData { data_hash: event.data_hash, verification_passed: false, }, ); - // Dedup: don't create multiple accusations for the same (accused, proof_type) - if !self.accused_proofs.insert(key) { - info!( - "Already accused {:?} for {:?} — skipping duplicate", - accused_address, event.proof_type - ); - return; - } - // For C3a/C3b, include the signed payload so other nodes can re-verify let forwarded_payload = match event.proof_type { ProofType::C3aSkShareEncryption | ProofType::C3bESmShareEncryption => { @@ -363,14 +356,83 @@ impl AccusationManager { _ => None, }; + self.initiate_accusation( + accused_address, + event.accused_party_id, + event.proof_type, + event.data_hash, + forwarded_payload, + ec, + ctx, + ); + } + + /// Called when the `CommitmentConsistencyChecker` detects a cross-circuit + /// commitment mismatch for a party. + /// + /// Caches the failure and delegates to `initiate_accusation` — the same + /// quorum protocol as ZK proof failures. + fn on_consistency_violation( + &mut self, + data: CommitmentConsistencyViolation, + ec: &EventContext, + ctx: &mut Context, + ) { + // Cache as a failed verification for voting on future accusations + self.received_data.insert( + (data.accused_address, data.proof_type), + ReceivedProofData { + data_hash: data.data_hash, + verification_passed: false, + }, + ); + + self.initiate_accusation( + data.accused_address, + data.accused_party_id, + data.proof_type, + data.data_hash, + None, // No forwarding needed — violations are detected from public signals all nodes have + ec, + ctx, + ); + } + + /// Shared accusation creation and broadcast logic. + /// + /// Called by [`on_local_proof_failure`] (ZK verification failure) and + /// [`on_consistency_violation`] (commitment consistency mismatch). + /// Deduplicates, creates and signs a [`ProofFailureAccusation`], casts + /// the node's own vote, and begins vote collection with a timeout. + fn initiate_accusation( + &mut self, + accused_address: Address, + accused_party_id: u64, + proof_type: ProofType, + data_hash: [u8; 32], + forwarded_payload: Option, + ec: &EventContext, + ctx: &mut Context, + ) { + let key = (accused_address, proof_type); + + // Dedup: don't create multiple accusations for the same (accused, proof_type) + if !self.accused_proofs.insert(key) { + info!( + "Already accused {:?} for {:?} — skipping duplicate", + accused_address, proof_type + ); + return; + } + // Create the accusation let mut accusation = ProofFailureAccusation { e3_id: self.e3_id.clone(), accuser: self.my_address, accused: accused_address, - accused_party_id: event.accused_party_id, - proof_type: event.proof_type, - data_hash: event.data_hash, + accused_party_id, + proof_type, + data_hash, signed_payload: forwarded_payload, signature: Vec::new(), }; @@ -379,8 +441,8 @@ impl AccusationManager { let accusation_id = Self::accusation_id(&accusation); info!( - "Broadcasting accusation against {} for {:?} proof failure", - accused_address, event.proof_type + "Broadcasting accusation against {} for {:?} failure", + accused_address, proof_type ); // Broadcast accusation via gossip @@ -395,7 +457,7 @@ impl AccusationManager { accusation_id, voter: self.my_address, agrees: true, - data_hash: event.data_hash, + data_hash, signature: Vec::new(), }; own_vote.signature = self.sign_vote_digest(&own_vote); @@ -1115,6 +1177,9 @@ impl Handler for AccusationManager { EnclaveEventData::SlashExecuted(data) => { self.on_slash_executed(data); } + EnclaveEventData::CommitmentConsistencyViolation(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } _ => (), } } @@ -1198,3 +1263,16 @@ impl Handler> for AccusationManager { self.handle_reverification_error(msg); } } + +impl Handler> for AccusationManager { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + ctx: &mut Self::Context, + ) -> Self::Result { + let (data, ec) = msg.into_components(); + self.on_consistency_violation(data, &ec, ctx); + } +} diff --git a/crates/zk-prover/src/actors/commitment_consistency_checker.rs b/crates/zk-prover/src/actors/commitment_consistency_checker.rs index 30642cb45a..e86f601e37 100644 --- a/crates/zk-prover/src/actors/commitment_consistency_checker.rs +++ b/crates/zk-prover/src/actors/commitment_consistency_checker.rs @@ -6,9 +6,19 @@ //! Actor that cross-checks commitment values across different circuit proofs. //! -//! Subscribes to [`ProofVerificationPassed`] events and, for each registered -//! [`CommitmentLink`], compares commitment field values extracted from public -//! signals of related proof types. +//! Has two roles: +//! +//! 1. **Pre-ZK gating** (request/response): Subscribes to +//! [`CommitmentConsistencyCheckRequested`] from [`ShareVerificationActor`], +//! caches each party's public signals, evaluates all registered +//! [`CommitmentLink`]s, and responds with +//! [`CommitmentConsistencyCheckComplete`]. Inconsistent parties are excluded +//! from ZK verification. +//! +//! 2. **Post-ZK cross-circuit checking**: Subscribes to +//! [`ProofVerificationPassed`] events and, for each registered link, +//! compares commitment values across different circuit proofs. On mismatch, +//! publishes [`CommitmentConsistencyViolation`] for the accusation pipeline. //! //! ## Architecture //! @@ -24,19 +34,30 @@ use super::commitment_links::{CommitmentLink, LinkScope}; use actix::{Actor, Addr, Context, Handler}; use alloy::primitives::Address; use e3_events::{ - BusHandle, E3id, EnclaveEvent, EnclaveEventData, EventSubscriber, EventType, ProofType, - ProofVerificationPassed, TypedEvent, + BusHandle, CommitmentConsistencyCheckComplete, CommitmentConsistencyCheckRequested, + CommitmentConsistencyViolation, E3id, EnclaveEvent, EnclaveEventData, EventContext, + EventPublisher, EventSubscriber, EventType, ProofType, ProofVerificationPassed, Sequenced, + TypedEvent, }; use e3_utils::utility_types::ArcBytes; use e3_utils::NotifySync; -use std::collections::HashMap; -use tracing::{info, warn}; +use std::collections::{BTreeSet, HashMap}; +use tracing::{error, info, warn}; /// Cached data from a verified proof. struct VerifiedProofData { party_id: u64, address: Address, public_signals: ArcBytes, + data_hash: [u8; 32], +} + +/// Describes a source entry whose commitments are inconsistent with a target. +struct Mismatch { + party_id: u64, + address: Address, + proof_type: ProofType, + data_hash: [u8; 32], } /// Per-E3 actor that enforces cross-circuit commitment consistency. @@ -45,10 +66,6 @@ pub struct CommitmentConsistencyChecker { e3_id: E3id, links: Vec>, /// Verified proof outputs: `(address, proof_type) → data`. - /// - /// For cross-party links the target proof type may come from a different - /// address than the source, so lookups iterate over all entries whose - /// `proof_type` matches. verified: HashMap<(Address, ProofType), VerifiedProofData>, } @@ -65,109 +82,125 @@ impl CommitmentConsistencyChecker { pub fn setup(bus: &BusHandle, e3_id: E3id, links: Vec>) -> Addr { let actor = Self::new(bus, e3_id, links); let addr = actor.start(); + bus.subscribe( + EventType::CommitmentConsistencyCheckRequested, + addr.clone().into(), + ); bus.subscribe(EventType::ProofVerificationPassed, addr.clone().into()); addr } - /// Evaluate all registered links given a newly arrived proof. - fn check_links(&self, new_proof_type: ProofType, new_address: Address) { - for link in &self.links { - match link.scope() { - LinkScope::SameParty => { - self.check_same_party_link(link.as_ref(), new_proof_type, new_address); - } - LinkScope::CrossParty => { - self.check_cross_party_link(link.as_ref(), new_proof_type); - } - } - } - } - - /// Same-party: compare source and target from the same address. - fn check_same_party_link( - &self, - link: &dyn CommitmentLink, - new_proof_type: ProofType, - address: Address, - ) { + /// Find all source entries whose commitments are inconsistent with cached + /// targets for a given link. + fn find_mismatches(&self, link: &dyn CommitmentLink) -> Vec { let src_type = link.source_proof_type(); let tgt_type = link.target_proof_type(); - // Only run when the newly arrived proof completes a pair. - if new_proof_type != src_type && new_proof_type != tgt_type { - return; - } + match link.scope() { + LinkScope::SameParty => self + .verified + .keys() + .filter(|(_, pt)| *pt == src_type) + .filter_map(|(addr, _)| { + let src = self.verified.get(&(*addr, src_type))?; + let tgt = self.verified.get(&(*addr, tgt_type))?; + let vals = link.extract_source_values(&src.public_signals); + (!link.check_consistency(&vals, &tgt.public_signals)).then(|| Mismatch { + party_id: src.party_id, + address: *addr, + proof_type: src_type, + data_hash: src.data_hash, + }) + }) + .collect(), - let source = self.verified.get(&(address, src_type)); - let target = self.verified.get(&(address, tgt_type)); + LinkScope::CrossParty => { + let targets: Vec<_> = self + .verified + .iter() + .filter(|((_, pt), _)| *pt == tgt_type) + .map(|(_, v)| v) + .collect(); - if let (Some(src), Some(tgt)) = (source, target) { - let source_values = link.extract_source_values(&src.public_signals); - if !link.check_consistency(&source_values, &tgt.public_signals) { - warn!( - "[{}] Commitment mismatch for E3 {} — party {} ({}): \ - source {:?} vs target {:?} from same address", - link.name(), - self.e3_id, - src.party_id, - address, - src_type, - tgt_type, - ); + self.verified + .iter() + .filter(|((_, pt), _)| *pt == src_type) + .filter_map(|(_, src)| { + let vals = link.extract_source_values(&src.public_signals); + if vals.is_empty() { + return None; + } + targets + .iter() + .any(|tgt| !link.check_consistency(&vals, &tgt.public_signals)) + .then(|| Mismatch { + party_id: src.party_id, + address: src.address, + proof_type: src_type, + data_hash: src.data_hash, + }) + }) + .collect() } } } - /// Cross-party: check all cached sources against the target (or the new - /// source against all cached targets). - fn check_cross_party_link(&self, link: &dyn CommitmentLink, new_proof_type: ProofType) { - let src_type = link.source_proof_type(); - let tgt_type = link.target_proof_type(); - - if new_proof_type != src_type && new_proof_type != tgt_type { - return; - } - - // Collect all entries matching the source proof type. - let sources: Vec<&VerifiedProofData> = self - .verified - .iter() - .filter(|((_, pt), _)| *pt == src_type) - .map(|(_, v)| v) - .collect(); - - // Collect all entries matching the target proof type. - let targets: Vec<&VerifiedProofData> = self - .verified - .iter() - .filter(|((_, pt), _)| *pt == tgt_type) - .map(|(_, v)| v) - .collect(); - - // For each (source, target) pair, check consistency. - for src in &sources { - let source_values = link.extract_source_values(&src.public_signals); - if source_values.is_empty() { + /// Post-ZK: evaluate links relevant to a newly arrived proof and emit + /// violations on mismatch. + fn check_links(&self, new_proof_type: ProofType, ec: &EventContext) { + for link in &self.links { + if new_proof_type != link.source_proof_type() + && new_proof_type != link.target_proof_type() + { continue; } - for tgt in &targets { - if !link.check_consistency(&source_values, &tgt.public_signals) { + for m in self.find_mismatches(link.as_ref()) { + // Defense-in-depth: skip entries with unresolved data_hash + // (should not happen now that pre-ZK caching uses real hashes, + // but guards against future regressions). + if m.data_hash == [0u8; 32] { warn!( - "[{}] Commitment mismatch for E3 {} — source party {} ({}) {:?} \ - not consistent with target party {} ({}) {:?}", + "[{}] Skipping mismatch with zero data_hash for party {} ({}) {:?}", link.name(), - self.e3_id, - src.party_id, - src.address, - src_type, - tgt.party_id, - tgt.address, - tgt_type, + m.party_id, + m.address, + m.proof_type, ); + continue; } + warn!( + "[{}] Commitment mismatch for E3 {} — party {} ({}) {:?}", + link.name(), + self.e3_id, + m.party_id, + m.address, + m.proof_type, + ); + self.emit_violation(m.party_id, m.address, m.proof_type, m.data_hash, ec); } } } + + /// Publish a [`CommitmentConsistencyViolation`] for the accusation pipeline. + fn emit_violation( + &self, + accused_party_id: u64, + accused_address: Address, + proof_type: ProofType, + data_hash: [u8; 32], + ec: &EventContext, + ) { + let violation = CommitmentConsistencyViolation { + e3_id: self.e3_id.clone(), + accused_party_id, + accused_address, + proof_type, + data_hash, + }; + if let Err(err) = self.bus.publish(violation, ec.clone()) { + error!("Failed to publish CommitmentConsistencyViolation: {err}"); + } + } } impl Actor for CommitmentConsistencyChecker { @@ -187,8 +220,14 @@ impl Handler for CommitmentConsistencyChecker { fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { let (msg, ec) = msg.into_components(); - if let EnclaveEventData::ProofVerificationPassed(data) = msg { - self.notify_sync(ctx, TypedEvent::new(data, ec)); + match msg { + EnclaveEventData::CommitmentConsistencyCheckRequested(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::ProofVerificationPassed(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + _ => (), } } } @@ -201,21 +240,88 @@ impl Handler> for CommitmentConsistencyCheck msg: TypedEvent, _ctx: &mut Self::Context, ) -> Self::Result { - let (data, _ec) = msg.into_components(); + let (data, ec) = msg.into_components(); let proof_type = data.proof_type; let address = data.address; - let public_signals = data.public_signals; self.verified.insert( (address, proof_type), VerifiedProofData { party_id: data.party_id, address, - public_signals, + public_signals: data.public_signals, + data_hash: data.data_hash, }, ); - self.check_links(proof_type, address); + self.check_links(proof_type, &ec); + } +} + +impl Handler> for CommitmentConsistencyChecker { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + let (data, ec) = msg.into_components(); + + let mut inconsistent_parties = BTreeSet::new(); + + // Cache each party's proof data for link evaluation. + for party in &data.party_proofs { + for (proof_type, public_signals, data_hash) in &party.proofs { + self.verified.insert( + (party.address, *proof_type), + VerifiedProofData { + party_id: party.party_id, + address: party.address, + public_signals: public_signals.clone(), + data_hash: *data_hash, + }, + ); + } + } + + // Evaluate every link and collect inconsistent parties. + // Also emit violations so AccusationManager can initiate the quorum + // protocol — parties excluded pre-ZK would otherwise never trigger a + // post-ZK violation. + for link in &self.links { + for m in self.find_mismatches(link.as_ref()) { + warn!( + "[{}] Pre-ZK commitment mismatch for E3 {} — party {} ({})", + link.name(), + self.e3_id, + m.party_id, + m.address, + ); + inconsistent_parties.insert(m.party_id); + self.emit_violation(m.party_id, m.address, m.proof_type, m.data_hash, &ec); + } + } + + // Remove cached entries for inconsistent parties so they don't + // participate in future post-ZK `find_mismatches` evaluations. + if !inconsistent_parties.is_empty() { + self.verified + .retain(|_, v| !inconsistent_parties.contains(&v.party_id)); + } + + // Respond to ShareVerificationActor. + if let Err(err) = self.bus.publish( + CommitmentConsistencyCheckComplete { + e3_id: data.e3_id, + kind: data.kind, + correlation_id: data.correlation_id, + inconsistent_parties, + }, + ec, + ) { + error!("Failed to publish CommitmentConsistencyCheckComplete: {err}"); + } } } diff --git a/crates/zk-prover/src/actors/share_verification.rs b/crates/zk-prover/src/actors/share_verification.rs index 38a24c9b83..f8f73e9b64 100644 --- a/crates/zk-prover/src/actors/share_verification.rs +++ b/crates/zk-prover/src/actors/share_verification.rs @@ -27,9 +27,10 @@ use actix::{Actor, Addr, Context, Handler}; use alloy::primitives::{keccak256, Address, Bytes}; use alloy::sol_types::SolValue; use e3_events::{ - BusHandle, ComputeRequest, ComputeRequestError, ComputeResponse, ComputeResponseKind, - CorrelationId, E3id, EnclaveEvent, EnclaveEventData, EventContext, EventPublisher, - EventSubscriber, EventType, PartyProofsToVerify, PartyShareDecryptionProofsToVerify, + BusHandle, CommitmentConsistencyCheckComplete, CommitmentConsistencyCheckRequested, + ComputeRequest, ComputeRequestError, ComputeResponse, ComputeResponseKind, CorrelationId, E3id, + EnclaveEvent, EnclaveEventData, EventContext, EventPublisher, EventSubscriber, EventType, + PartyProofData, PartyProofsToVerify, PartyShareDecryptionProofsToVerify, PartyVerificationResult, ProofType, ProofVerificationFailed, ProofVerificationPassed, Sequenced, ShareVerificationComplete, ShareVerificationDispatched, SignedProofFailed, SignedProofPayload, TypedEvent, VerificationKind, VerifyShareDecryptionProofsRequest, @@ -91,16 +92,73 @@ struct PendingVerification { party_public_signals: HashMap>, } -/// Actor that handles C2/C3/C4 share proof verification. +/// Pending consistency check — stored between ECDSA pass and ZK dispatch. /// -/// 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. +/// After ECDSA validation, the actor publishes +/// [`CommitmentConsistencyCheckRequested`] and waits for the checker's +/// response. This struct buffers the ECDSA results and the original party +/// proofs so that ZK verification can be dispatched once the consistency +/// check completes. +/// +/// Several fields overlap with [`PendingVerification`] (e3_id, kind, ec, +/// party_addresses, party_proof_hashes, party_public_signals). When the +/// consistency check completes, they are transferred to a new +/// `PendingVerification` entry for the ZK phase. +struct PendingConsistencyCheck { + e3_id: E3id, + kind: VerificationKind, + ec: EventContext, + /// Parties that failed ECDSA (dishonest before consistency runs). + ecdsa_dishonest: HashSet, + /// Pre-dishonest parties from the dispatch (missing/incomplete proofs). + pre_dishonest: BTreeSet, + /// Recovered address per ECDSA-passed party. + party_addresses: HashMap, + /// (proof_type, data_hash) per party — for ProofVerificationPassed after ZK. + party_proof_hashes: HashMap>, + /// (proof_type, public_signals) per party — for consistency & ZK. + party_public_signals: HashMap>, + /// Original ECDSA-passed share proofs for ZK dispatch. + /// Populated for ShareProofs / ThresholdDecryptionProofs / PkGenerationProofs. + ecdsa_passed_share_proofs: Vec, + /// Original ECDSA-passed decryption proofs for ZK dispatch. + /// Populated for DecryptionProofs. + ecdsa_passed_decryption_proofs: Vec, +} + +/// Filter out inconsistent parties and collect dispatched party IDs. +/// Returns `None` if all parties were filtered out (nothing to verify). +fn filter_consistent

( + proofs: Vec

, + inconsistent: &BTreeSet, + party_id_of: impl Fn(&P) -> u64, +) -> Option<(Vec

, HashSet)> { + let passed: Vec

= proofs + .into_iter() + .filter(|p| !inconsistent.contains(&party_id_of(p))) + .collect(); + if passed.is_empty() { + return None; + } + let ids = passed.iter().map(|p| party_id_of(p)).collect(); + Some((passed, ids)) +} + +/// Actor that handles C1/C2/C3/C4/C6 share proof verification. +/// +/// Three-stage pipeline: +/// 1. ECDSA validation (lightweight, done inline) +/// 2. Commitment consistency check (dispatched to per-E3 checker via event bus) +/// 3. 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. + /// Tracks pending ZK verifications by correlation ID. pending: HashMap, + /// Tracks pending consistency checks by correlation ID (between ECDSA and ZK). + pending_consistency: HashMap, } impl ShareVerificationActor { @@ -108,6 +166,7 @@ impl ShareVerificationActor { Self { bus: bus.clone(), pending: HashMap::new(), + pending_consistency: HashMap::new(), } } @@ -116,6 +175,10 @@ impl ShareVerificationActor { bus.subscribe(EventType::ShareVerificationDispatched, addr.clone().into()); bus.subscribe(EventType::ComputeResponse, addr.clone().into()); bus.subscribe(EventType::ComputeRequestError, addr.clone().into()); + bus.subscribe( + EventType::CommitmentConsistencyCheckComplete, + addr.clone().into(), + ); addr } @@ -142,14 +205,8 @@ impl ShareVerificationActor { msg.share_proofs, msg.pre_dishonest, ec, - |passed, corr_id, e3| { - ComputeRequest::zk( - ZkRequest::VerifyShareProofs(VerifyShareProofsRequest { - party_proofs: passed, - }), - corr_id, - e3, - ) + |pending, passed| { + pending.ecdsa_passed_share_proofs = passed; }, ); } @@ -160,24 +217,19 @@ impl ShareVerificationActor { msg.decryption_proofs, msg.pre_dishonest, ec, - |passed, corr_id, e3| { - ComputeRequest::zk( - ZkRequest::VerifyShareDecryptionProofs( - VerifyShareDecryptionProofsRequest { - party_proofs: passed, - }, - ), - corr_id, - e3, - ) + |pending, passed| { + pending.ecdsa_passed_decryption_proofs = passed; }, ); } } } - /// Generic ECDSA + ZK verification: validates signed proofs for each party, - /// then dispatches ZK verification for ECDSA-passed parties. + /// Generic ECDSA validation + consistency check dispatch. + /// + /// After ECDSA validation, publishes [`CommitmentConsistencyCheckRequested`] + /// and stores a [`PendingConsistencyCheck`]. ZK verification is deferred + /// until the consistency check response arrives. fn verify_proofs( &mut self, e3_id: E3id, @@ -185,7 +237,7 @@ impl ShareVerificationActor { party_proofs: Vec

, pre_dishonest: BTreeSet, ec: EventContext, - build_request: impl FnOnce(Vec

, CorrelationId, E3id) -> ComputeRequest, + store_passed_proofs: impl FnOnce(&mut PendingConsistencyCheck, Vec

), ) { let e3_id_str = e3_id.to_string(); let label = match &kind { @@ -232,12 +284,7 @@ impl ShareVerificationActor { return; } - // Dispatch ZK-only verification to multithread - let correlation_id = CorrelationId::new(); - let dispatched_party_ids: HashSet = - ecdsa_passed_parties.iter().map(|p| p.party_id()).collect(); - - // Compute proof hashes for ECDSA-passed parties (for ProofVerificationPassed on success) + // Compute proof hashes and public signals for ECDSA-passed parties let mut party_proof_hashes: HashMap> = HashMap::new(); let mut party_public_signals: HashMap> = HashMap::new(); for party in &ecdsa_passed_parties { @@ -267,31 +314,220 @@ impl ShareVerificationActor { party_public_signals.insert(party.party_id(), signals); } - self.pending.insert( - correlation_id, - PendingVerification { + // Build consistency check request + let correlation_id = CorrelationId::new(); + let party_proof_data: Vec = ecdsa_passed_parties + .iter() + .map(|party| { + let signals = party_public_signals + .get(&party.party_id()) + .cloned() + .unwrap_or_default(); + let hashes = party_proof_hashes + .get(&party.party_id()) + .cloned() + .unwrap_or_default(); + let proofs = signals + .into_iter() + .zip(hashes) + .map(|((pt, ps), (_, dh))| (pt, ps, dh)) + .collect(); + PartyProofData { + party_id: party.party_id(), + address: party_addresses + .get(&party.party_id()) + .copied() + .unwrap_or_default(), + proofs, + } + }) + .collect(); + + // Store pending consistency check with the original party proofs + let mut pending = PendingConsistencyCheck { + e3_id: e3_id.clone(), + kind: kind.clone(), + ec: ec.clone(), + ecdsa_dishonest, + pre_dishonest, + party_addresses, + party_proof_hashes, + party_public_signals, + ecdsa_passed_share_proofs: Vec::new(), + ecdsa_passed_decryption_proofs: Vec::new(), + }; + store_passed_proofs(&mut pending, ecdsa_passed_parties); + self.pending_consistency.insert(correlation_id, pending); + + // Publish consistency check request + if let Err(err) = self.bus.publish( + CommitmentConsistencyCheckRequested { e3_id: e3_id.clone(), kind: kind.clone(), - ec: ec.clone(), - ecdsa_dishonest, - pre_dishonest, - dispatched_party_ids, + correlation_id, + party_proofs: party_proof_data, + }, + ec.clone(), + ) { + error!( + "Failed to dispatch {} consistency check: {err} — treating all as dishonest", + label + ); + if let Some(pending) = self.pending_consistency.remove(&correlation_id) { + let mut all_dishonest: BTreeSet = pending.pre_dishonest; + all_dishonest.extend(pending.ecdsa_dishonest); + for p in &pending.ecdsa_passed_share_proofs { + all_dishonest.insert(p.sender_party_id); + } + for p in &pending.ecdsa_passed_decryption_proofs { + all_dishonest.insert(p.sender_party_id); + } + self.publish_complete(e3_id, kind, all_dishonest, ec); + } + } + } + + /// Handle consistency check response: add inconsistent parties to the + /// dishonest set, then dispatch ZK verification for the remaining + /// consistent parties. + fn handle_consistency_check_complete( + &mut self, + msg: TypedEvent, + ) { + let (data, _ec) = msg.into_components(); + + let Some(pending) = self.pending_consistency.remove(&data.correlation_id) else { + return; // Not our correlation ID + }; + + let label = match &pending.kind { + VerificationKind::ShareProofs => "C2/C3", + VerificationKind::ThresholdDecryptionProofs => "C6", + VerificationKind::PkGenerationProofs => "C1", + VerificationKind::DecryptionProofs => "C4", + }; + + if !data.inconsistent_parties.is_empty() { + warn!( + "{} consistency check found {} inconsistent parties for E3 {}: {:?}", + label, + data.inconsistent_parties.len(), + pending.e3_id, + data.inconsistent_parties + ); + } + + // Accumulate all dishonest parties discovered so far + let mut dishonest_so_far: BTreeSet = pending.pre_dishonest.clone(); + dishonest_so_far.extend(&pending.ecdsa_dishonest); + dishonest_so_far.extend(&data.inconsistent_parties); + + // Filter ECDSA-passed proofs to only consistent parties and dispatch ZK + let inconsistent = &data.inconsistent_parties; + let zk_correlation_id = CorrelationId::new(); + + let (request, dispatched_party_ids) = match pending.kind { + VerificationKind::ShareProofs + | VerificationKind::ThresholdDecryptionProofs + | VerificationKind::PkGenerationProofs => { + let Some((passed, ids)) = + filter_consistent(pending.ecdsa_passed_share_proofs, inconsistent, |p| { + p.sender_party_id + }) + else { + self.publish_complete( + pending.e3_id, + pending.kind, + dishonest_so_far, + pending.ec, + ); + return; + }; + let req = ComputeRequest::zk( + ZkRequest::VerifyShareProofs(VerifyShareProofsRequest { + party_proofs: passed, + }), + zk_correlation_id, + pending.e3_id.clone(), + ); + (req, ids) + } + VerificationKind::DecryptionProofs => { + let Some((passed, ids)) = + filter_consistent(pending.ecdsa_passed_decryption_proofs, inconsistent, |p| { + p.sender_party_id + }) + else { + self.publish_complete( + pending.e3_id, + pending.kind, + dishonest_so_far, + pending.ec, + ); + return; + }; + let req = ComputeRequest::zk( + ZkRequest::VerifyShareDecryptionProofs(VerifyShareDecryptionProofsRequest { + party_proofs: passed, + }), + zk_correlation_id, + pending.e3_id.clone(), + ); + (req, ids) + } + }; + + // Only keep proof hashes/signals/addresses for parties going to ZK + let party_addresses: HashMap = pending + .party_addresses + .into_iter() + .filter(|(pid, _)| dispatched_party_ids.contains(pid)) + .collect(); + let party_proof_hashes: HashMap> = pending + .party_proof_hashes + .into_iter() + .filter(|(pid, _)| dispatched_party_ids.contains(pid)) + .collect(); + let party_public_signals: HashMap> = pending + .party_public_signals + .into_iter() + .filter(|(pid, _)| dispatched_party_ids.contains(pid)) + .collect(); + + // Store pending ZK verification state. + // All prior dishonest parties (pre_dishonest + ECDSA + consistency) are + // folded into `pre_dishonest` so that `handle_compute_response` produces + // the correct final dishonest set when it adds ZK failures. + self.pending.insert( + zk_correlation_id, + PendingVerification { + e3_id: pending.e3_id.clone(), + kind: pending.kind.clone(), + ec: pending.ec.clone(), + ecdsa_dishonest: HashSet::new(), + pre_dishonest: dishonest_so_far, + dispatched_party_ids: dispatched_party_ids.clone(), party_addresses, party_proof_hashes, party_public_signals, }, ); - let request = build_request(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}", label); - if let Some(pending) = self.pending.remove(&correlation_id) { - let mut all_dishonest: BTreeSet = pending.pre_dishonest; - all_dishonest.extend(pending.ecdsa_dishonest); - // Dispatched parties were never ZK-verified — treat as dishonest - all_dishonest.extend(pending.dispatched_party_ids); - self.publish_complete(e3_id, kind, all_dishonest, ec); + if let Err(err) = self.bus.publish(request, pending.ec.clone()) { + error!( + "Failed to dispatch {} ZK verification after consistency check: {err}", + label + ); + if let Some(zk_pending) = self.pending.remove(&zk_correlation_id) { + let mut all_dishonest: BTreeSet = zk_pending.pre_dishonest; + all_dishonest.extend(zk_pending.ecdsa_dishonest); + all_dishonest.extend(zk_pending.dispatched_party_ids); + self.publish_complete( + zk_pending.e3_id, + zk_pending.kind, + all_dishonest, + zk_pending.ec, + ); } } } @@ -603,6 +839,9 @@ impl Handler for ShareVerificationActor { EnclaveEventData::ComputeRequestError(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::CommitmentConsistencyCheckComplete(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } _ => (), } } @@ -643,3 +882,15 @@ impl Handler> for ShareVerificationActor { self.handle_compute_request_error(msg) } } + +impl Handler> for ShareVerificationActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_consistency_check_complete(msg) + } +}