Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions agent/flow-trace/00_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 24 additions & 6 deletions agent/flow-trace/04_DKG_AND_COMPUTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
82 changes: 46 additions & 36 deletions agent/flow-trace/05_FAILURE_REFUND_SLASHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
83 changes: 83 additions & 0 deletions crates/events/src/enclave_event/commitment_consistency.rs
Original file line number Diff line number Diff line change
@@ -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<PartyProofData>,
}

/// 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<u64>,
}

/// 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],
}
17 changes: 16 additions & 1 deletion crates/events/src/enclave_event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::*;
Expand Down Expand Up @@ -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),
}
Expand Down Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -662,7 +674,10 @@ impl_event_types!(
AggregationProofPending,
AggregationProofSigned,
DKGInnerProofReady,
DKGRecursiveAggregationComplete
DKGRecursiveAggregationComplete,
CommitmentConsistencyCheckRequested,
CommitmentConsistencyCheckComplete,
CommitmentConsistencyViolation
);

impl TryFrom<&EnclaveEvent<Sequenced>> for EnclaveError {
Expand Down
20 changes: 18 additions & 2 deletions crates/tests/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand Down
Loading
Loading