diff --git a/Cargo.lock b/Cargo.lock index 93a9a02ee5..b42b1c3a9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3712,6 +3712,7 @@ dependencies = [ "e3-test-helpers", "e3-trbfv", "e3-utils", + "e3-zk-prover", "fhe", "fhe-traits", "fhe-util", @@ -3719,6 +3720,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "serial_test", + "tempfile", "tokio", "tracing", "tracing-subscriber", @@ -3817,6 +3819,7 @@ dependencies = [ "acir", "actix", "acvm", + "alloy", "anyhow", "async-trait", "base64", diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 3503a07446..4c0dc5ecd7 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -7,6 +7,7 @@ use crate::event_system::AggregateConfig; use crate::{CiphernodeHandle, EventSystem, EvmSystemChainBuilder, ProviderCache, WriteEnabled}; use actix::{Actor, Addr}; +use alloy::signers::local::PrivateKeySigner; use anyhow::Result; use derivative::Derivative; use e3_aggregator::ext::{PublicKeyAggregatorExtension, ThresholdPlaintextAggregatorExtension}; @@ -67,6 +68,7 @@ pub struct CiphernodeBuilder { testmode_history: bool, task_pool: Option, threads: Option, + testmode_signer: Option, threshold_plaintext_agg: bool, zk_backend: Option, net_config: Option, @@ -132,6 +134,7 @@ impl CiphernodeBuilder { testmode_history: false, task_pool: None, threads: None, + testmode_signer: None, threshold_plaintext_agg: false, net_config: None, zk_backend: None, @@ -261,6 +264,13 @@ impl CiphernodeBuilder { self } + /// Pre-populate the signer cache with the given signer. + /// This is conspicuously named so we understand that this should only be used when testing. + pub fn testmode_with_signer(mut self, signer: PrivateKeySigner) -> Self { + self.testmode_signer = Some(signer); + self + } + /// Use score-based sortition (recommended) pub fn with_sortition_score(mut self) -> Self { self.sortition_backend = SortitionBackend::score(); @@ -358,7 +368,11 @@ impl CiphernodeBuilder { }; // Create provider cache early to use for chain validation - let mut provider_cache = ProviderCache::new(); + let mut provider_cache = if let Some(signer) = self.testmode_signer.take() { + ProviderCache::new().with_signer(signer) + } else { + ProviderCache::new() + }; let aggregate_config = self.create_aggregate_config(&mut provider_cache).await?; // Get an event system instance. @@ -434,8 +448,13 @@ 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"); - setup_zk_actors(&bus, self.zk_backend.as_ref()); + let signer = provider_cache.ensure_signer().await?; + setup_zk_actors(&bus, backend, signer); } if self.pubkey_agg { diff --git a/crates/ciphernode-builder/src/provider_caches.rs b/crates/ciphernode-builder/src/provider_caches.rs index 628a7e525a..754069c3a9 100644 --- a/crates/ciphernode-builder/src/provider_caches.rs +++ b/crates/ciphernode-builder/src/provider_caches.rs @@ -44,6 +44,11 @@ impl ProviderCache { } } + pub fn with_signer(mut self, signer: LocalSigner) -> Self { + self.signer_cache = Some(signer); + self + } + pub fn from_single_read_provider( chain: ChainConfig, provider: EthProvider, diff --git a/crates/events/src/enclave_event/encryption_key_created.rs b/crates/events/src/enclave_event/encryption_key_created.rs index 60a99e2916..08793841b0 100644 --- a/crates/events/src/enclave_event/encryption_key_created.rs +++ b/crates/events/src/enclave_event/encryption_key_created.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::{E3id, Proof}; +use crate::{E3id, Proof, SignedProofPayload}; use actix::Message; use derivative::Derivative; use e3_utils::utility_types::ArcBytes; @@ -23,6 +23,9 @@ pub struct EncryptionKey { pub pk_bfv: ArcBytes, /// Proof of correct BFV public key generation (T0 proof). pub proof: Option, + /// ECDSA-signed payload for fault attribution. + /// Present when the node signs its proof before broadcasting. + pub signed_payload: Option, } impl EncryptionKey { @@ -31,6 +34,7 @@ impl EncryptionKey { party_id, pk_bfv: pk_bfv.into(), proof: None, + signed_payload: None, } } @@ -38,6 +42,11 @@ impl EncryptionKey { self.proof = Some(proof); self } + + pub fn with_signed_payload(mut self, signed: SignedProofPayload) -> Self { + self.signed_payload = Some(signed); + self + } } #[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 2256a8e979..e8d0ff7d33 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -36,6 +36,7 @@ mod proof; mod publickey_aggregated; mod publish_document; mod shutdown; +mod signed_proof; mod sync_effect; mod sync_end; mod sync_start; @@ -80,6 +81,7 @@ pub use proof::*; pub use publickey_aggregated::*; pub use publish_document::*; pub use shutdown::*; +pub use signed_proof::*; use strum::IntoStaticStr; pub use sync_effect::*; pub use sync_end::*; @@ -223,6 +225,7 @@ pub enum EnclaveEventData { ComputeRequest(ComputeRequest), // ComputeRequested ComputeResponse(ComputeResponse), // ComputeResponseReceived ComputeRequestError(ComputeRequestError), // ComputeRequestFailed + SignedProofFailed(SignedProofFailed), OutgoingSyncRequested(OutgoingSyncRequested), NetSyncEventsReceived(NetSyncEventsReceived), EvmSyncEventsReceived(EvmSyncEventsReceived), @@ -438,6 +441,7 @@ impl EnclaveEventData { EnclaveEventData::TicketSubmitted(ref data) => Some(data.e3_id.clone()), EnclaveEventData::EncryptionKeyCreated(ref data) => Some(data.e3_id.clone()), EnclaveEventData::ComputeResponse(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::SignedProofFailed(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, @@ -505,6 +509,7 @@ impl_event_types!( ComputeRequest, ComputeResponse, ComputeRequestError, + SignedProofFailed, OutgoingSyncRequested, NetSyncEventsReceived, EvmSyncEventsReceived, diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs new file mode 100644 index 0000000000..690805c1ad --- /dev/null +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -0,0 +1,315 @@ +// 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. + +//! Signed proof payload types for fault attribution. +//! +//! Every ZK proof a node broadcasts is wrapped in a [`SignedProofPayload`] — the node's +//! ECDSA signature over the canonical encoding of the data + proof. If the proof later +//! fails verification, the signed bundle is self-authenticating evidence of fault: +//! the signature proves authorship and the proof bytes prove invalidity. + +use crate::{CircuitName, E3id, Proof}; +use actix::Message; +use alloy::primitives::{keccak256, Address, Bytes, Signature, U256}; +use alloy::signers::{local::PrivateKeySigner, SignerSync}; +use alloy::sol_types::SolValue; +use anyhow::{anyhow, Result}; +use derivative::Derivative; +use e3_utils::utility_types::ArcBytes; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +/// Proof type identifier covering all node-generated proofs. +/// +/// Aggregation proofs (Proofs 5 and 7) are excluded — they are published on-chain +/// directly and verified by the contract at submission time. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ProofType { + /// T0 — BFV public key proof (Proof 0). + T0PkBfv = 0, + /// T1 — TrBFV public key generation proof (Proof 1). + T1PkGeneration = 1, + /// T1 — Secret key share computation proof (Proof 2a). + T1SkShareComputation = 2, + /// T1 — Smudging noise share computation proof (Proof 2b). + T1ESmShareComputation = 3, + /// T1 — Secret key share encryption proof (Proof 3a). + T1SkShareEncryption = 4, + /// T1 — Smudging noise share encryption proof (Proof 3b). + T1ESmShareEncryption = 5, + /// T2 — Secret key share decryption proof (Proof 4a). + T2SkShareDecryption = 6, + /// T2 — Smudging noise share decryption proof (Proof 4b). + T2ESmShareDecryption = 7, + /// T5 — Share decryption proof (Proof 6). + T5ShareDecryption = 8, +} + +impl ProofType { + /// Map this proof type to its corresponding circuit name. + pub fn circuit_name(&self) -> CircuitName { + match self { + ProofType::T0PkBfv => CircuitName::PkBfv, + ProofType::T1PkGeneration => CircuitName::PkGeneration, + ProofType::T1SkShareComputation + | ProofType::T1ESmShareComputation + | ProofType::T1SkShareEncryption + | ProofType::T1ESmShareEncryption => CircuitName::EncShares, + ProofType::T2SkShareDecryption | ProofType::T2ESmShareDecryption => { + CircuitName::DecShares + } + ProofType::T5ShareDecryption => CircuitName::DecShares, + } + } + + /// Slash reason identifier for on-chain policies. + pub fn slash_reason(&self) -> &'static str { + match self { + ProofType::T0PkBfv + | ProofType::T1PkGeneration + | ProofType::T1SkShareComputation + | ProofType::T1ESmShareComputation + | ProofType::T1SkShareEncryption + | ProofType::T1ESmShareEncryption + | ProofType::T2SkShareDecryption + | ProofType::T2ESmShareDecryption => "E3_BAD_DKG_PROOF", + ProofType::T5ShareDecryption => "E3_BAD_DECRYPTION_PROOF", + } + } +} + +impl fmt::Display for ProofType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +/// Data payload that a node signs before broadcasting. +/// +/// Only contains data needed for on-chain fault verification: +/// the E3 identifier, proof type, and the ZK proof itself. +/// Encoded via `abi.encode(chainId, e3Id, proofType, proof, publicSignals)` +/// so on-chain `ecrecover` can reconstruct the same digest. +#[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derivative(Debug)] +pub struct ProofPayload { + /// E3 computation identifier. + pub e3_id: E3id, + /// Which proof this payload carries. + pub proof_type: ProofType, + /// The ZK proof that attests to the data. + pub proof: Proof, +} + +impl ProofPayload { + /// Compute the keccak256 digest of the canonical encoding. + /// + /// Uses standard ABI encoding (`abi.encode`) which includes offsets and + /// lengths for dynamic types, avoiding collision between the two + /// variable-length `Bytes` fields (`proof` and `publicSignals`). + pub fn digest(&self) -> Result<[u8; 32]> { + let e3_id_u256: U256 = self + .e3_id + .clone() + .try_into() + .map_err(|_| anyhow!("E3id cannot be converted to U256"))?; + + // keccak256(abi.encode(chainId, e3Id, proofType, proof, publicSignals)) + let encoded = ( + U256::from(self.e3_id.chain_id()), + e3_id_u256, + U256::from(self.proof_type as u8), + Bytes::copy_from_slice(&self.proof.data), + Bytes::copy_from_slice(&self.proof.public_signals), + ) + .abi_encode(); + + Ok(keccak256(&encoded).into()) + } +} + +/// Signed wrapper around a [`ProofPayload`]. +/// +/// This is the unit of data broadcast over the p2p network. The signature +/// is an Ethereum-style `eth_sign` (EIP-191 personal message) over the +/// keccak256 digest of the payload's canonical encoding. +#[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derivative(Debug)] +pub struct SignedProofPayload { + /// The payload that was signed. + pub payload: ProofPayload, + /// 65-byte ECDSA signature (r ‖ s ‖ v) computed via `eth_sign`. + #[derivative(Debug(format_with = "e3_utils::formatters::hexf"))] + pub signature: ArcBytes, +} + +impl SignedProofPayload { + /// Sign a [`ProofPayload`] with the node's ECDSA key. + pub fn sign(payload: ProofPayload, signer: &PrivateKeySigner) -> Result { + let digest = payload.digest()?; + let sig = signer + .sign_message_sync(&digest) + .map_err(|e| anyhow!("Failed to sign proof payload: {e}"))?; + + Ok(Self { + payload, + signature: ArcBytes::from_bytes(&sig.as_bytes()), + }) + } + + /// Recover the Ethereum address that produced this signature. + pub fn recover_address(&self) -> Result
{ + let sig = Signature::try_from(&self.signature[..]) + .map_err(|e| anyhow!("Invalid signature: {e}"))?; + + let digest = self.payload.digest()?; + sig.recover_address_from_msg(&digest) + .map_err(|e| anyhow!("Failed to recover address: {e}")) + } + + /// Verify that the recovered address matches the expected address. + pub fn verify_address(&self, expected: &Address) -> Result { + let recovered = self.recover_address()?; + Ok(recovered == *expected) + } +} + +/// Emitted when a node detects a signed proof that fails ZK verification. +/// +/// This event carries the complete evidence bundle: the bad proof bytes, +/// the public signals, and the faulting node's signature. The +/// [`FaultSubmitter`] actor consumes this to submit a slash proposal +/// on-chain. +#[derive(Message, Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +#[derivative(Debug)] +pub struct SignedProofFailed { + /// E3 computation identifier. + pub e3_id: E3id, + /// Ethereum address of the faulting node (recovered from signature). + pub faulting_node: Address, + /// Which proof type failed. + pub proof_type: ProofType, + /// The full signed payload — self-authenticating evidence. + pub signed_payload: SignedProofPayload, +} + +impl Display for SignedProofFailed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SignedProofFailed {{ e3_id: {}, faulting_node: {}, proof_type: {} }}", + self.e3_id, self.faulting_node, self.proof_type + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_signer() -> PrivateKeySigner { + // Deterministic test key + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + .parse() + .unwrap() + } + + fn test_payload() -> ProofPayload { + ProofPayload { + e3_id: E3id::new("1", 42), + proof_type: ProofType::T0PkBfv, + proof: Proof::new( + CircuitName::PkBfv, + ArcBytes::from_bytes(&[10, 20, 30]), + ArcBytes::from_bytes(&[100, 200]), + ), + } + } + + #[test] + fn sign_and_recover_roundtrip() { + let signer = test_signer(); + let payload = test_payload(); + + let signed = + SignedProofPayload::sign(payload.clone(), &signer).expect("signing should succeed"); + + let recovered = signed.recover_address().expect("recovery should succeed"); + assert_eq!(recovered, signer.address()); + } + + #[test] + fn verify_address_correct() { + let signer = test_signer(); + let payload = test_payload(); + + let signed = SignedProofPayload::sign(payload, &signer).expect("signing should succeed"); + assert!(signed + .verify_address(&signer.address()) + .expect("verify should succeed")); + } + + #[test] + fn verify_address_wrong() { + let signer = test_signer(); + let payload = test_payload(); + + let signed = SignedProofPayload::sign(payload, &signer).expect("signing should succeed"); + + let wrong_addr: Address = "0x0000000000000000000000000000000000000001" + .parse() + .unwrap(); + assert!(!signed + .verify_address(&wrong_addr) + .expect("verify should succeed")); + } + + #[test] + fn different_payloads_produce_different_digests() { + let p1 = test_payload(); + let mut p2 = test_payload(); + p2.proof_type = ProofType::T1PkGeneration; + + assert_ne!( + p1.digest().expect("digest should succeed"), + p2.digest().expect("digest should succeed") + ); + } + + #[test] + fn tampered_payload_fails_recovery() { + let signer = test_signer(); + let payload = test_payload(); + + let mut signed = + SignedProofPayload::sign(payload, &signer).expect("signing should succeed"); + // Tamper with the payload after signing + signed.payload.proof_type = ProofType::T1PkGeneration; + + let recovered = signed.recover_address().expect("recovery should succeed"); + // Recovered address won't match the signer because payload was tampered + assert_ne!(recovered, signer.address()); + } + + #[test] + fn proof_type_circuit_name_mapping() { + assert_eq!(ProofType::T0PkBfv.circuit_name(), CircuitName::PkBfv); + assert_eq!( + ProofType::T1PkGeneration.circuit_name(), + CircuitName::PkGeneration + ); + assert_eq!( + ProofType::T1SkShareEncryption.circuit_name(), + CircuitName::EncShares + ); + assert_eq!( + ProofType::T2SkShareDecryption.circuit_name(), + CircuitName::DecShares + ); + } +} diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml index 2ebbf31e48..2147af77b2 100644 --- a/crates/tests/Cargo.toml +++ b/crates/tests/Cargo.toml @@ -34,6 +34,7 @@ e3-trbfv = { workspace = true } e3-bfv-client = { workspace = true } e3-fhe-params = { workspace = true } e3-utils = { workspace = true } +e3-zk-prover = { workspace = true } fhe-traits = { workspace = true } fhe-util = { workspace = true } fhe = { workspace = true } @@ -41,6 +42,7 @@ num-bigint = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } serial_test = { workspace = true } +tempfile = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 7813a55428..cfdd240155 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -6,6 +6,7 @@ use actix::Actor; use alloy::primitives::{Address, FixedBytes, I256, U256}; +use alloy::signers::local::PrivateKeySigner; use anyhow::{bail, Result}; use e3_bfv_client::decode_bytes_to_vec_u64; use e3_ciphernode_builder::{CiphernodeBuilder, EventSystem}; @@ -26,18 +27,98 @@ use e3_test_helpers::{create_seed_from_u64, create_shared_rng_from_u64, AddToCom use e3_trbfv::helpers::calculate_error_size; use e3_utils::rand_eth_addr; use e3_utils::utility_types::ArcBytes; +use e3_zk_prover::{ZkBackend, ZkConfig}; use fhe::bfv::PublicKey; use fhe_traits::{DeserializeParametrized, Serialize}; use num_bigint::BigUint; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; use std::time::{Duration, Instant}; -use std::{fs, sync::Arc}; +use std::{fs, path::PathBuf, sync::Arc}; use tokio::{ sync::{broadcast, mpsc}, time::sleep, }; +/// Find the bb binary on the system. +async fn find_bb() -> Option { + if let Ok(output) = tokio::process::Command::new("which") + .arg("bb") + .output() + .await + { + if output.status.success() { + let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !path.is_empty() { + return Some(PathBuf::from(path)); + } + } + } + if let Ok(home) = std::env::var("HOME") { + for path in [ + format!("{}/.bb/bb", home), + format!("{}/.nargo/bin/bb", home), + format!("{}/.enclave/noir/bin/bb", home), + ] { + if std::path::Path::new(&path).exists() { + return Some(PathBuf::from(path)); + } + } + } + None +} + +/// Create a ZkBackend for integration tests. +/// If a local bb binary is found, uses it with fixture files (fast path). +/// Otherwise, calls `ensure_installed()` to download bb + circuits (CI path). +async fn setup_test_zk_backend() -> (ZkBackend, tempfile::TempDir) { + let temp = tempfile::tempdir().unwrap(); + let temp_path = temp.path(); + let noir_dir = temp_path.join("noir"); + let bb_binary = noir_dir.join("bin").join("bb"); + let circuits_dir = noir_dir.join("circuits"); + let work_dir = noir_dir.join("work").join("test_node"); + + if let Some(bb) = find_bb().await { + tokio::fs::create_dir_all(bb_binary.parent().unwrap()) + .await + .unwrap(); + tokio::fs::create_dir_all(&circuits_dir).await.unwrap(); + tokio::fs::create_dir_all(&work_dir).await.unwrap(); + + #[cfg(unix)] + std::os::unix::fs::symlink(&bb, &bb_binary).unwrap(); + #[cfg(not(unix))] + compile_error!("Integration tests require unix symlink support"); + + // Copy circuit fixtures from the zk-prover crate's test fixtures + let fixtures_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("zk-prover") + .join("tests") + .join("fixtures"); + let pk_circuit_dir = circuits_dir.join("dkg").join("pk"); + tokio::fs::create_dir_all(&pk_circuit_dir).await.unwrap(); + tokio::fs::copy(fixtures_dir.join("pk.json"), pk_circuit_dir.join("pk.json")) + .await + .unwrap(); + tokio::fs::copy(fixtures_dir.join("pk.vk"), pk_circuit_dir.join("pk.vk")) + .await + .unwrap(); + + let backend = ZkBackend::new(bb_binary, circuits_dir, work_dir, ZkConfig::default()); + (backend, temp) + } else { + println!("bb binary not found locally, downloading via ensure_installed()..."); + let backend = ZkBackend::new(bb_binary, circuits_dir, work_dir, ZkConfig::default()); + backend + .ensure_installed() + .await + .expect("Failed to download and install ZK backend"); + (backend, temp) + } +} + pub fn save_snapshot(file_name: &str, bytes: &[u8]) { println!("### WRITING SNAPSHOT TO `{file_name}` ###"); fs::write(format!("tests/{file_name}"), bytes).unwrap(); @@ -246,6 +327,9 @@ async fn test_trbfv_actor() -> Result<()> { let task_pool = Multithread::create_taskpool(max_threadroom, concurrent_jobs); let multithread_report = MultithreadReport::new(max_threadroom, concurrent_jobs).start(); + // Setup ZK backend for proof generation/verification + let (zk_backend, _zk_temp) = setup_test_zk_backend().await; + let nodes = CiphernodeSystemBuilder::new() // Adding 20 total nodes: 5 for committee + 4 buffer = 9 selected, 11 unselected .add_group(1, || async { @@ -258,6 +342,8 @@ async fn test_trbfv_actor() -> Result<()> { .with_multithread_concurrent_jobs(concurrent_jobs) .with_shared_multithread_report(&multithread_report) .with_trbfv() + .with_zkproof(zk_backend.clone()) + .testmode_with_signer(PrivateKeySigner::random()) .with_pubkey_aggregation() .with_sortition_score() .with_threshold_plaintext_aggregation() @@ -275,6 +361,8 @@ async fn test_trbfv_actor() -> Result<()> { .with_multithread_concurrent_jobs(concurrent_jobs) .with_shared_multithread_report(&multithread_report) .with_trbfv() + .with_zkproof(zk_backend.clone()) + .testmode_with_signer(PrivateKeySigner::random()) .with_sortition_score() .testmode_with_forked_bus(bus.event_bus()) .with_logging() diff --git a/crates/zk-prover/Cargo.toml b/crates/zk-prover/Cargo.toml index 3628916fd5..58356283f1 100644 --- a/crates/zk-prover/Cargo.toml +++ b/crates/zk-prover/Cargo.toml @@ -45,6 +45,7 @@ e3-request.workspace = true e3-events.workspace = true e3-data.workspace = true e3-utils.workspace = true +alloy = { workspace = true } [dev-dependencies] paste = "1" diff --git a/crates/zk-prover/src/actors/mod.rs b/crates/zk-prover/src/actors/mod.rs index e3d8ba210e..4a589ac764 100644 --- a/crates/zk-prover/src/actors/mod.rs +++ b/crates/zk-prover/src/actors/mod.rs @@ -22,12 +22,14 @@ //! ```rust,ignore //! use e3_zk_prover::{ZkBackend, setup_zk_actors}; //! use e3_events::BusHandle; +//! use alloy::signers::local::PrivateKeySigner; //! //! let bus = BusHandle::default(); //! let backend = ZkBackend::with_default_dir().await?; +//! let signer = PrivateKeySigner::random(); //! //! // Setup all actors with proper separation of concerns -//! setup_zk_actors(&bus, Some(&backend)); +//! setup_zk_actors(&bus, &backend, signer); //! ``` pub mod proof_request; @@ -41,29 +43,20 @@ pub use proof_verification::{ pub use zk_actor::ZkActor; use actix::{Actor, Addr}; +use alloy::signers::local::PrivateKeySigner; use e3_events::BusHandle; use crate::ZkBackend; /// Setup all ZK-related actors with proper separation of concerns. /// -/// When `backend` is provided: -/// - Creates IO actor (ZkActor) for proof generation/verification -/// - Creates core actors that delegate to IO actor -/// -/// When `backend` is None: -/// - Creates core actors without verification capabilities -/// - Proofs are disabled, keys are accepted without verification -pub fn setup_zk_actors(bus: &BusHandle, backend: Option<&ZkBackend>) -> ZkActors { - let (zk_actor, verifier) = if let Some(backend) = backend { - let zk_actor = ZkActor::new(backend).start(); - let verifier = Some(zk_actor.clone().recipient()); - (Some(zk_actor), verifier) - } else { - (None, None) - }; +/// Requires a `ZkBackend` for proof generation/verification and a +/// `PrivateKeySigner` for signing proofs (fault attribution). +pub fn setup_zk_actors(bus: &BusHandle, backend: &ZkBackend, signer: PrivateKeySigner) -> ZkActors { + let zk_actor = ZkActor::new(backend).start(); + let verifier = zk_actor.clone().recipient(); - let proof_request = ProofRequestActor::setup(bus, backend.is_some()); + let proof_request = ProofRequestActor::setup(bus, signer); let proof_verification = ProofVerificationActor::setup(bus, verifier); ZkActors { @@ -75,7 +68,7 @@ pub fn setup_zk_actors(bus: &BusHandle, backend: Option<&ZkBackend>) -> ZkActors /// Container for all ZK-related actor addresses. pub struct ZkActors { - pub zk_actor: Option>, + pub zk_actor: Addr, pub proof_request: Addr, pub proof_verification: Addr, } diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index 9896f5297a..2a7ffe7408 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -1,172 +1,177 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. - -use std::collections::HashMap; -use std::sync::Arc; - -use actix::{Actor, Addr, Context, Handler}; -use e3_events::{ - BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeResponse, - ComputeResponseKind, CorrelationId, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, - EncryptionKeyCreated, EncryptionKeyPending, Event, EventPublisher, EventSubscriber, EventType, - PkBfvProofRequest, ZkRequest, ZkResponse, -}; -use e3_utils::NotifySync; -use tracing::{error, info, warn}; - -#[derive(Clone, Debug)] -struct PendingProofRequest { - e3_id: E3id, - key: Arc, -} - -/// Core actor that handles encryption key proof requests. -pub struct ProofRequestActor { - bus: BusHandle, - proofs_enabled: bool, - pending: HashMap, -} - -impl ProofRequestActor { - pub fn new(bus: &BusHandle, proofs_enabled: bool) -> Self { - Self { - bus: bus.clone(), - proofs_enabled, - pending: HashMap::new(), - } - } - - pub fn setup(bus: &BusHandle, proofs_enabled: bool) -> Addr { - let addr = Self::new(bus, proofs_enabled).start(); - bus.subscribe(EventType::EncryptionKeyPending, addr.clone().into()); - bus.subscribe(EventType::ComputeResponse, addr.clone().into()); - bus.subscribe(EventType::ComputeRequestError, addr.clone().into()); - addr - } - - fn handle_encryption_key_pending(&mut self, msg: EncryptionKeyPending) { - if !self.proofs_enabled { - info!( - "ZK proofs disabled; publishing EncryptionKeyCreated without proof for party {}", - msg.key.party_id - ); - if let Err(err) = self.bus.publish(EncryptionKeyCreated { - e3_id: msg.e3_id, - key: msg.key, - external: false, - }) { - error!("Failed to publish EncryptionKeyCreated: {err}"); - } - return; - } - - let correlation_id = CorrelationId::new(); - self.pending.insert( - correlation_id, - PendingProofRequest { - e3_id: msg.e3_id.clone(), - key: msg.key.clone(), - }, - ); - - let request = ComputeRequest::zk( - ZkRequest::PkBfv(PkBfvProofRequest::new( - msg.key.pk_bfv.clone(), - msg.params_preset, - )), - correlation_id, - msg.e3_id, - ); - - info!("Requesting T0 proof generation"); - if let Err(err) = self.bus.publish(request) { - error!("Failed to publish ZK proof request: {err}"); - self.pending.remove(&correlation_id); - } - } - - fn handle_compute_response(&mut self, msg: ComputeResponse) { - let ComputeResponseKind::Zk(ZkResponse::PkBfv(resp)) = msg.response else { - return; - }; - - let Some(pending) = self.pending.remove(&msg.correlation_id) else { - return; - }; - - let mut key = (*pending.key).clone(); - key.proof = Some(resp.proof); - - if let Err(err) = self.bus.publish(EncryptionKeyCreated { - e3_id: pending.e3_id, - key: Arc::new(key), - external: false, - }) { - error!("Failed to publish EncryptionKeyCreated: {err}"); - } - } - - fn handle_compute_request_error(&mut self, msg: ComputeRequestError) { - let ComputeRequestErrorKind::Zk(err) = msg.get_err() else { - return; - }; - - if let Some(pending) = self.pending.remove(msg.correlation_id()) { - warn!("ZK proof request failed for E3 {}: {err}", pending.e3_id); - - // Publish EncryptionKeyCreated without proof to allow the system to continue - // Applications can check the proof field to determine if validation occurred - if let Err(err) = self.bus.publish(EncryptionKeyCreated { - e3_id: pending.e3_id, - key: pending.key, - external: false, - }) { - error!("Failed to publish EncryptionKeyCreated after ZK proof failure: {err}"); - } - } - } -} - -impl Actor for ProofRequestActor { - type Context = Context; -} - -impl Handler for ProofRequestActor { - type Result = (); - - fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { - match msg.into_data() { - EnclaveEventData::EncryptionKeyPending(data) => self.notify_sync(ctx, data), - EnclaveEventData::ComputeResponse(data) => self.notify_sync(ctx, data), - EnclaveEventData::ComputeRequestError(data) => self.notify_sync(ctx, data), - _ => (), - } - } -} - -impl Handler for ProofRequestActor { - type Result = (); - - fn handle(&mut self, msg: EncryptionKeyPending, _ctx: &mut Self::Context) -> Self::Result { - self.handle_encryption_key_pending(msg) - } -} - -impl Handler for ProofRequestActor { - type Result = (); - - fn handle(&mut self, msg: ComputeResponse, _ctx: &mut Self::Context) -> Self::Result { - self.handle_compute_response(msg) - } -} - -impl Handler for ProofRequestActor { - type Result = (); - - fn handle(&mut self, msg: ComputeRequestError, _ctx: &mut Self::Context) -> Self::Result { - self.handle_compute_request_error(msg) - } -} +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use std::collections::HashMap; +use std::sync::Arc; + +use actix::{Actor, Addr, Context, Handler}; +use alloy::signers::local::PrivateKeySigner; +use e3_events::{ + BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeResponse, + ComputeResponseKind, CorrelationId, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, + EncryptionKeyCreated, EncryptionKeyPending, Event, EventPublisher, EventSubscriber, EventType, + PkBfvProofRequest, ProofPayload, ProofType, SignedProofPayload, ZkRequest, ZkResponse, +}; +use e3_utils::NotifySync; +use tracing::{error, info}; + +#[derive(Clone, Debug)] +struct PendingProofRequest { + e3_id: E3id, + key: Arc, +} + +/// Core actor that handles encryption key proof requests. +/// +/// Proofs are always wrapped in a [`SignedProofPayload`] before being published, +/// enabling fault attribution via the signed proof model. +/// A signer is required — if signing fails, the proof is not published. +pub struct ProofRequestActor { + bus: BusHandle, + signer: PrivateKeySigner, + pending: HashMap, +} + +impl ProofRequestActor { + pub fn new(bus: &BusHandle, signer: PrivateKeySigner) -> Self { + Self { + bus: bus.clone(), + signer, + pending: HashMap::new(), + } + } + + pub fn setup(bus: &BusHandle, signer: PrivateKeySigner) -> Addr { + let addr = Self::new(bus, signer).start(); + bus.subscribe(EventType::EncryptionKeyPending, addr.clone().into()); + bus.subscribe(EventType::ComputeResponse, addr.clone().into()); + bus.subscribe(EventType::ComputeRequestError, addr.clone().into()); + addr + } + + fn handle_encryption_key_pending(&mut self, msg: EncryptionKeyPending) { + let correlation_id = CorrelationId::new(); + self.pending.insert( + correlation_id, + PendingProofRequest { + e3_id: msg.e3_id.clone(), + key: msg.key.clone(), + }, + ); + + let request = ComputeRequest::zk( + ZkRequest::PkBfv(PkBfvProofRequest::new( + msg.key.pk_bfv.clone(), + msg.params_preset, + )), + correlation_id, + msg.e3_id, + ); + + info!("Requesting T0 proof generation"); + if let Err(err) = self.bus.publish(request) { + error!("Failed to publish ZK proof request: {err}"); + self.pending.remove(&correlation_id); + } + } + + fn handle_compute_response(&mut self, msg: ComputeResponse) { + let ComputeResponseKind::Zk(ZkResponse::PkBfv(resp)) = msg.response else { + return; + }; + + let Some(pending) = self.pending.remove(&msg.correlation_id) else { + return; + }; + + let mut key = (*pending.key).clone(); + key.proof = Some(resp.proof.clone()); + + // Always sign the proof payload — unsigned proofs are not published + let payload = ProofPayload { + e3_id: pending.e3_id.clone(), + proof_type: ProofType::T0PkBfv, + proof: resp.proof.clone(), + }; + + match SignedProofPayload::sign(payload, &self.signer) { + Ok(signed) => { + info!( + "Signed T0 proof for party {} (signer: {})", + key.party_id, + self.signer.address() + ); + key.signed_payload = Some(signed); + } + Err(err) => { + error!("Failed to sign T0 proof payload: {err} — proof will not be published"); + return; + } + } + + if let Err(err) = self.bus.publish(EncryptionKeyCreated { + e3_id: pending.e3_id, + key: Arc::new(key), + external: false, + }) { + error!("Failed to publish EncryptionKeyCreated: {err}"); + } + } + + fn handle_compute_request_error(&mut self, msg: ComputeRequestError) { + let ComputeRequestErrorKind::Zk(err) = msg.get_err() else { + return; + }; + + if let Some(pending) = self.pending.remove(msg.correlation_id()) { + error!( + "ZK proof request failed for E3 {}: {err} — key will not be published without proof", + pending.e3_id + ); + } + } +} + +impl Actor for ProofRequestActor { + type Context = Context; +} + +impl Handler for ProofRequestActor { + type Result = (); + + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + match msg.into_data() { + EnclaveEventData::EncryptionKeyPending(data) => self.notify_sync(ctx, data), + EnclaveEventData::ComputeResponse(data) => self.notify_sync(ctx, data), + EnclaveEventData::ComputeRequestError(data) => self.notify_sync(ctx, data), + _ => (), + } + } +} + +impl Handler for ProofRequestActor { + type Result = (); + + fn handle(&mut self, msg: EncryptionKeyPending, _ctx: &mut Self::Context) -> Self::Result { + self.handle_encryption_key_pending(msg) + } +} + +impl Handler for ProofRequestActor { + type Result = (); + + fn handle(&mut self, msg: ComputeResponse, _ctx: &mut Self::Context) -> Self::Result { + self.handle_compute_response(msg) + } +} + +impl Handler for ProofRequestActor { + type Result = (); + + fn handle(&mut self, msg: ComputeRequestError, _ctx: &mut Self::Context) -> Self::Result { + self.handle_compute_request_error(msg) + } +} diff --git a/crates/zk-prover/src/actors/proof_verification.rs b/crates/zk-prover/src/actors/proof_verification.rs index b6fe432b70..5b209d3784 100644 --- a/crates/zk-prover/src/actors/proof_verification.rs +++ b/crates/zk-prover/src/actors/proof_verification.rs @@ -1,146 +1,232 @@ -// 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. - -//! Core business logic actor for verifying received encryption keys. -//! This actor verifies EncryptionKeyReceived events and converts them -//! to EncryptionKeyCreated events after validation. -//! -//! This is a CORE actor - it delegates IO operations (verification) to ZkActor. - -use std::sync::Arc; - -use actix::{Actor, Addr, AsyncContext, Context, Handler, Message, Recipient}; -use e3_events::{ - BusHandle, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCreated, - EncryptionKeyReceived, Event, EventPublisher, EventSubscriber, EventType, Proof, -}; -use e3_utils::NotifySync; -use tracing::{error, info, warn}; - -/// Request to verify a ZK proof. -#[derive(Debug, Message)] -#[rtype(result = "()")] -pub struct ZkVerificationRequest { - pub proof: Proof, - pub e3_id: E3id, - pub key: Arc, - pub sender: Recipient, -} - -/// Response from ZK proof verification with context. -#[derive(Debug, Clone, Message)] -#[rtype(result = "()")] -pub struct ZkVerificationResponse { - pub verified: bool, - pub error: Option, - pub e3_id: E3id, - pub key: Arc, -} - -/// Core actor that handles encryption key verification. -pub struct ProofVerificationActor { - bus: BusHandle, - verifier: Option>, -} - -impl ProofVerificationActor { - pub fn new(bus: &BusHandle, verifier: Option>) -> Self { - Self { - bus: bus.clone(), - verifier, - } - } - - pub fn setup( - bus: &BusHandle, - verifier: Option>, - ) -> Addr { - let addr = Self::new(bus, verifier).start(); - bus.subscribe(EventType::EncryptionKeyReceived, addr.clone().into()); - addr - } - - fn handle_encryption_key_received(&mut self, msg: EncryptionKeyReceived, ctx: &Context) { - let Some(ref verifier) = self.verifier else { - warn!( - "ZK verifier not available - accepting key from party {} without verification", - msg.key.party_id - ); - self.publish_key_created(msg.e3_id, msg.key); - return; - }; - - let Some(ref proof) = msg.key.proof else { - warn!( - "External key from party {} is missing T0 proof - rejecting", - msg.key.party_id - ); - return; - }; - - let request = ZkVerificationRequest { - proof: proof.clone(), - e3_id: msg.e3_id, - key: msg.key, - sender: ctx.address().recipient(), - }; - - verifier.do_send(request); - } - - fn publish_key_created(&self, e3_id: E3id, key: Arc) { - if let Err(err) = self.bus.publish(EncryptionKeyCreated { - e3_id, - key, - external: true, - }) { - error!("Failed to publish EncryptionKeyCreated: {err}"); - } - } -} - -impl Actor for ProofVerificationActor { - type Context = Context; -} - -impl Handler for ProofVerificationActor { - type Result = (); - - fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { - match msg.into_data() { - EnclaveEventData::EncryptionKeyReceived(data) => self.notify_sync(ctx, data), - _ => (), - } - } -} - -impl Handler for ProofVerificationActor { - type Result = (); - - fn handle(&mut self, msg: EncryptionKeyReceived, ctx: &mut Self::Context) -> Self::Result { - self.handle_encryption_key_received(msg, ctx) - } -} - -impl Handler for ProofVerificationActor { - type Result = (); - - fn handle(&mut self, msg: ZkVerificationResponse, _ctx: &mut Self::Context) -> Self::Result { - if msg.verified { - info!( - "T0 proof verified for party {} - accepting key", - msg.key.party_id - ); - self.publish_key_created(msg.e3_id, msg.key); - } else { - error!( - "T0 proof verification FAILED for party {} - rejecting key: {}", - msg.key.party_id, - msg.error.unwrap_or_else(|| "unknown error".to_string()) - ); - } - } -} +// 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. + +//! Core business logic actor for verifying received encryption keys. +//! +//! This actor verifies `EncryptionKeyReceived` events and converts them +//! to `EncryptionKeyCreated` events after validation. +//! +//! ## Signature Verification +//! +//! Every received key must carry a [`SignedProofPayload`]. This actor: +//! 1. Recovers the address from the ECDSA signature. +//! 2. Delegates the ZK proof to `ZkActor` for verification. +//! 3. On ZK failure, emits [`SignedProofFailed`] with the full evidence bundle +//! and [`E3Failed`] to stop the E3 computation. +//! +//! Keys without a signed proof are rejected outright. + +use std::collections::HashMap; +use std::sync::Arc; + +use actix::{Actor, Addr, AsyncContext, Context, Handler, Message, Recipient}; +use alloy::primitives::Address; +use e3_events::{ + BusHandle, E3Failed, E3Stage, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, + EncryptionKeyCreated, EncryptionKeyReceived, Event, EventPublisher, EventSubscriber, EventType, + FailureReason, Proof, SignedProofFailed, SignedProofPayload, +}; +use e3_utils::NotifySync; +use tracing::{error, info, warn}; + +/// Request to verify a ZK proof. +#[derive(Debug, Message)] +#[rtype(result = "()")] +pub struct ZkVerificationRequest { + pub proof: Proof, + pub e3_id: E3id, + pub key: Arc, + pub sender: Recipient, +} + +/// Response from ZK proof verification with context. +#[derive(Debug, Clone, Message)] +#[rtype(result = "()")] +pub struct ZkVerificationResponse { + pub verified: bool, + pub error: Option, + pub e3_id: E3id, + pub key: Arc, +} + +/// Tracks a pending verification including the signed payload for fault evidence. +#[derive(Clone, Debug)] +struct PendingVerification { + signed_payload: SignedProofPayload, + recovered_signer: Address, +} + +/// Core actor that handles encryption key verification. +/// +/// Requires every received key to carry a [`SignedProofPayload`]. +/// On ZK verification failure, emits both [`SignedProofFailed`] (for fault +/// attribution) and [`E3Failed`] (to stop the E3 computation). +pub struct ProofVerificationActor { + bus: BusHandle, + verifier: Recipient, + /// Tracks signed payloads for keys currently being verified, + /// keyed by `(e3_id, party_id)`. + pending: HashMap<(E3id, u64), PendingVerification>, +} + +impl ProofVerificationActor { + pub fn new(bus: &BusHandle, verifier: Recipient) -> Self { + Self { + bus: bus.clone(), + verifier, + pending: HashMap::new(), + } + } + + pub fn setup(bus: &BusHandle, verifier: Recipient) -> Addr { + let addr = Self::new(bus, verifier).start(); + bus.subscribe(EventType::EncryptionKeyReceived, addr.clone().into()); + addr + } + + fn handle_encryption_key_received(&mut self, msg: EncryptionKeyReceived, ctx: &Context) { + let Some(ref proof) = msg.key.proof else { + error!( + "External key from party {} is missing T0 proof - rejecting", + msg.key.party_id + ); + return; + }; + + // Signed proofs are mandatory — reject keys without a signed payload + let signed = match &msg.key.signed_payload { + Some(signed) => signed.clone(), + None => { + error!( + "Key from party {} has no signed payload - rejecting (signed proofs are required)", + msg.key.party_id + ); + return; + } + }; + + // Recover the address from the signature + let recovered_address = match signed.recover_address() { + Ok(addr) => { + info!( + "Recovered address {} for key from party {}", + addr, msg.key.party_id + ); + addr + } + Err(err) => { + error!( + "Invalid signature on key from party {} - rejecting: {err}", + msg.key.party_id + ); + 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), + PendingVerification { + signed_payload: signed, + recovered_signer: recovered_address, + }, + ); + + let request = ZkVerificationRequest { + proof: proof.clone(), + e3_id: msg.e3_id, + key: msg.key, + sender: ctx.address().recipient(), + }; + + self.verifier.do_send(request); + } + + fn publish_key_created(&self, e3_id: E3id, key: Arc) { + if let Err(err) = self.bus.publish(EncryptionKeyCreated { + e3_id, + key, + external: true, + }) { + error!("Failed to publish EncryptionKeyCreated: {err}"); + } + } +} + +impl Actor for ProofVerificationActor { + type Context = Context; +} + +impl Handler for ProofVerificationActor { + type Result = (); + + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + match msg.into_data() { + EnclaveEventData::EncryptionKeyReceived(data) => self.notify_sync(ctx, data), + _ => (), + } + } +} + +impl Handler for ProofVerificationActor { + type Result = (); + + fn handle(&mut self, msg: EncryptionKeyReceived, ctx: &mut Self::Context) -> Self::Result { + self.handle_encryption_key_received(msg, ctx) + } +} + +impl Handler for ProofVerificationActor { + type Result = (); + + fn handle(&mut self, msg: ZkVerificationResponse, _ctx: &mut Self::Context) -> Self::Result { + let pending_key = (msg.e3_id.clone(), msg.key.party_id); + let pending = self.pending.remove(&pending_key); + + if msg.verified { + info!( + "T0 proof verified for party {} - accepting key", + msg.key.party_id + ); + self.publish_key_created(msg.e3_id, msg.key); + } else { + let error_msg = msg.error.unwrap_or_else(|| "unknown error".to_string()); + error!( + "T0 proof verification FAILED for party {} - rejecting key and stopping E3: {}", + msg.key.party_id, error_msg + ); + + // Emit SignedProofFailed for fault attribution + if let Some(PendingVerification { + signed_payload, + recovered_signer, + }) = pending + { + warn!( + "Emitting SignedProofFailed for party {} (address: {recovered_signer})", + msg.key.party_id + ); + if let Err(err) = self.bus.publish(SignedProofFailed { + e3_id: msg.e3_id.clone(), + faulting_node: recovered_signer, + proof_type: signed_payload.payload.proof_type, + signed_payload, + }) { + error!("Failed to publish SignedProofFailed: {err}"); + } + } + + // Stop the E3 computation — proof verification failure is fatal + if let Err(err) = self.bus.publish(E3Failed { + e3_id: msg.e3_id, + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::VerificationFailed, + }) { + error!("Failed to publish E3Failed: {err}"); + } + } + } +}