From 60fa8e4a9a197564a25e76220a50dc237c8bc412 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 11 Feb 2026 04:54:54 +0500 Subject: [PATCH 1/9] feat: sign zk proofs --- Cargo.lock | 1 + .../src/ciphernode_builder.rs | 3 +- .../enclave_event/encryption_key_created.rs | 11 +- crates/events/src/enclave_event/mod.rs | 5 + .../events/src/enclave_event/signed_proof.rs | 338 +++++++++++++++ crates/zk-prover/Cargo.toml | 1 + crates/zk-prover/src/actors/mod.rs | 13 +- crates/zk-prover/src/actors/proof_request.rs | 50 ++- .../src/actors/proof_verification.rs | 384 +++++++++++------- 9 files changed, 651 insertions(+), 155 deletions(-) create mode 100644 crates/events/src/enclave_event/signed_proof.rs diff --git a/Cargo.lock b/Cargo.lock index 93a9a02ee5..89f21a35c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3817,6 +3817,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..e9557fc230 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -435,7 +435,8 @@ impl CiphernodeBuilder { )); info!("Setting up ZK actors"); - setup_zk_actors(&bus, self.zk_backend.as_ref()); + let signer = provider_cache.ensure_signer().await.ok(); + setup_zk_actors(&bus, self.zk_backend.as_ref(), signer); } if self.pubkey_agg { 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..1ee42280ff --- /dev/null +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -0,0 +1,338 @@ +// 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, Signature}; +use alloy::signers::{k256::ecdsa::SigningKey, local::LocalSigner, SignerSync}; +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. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ProofType { + /// T0 — BFV public key proof (Proof 0). + T0PkBfv, + /// T1 — TrBFV public key generation proof (Proof 1). + T1PkGeneration, + /// T1 — Secret key share computation proof (Proof 2a). + T1SkShareComputation, + /// T1 — Smudging noise share computation proof (Proof 2b). + T1ESmShareComputation, + /// T1 — Secret key share encryption proof (Proof 3a). + T1SkShareEncryption, + /// T1 — Smudging noise share encryption proof (Proof 3b). + T1ESmShareEncryption, + /// T2 — Secret key share decryption proof (Proof 4a). + T2SkShareDecryption, + /// T2 — Smudging noise share decryption proof (Proof 4b). + T2ESmShareDecryption, + /// T5 — Share decryption proof (Proof 6). + T5ShareDecryption, +} + +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. +/// +/// The canonical encoding matches Solidity's `abi.encode` layout so that +/// 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 sender's party ID within the committee (0-indexed). + pub party_id: u64, + /// The actual data being proven (e.g. `pk_bfv` bytes, share bytes). + #[derivative(Debug(format_with = "e3_utils::formatters::hexf"))] + pub data: ArcBytes, + /// The ZK proof that attests to `data`. + pub proof: Proof, +} + +impl ProofPayload { + /// Compute the keccak256 digest of the canonical encoding. + /// + /// The encoding concatenates all fields as length-prefixed byte arrays + /// preceded by fixed-size scalars, matching the structure the on-chain + /// verifier will reconstruct. + pub fn digest(&self) -> [u8; 32] { + // Encode: e3_id chain_id (u64) | e3_id id (u64) | proof_type (u8) | party_id (u64) + // | len(data) (u32) | data | len(proof) (u32) | proof + // | len(public_signals) (u32) | public_signals + let mut buf = Vec::new(); + buf.extend_from_slice(&self.e3_id.chain_id().to_be_bytes()); + let id_bytes = self.e3_id.e3_id().as_bytes(); + buf.extend_from_slice(&(id_bytes.len() as u32).to_be_bytes()); + buf.extend_from_slice(id_bytes); + buf.push(self.proof_type as u8); + buf.extend_from_slice(&self.party_id.to_be_bytes()); + // data + buf.extend_from_slice(&(self.data.len() as u32).to_be_bytes()); + buf.extend_from_slice(&self.data); + // proof bytes + buf.extend_from_slice(&(self.proof.data.len() as u32).to_be_bytes()); + buf.extend_from_slice(&self.proof.data); + // public_signals + buf.extend_from_slice(&(self.proof.public_signals.len() as u32).to_be_bytes()); + buf.extend_from_slice(&self.proof.public_signals); + + keccak256(&buf).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: &LocalSigner) -> Result { + let digest = payload.digest(); + let sig: Signature = signer + .sign_message_sync(&digest) + .map_err(|e| anyhow!("Failed to sign proof payload: {e}"))?; + + // Encode as 65-byte r ‖ s ‖ v + let mut sig_bytes = Vec::with_capacity(65); + sig_bytes.extend_from_slice(&sig.r().to_be_bytes::<32>()); + sig_bytes.extend_from_slice(&sig.s().to_be_bytes::<32>()); + sig_bytes.push(sig.v() as u8); + + Ok(Self { + payload, + signature: ArcBytes::from_bytes(&sig_bytes), + }) + } + + /// Recover the Ethereum address that produced this signature. + pub fn recover_signer(&self) -> Result
{ + if self.signature.len() != 65 { + return Err(anyhow!( + "Invalid signature length: expected 65, got {}", + self.signature.len() + )); + } + + let r = alloy::primitives::U256::from_be_slice(&self.signature[..32]); + let s = alloy::primitives::U256::from_be_slice(&self.signature[32..64]); + let v = self.signature[64] != 0; + let sig = Signature::new(r, s, v); + + let digest = self.payload.digest(); + // EIP-191 personal message hash: "\x19Ethereum Signed Message:\n32" ++ digest + let prefixed = alloy::primitives::eip191_hash_message(digest); + + sig.recover_address_from_prehash(&prefixed) + .map_err(|e| anyhow!("Failed to recover signer address: {e}")) + } + + /// Verify that the recovered signer matches the expected address. + pub fn verify_signer(&self, expected: &Address) -> Result { + let recovered = self.recover_signer()?; + 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::*; + use alloy::signers::local::PrivateKeySigner; + + 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, + party_id: 3, + data: ArcBytes::from_bytes(&[1, 2, 3, 4]), + 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_signer().expect("recovery should succeed"); + assert_eq!(recovered, signer.address()); + } + + #[test] + fn verify_signer_correct_address() { + let signer = test_signer(); + let payload = test_payload(); + + let signed = SignedProofPayload::sign(payload, &signer).expect("signing should succeed"); + assert!(signed + .verify_signer(&signer.address()) + .expect("verify should succeed")); + } + + #[test] + fn verify_signer_wrong_address() { + 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_signer(&wrong_addr) + .expect("verify should succeed")); + } + + #[test] + fn different_payloads_produce_different_digests() { + let p1 = test_payload(); + let mut p2 = test_payload(); + p2.party_id = 99; + + assert_ne!(p1.digest(), p2.digest()); + } + + #[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.party_id = 999; + + let recovered = signed.recover_signer().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/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..acf12e54f5 100644 --- a/crates/zk-prover/src/actors/mod.rs +++ b/crates/zk-prover/src/actors/mod.rs @@ -41,6 +41,7 @@ pub use proof_verification::{ pub use zk_actor::ZkActor; use actix::{Actor, Addr}; +use alloy::signers::{k256::ecdsa::SigningKey, local::LocalSigner}; use e3_events::BusHandle; use crate::ZkBackend; @@ -54,7 +55,15 @@ use crate::ZkBackend; /// 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 { +/// +/// When `signer` is provided: +/// - Proof request actor will sign proofs enabling fault attribution +/// - Without a signer, proofs are still generated but unsigned +pub fn setup_zk_actors( + bus: &BusHandle, + backend: Option<&ZkBackend>, + signer: Option>, +) -> ZkActors { let (zk_actor, verifier) = if let Some(backend) = backend { let zk_actor = ZkActor::new(backend).start(); let verifier = Some(zk_actor.clone().recipient()); @@ -63,7 +72,7 @@ pub fn setup_zk_actors(bus: &BusHandle, backend: Option<&ZkBackend>) -> ZkActors (None, None) }; - let proof_request = ProofRequestActor::setup(bus, backend.is_some()); + let proof_request = ProofRequestActor::setup(bus, backend.is_some(), signer); let proof_verification = ProofVerificationActor::setup(bus, verifier); ZkActors { diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index 9896f5297a..5b390e5755 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -8,11 +8,12 @@ use std::collections::HashMap; use std::sync::Arc; use actix::{Actor, Addr, Context, Handler}; +use alloy::signers::{k256::ecdsa::SigningKey, local::LocalSigner}; use e3_events::{ BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeResponse, ComputeResponseKind, CorrelationId, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCreated, EncryptionKeyPending, Event, EventPublisher, EventSubscriber, EventType, - PkBfvProofRequest, ZkRequest, ZkResponse, + PkBfvProofRequest, ProofPayload, ProofType, SignedProofPayload, ZkRequest, ZkResponse, }; use e3_utils::NotifySync; use tracing::{error, info, warn}; @@ -24,23 +25,36 @@ struct PendingProofRequest { } /// Core actor that handles encryption key proof requests. +/// +/// When a signer is provided, proofs are wrapped in a [`SignedProofPayload`] +/// before being published — enabling fault attribution via the signed proof model. pub struct ProofRequestActor { bus: BusHandle, proofs_enabled: bool, + signer: Option>, pending: HashMap, } impl ProofRequestActor { - pub fn new(bus: &BusHandle, proofs_enabled: bool) -> Self { + pub fn new( + bus: &BusHandle, + proofs_enabled: bool, + signer: Option>, + ) -> Self { Self { bus: bus.clone(), proofs_enabled, + signer, pending: HashMap::new(), } } - pub fn setup(bus: &BusHandle, proofs_enabled: bool) -> Addr { - let addr = Self::new(bus, proofs_enabled).start(); + pub fn setup( + bus: &BusHandle, + proofs_enabled: bool, + signer: Option>, + ) -> Addr { + let addr = Self::new(bus, proofs_enabled, signer).start(); bus.subscribe(EventType::EncryptionKeyPending, addr.clone().into()); bus.subscribe(EventType::ComputeResponse, addr.clone().into()); bus.subscribe(EventType::ComputeRequestError, addr.clone().into()); @@ -98,7 +112,33 @@ impl ProofRequestActor { }; let mut key = (*pending.key).clone(); - key.proof = Some(resp.proof); + key.proof = Some(resp.proof.clone()); + + // Sign the proof payload if we have a signer + if let Some(ref signer) = self.signer { + let payload = ProofPayload { + e3_id: pending.e3_id.clone(), + proof_type: ProofType::T0PkBfv, + party_id: key.party_id, + data: key.pk_bfv.clone(), + proof: resp.proof.clone(), + }; + + match SignedProofPayload::sign(payload, signer) { + Ok(signed) => { + info!( + "Signed T0 proof for party {} (signer: {})", + key.party_id, + signer.address() + ); + key.signed_payload = Some(signed); + } + Err(err) => { + error!("Failed to sign T0 proof payload: {err}"); + // Continue without signature — proof still valid, just not attributable + } + } + } if let Err(err) = self.bus.publish(EncryptionKeyCreated { e3_id: pending.e3_id, diff --git a/crates/zk-prover/src/actors/proof_verification.rs b/crates/zk-prover/src/actors/proof_verification.rs index b6fe432b70..42d86dc124 100644 --- a/crates/zk-prover/src/actors/proof_verification.rs +++ b/crates/zk-prover/src/actors/proof_verification.rs @@ -1,146 +1,238 @@ -// 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 +//! +//! When the received key carries a [`SignedProofPayload`], this actor: +//! 1. Recovers the signer 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 +//! so the `FaultSubmitter` can submit a slash proposal on-chain. +//! +//! This is a CORE actor - it delegates IO operations (verification) to ZkActor. + +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, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCreated, + EncryptionKeyReceived, Event, EventPublisher, EventSubscriber, EventType, 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: Option, + recovered_signer: Option
, +} + +/// Core actor that handles encryption key verification. +/// +/// On ZK verification failure, if the key carried a valid [`SignedProofPayload`], +/// emits a [`SignedProofFailed`] event with the signed evidence bundle. +pub struct ProofVerificationActor { + bus: BusHandle, + verifier: Option>, + /// 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: Option>) -> Self { + Self { + bus: bus.clone(), + verifier, + pending: HashMap::new(), + } + } + + 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; + }; + + // Validate the signed payload if present + let (signed_payload, recovered_signer) = if let Some(ref signed) = msg.key.signed_payload { + match signed.recover_signer() { + Ok(addr) => { + info!( + "Recovered signer {} for key from party {}", + addr, msg.key.party_id + ); + (Some(signed.clone()), Some(addr)) + } + Err(err) => { + warn!( + "Invalid signature on key from party {} - proceeding without \ + fault attribution: {err}", + msg.key.party_id + ); + (None, None) + } + } + } else { + warn!( + "Key from party {} has no signed payload - \ + proof verification will proceed but fault attribution unavailable", + msg.key.party_id + ); + (None, None) + }; + + // 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, + recovered_signer, + }, + ); + + 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 { + 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 { + error!( + "T0 proof verification FAILED for party {} - rejecting key: {}", + msg.key.party_id, + msg.error.unwrap_or_else(|| "unknown error".to_string()) + ); + + // If we have a signed payload, emit SignedProofFailed for fault attribution + if let Some(PendingVerification { + signed_payload: Some(signed), + recovered_signer: Some(signer), + }) = pending + { + warn!( + "Emitting SignedProofFailed for party {} (signer: {signer})", + msg.key.party_id + ); + if let Err(err) = self.bus.publish(SignedProofFailed { + e3_id: msg.e3_id, + faulting_node: signer, + proof_type: signed.payload.proof_type, + signed_payload: signed, + }) { + error!("Failed to publish SignedProofFailed: {err}"); + } + } else { + warn!( + "No signed payload available for party {} - \ + fault cannot be attributed on-chain", + msg.key.party_id + ); + } + } + } +} From a0255236bd71aae582c40af8d38d1d16d851f269 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 11 Feb 2026 05:00:57 +0500 Subject: [PATCH 2/9] feat: use alloy signature to verify proofs --- .../events/src/enclave_event/signed_proof.rs | 93 ++-- crates/zk-prover/src/actors/mod.rs | 4 +- crates/zk-prover/src/actors/proof_request.rs | 418 +++++++++--------- 3 files changed, 241 insertions(+), 274 deletions(-) diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index 1ee42280ff..a6c8f78339 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -13,8 +13,9 @@ use crate::{CircuitName, E3id, Proof}; use actix::Message; -use alloy::primitives::{keccak256, Address, Signature}; -use alloy::signers::{k256::ecdsa::SigningKey, local::LocalSigner, SignerSync}; +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; @@ -88,8 +89,10 @@ impl fmt::Display for ProofType { /// Data payload that a node signs before broadcasting. /// -/// The canonical encoding matches Solidity's `abi.encode` layout so that -/// on-chain `ecrecover` can reconstruct the same digest. +/// Only contains data needed for on-chain fault verification: +/// the E3 identifier, proof type, and the ZK proof itself. +/// Encoded via `abi.encodePacked(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 { @@ -97,12 +100,7 @@ pub struct ProofPayload { pub e3_id: E3id, /// Which proof this payload carries. pub proof_type: ProofType, - /// The sender's party ID within the committee (0-indexed). - pub party_id: u64, - /// The actual data being proven (e.g. `pk_bfv` bytes, share bytes). - #[derivative(Debug(format_with = "e3_utils::formatters::hexf"))] - pub data: ArcBytes, - /// The ZK proof that attests to `data`. + /// The ZK proof that attests to the data. pub proof: Proof, } @@ -113,27 +111,23 @@ impl ProofPayload { /// preceded by fixed-size scalars, matching the structure the on-chain /// verifier will reconstruct. pub fn digest(&self) -> [u8; 32] { - // Encode: e3_id chain_id (u64) | e3_id id (u64) | proof_type (u8) | party_id (u64) - // | len(data) (u32) | data | len(proof) (u32) | proof - // | len(public_signals) (u32) | public_signals - let mut buf = Vec::new(); - buf.extend_from_slice(&self.e3_id.chain_id().to_be_bytes()); - let id_bytes = self.e3_id.e3_id().as_bytes(); - buf.extend_from_slice(&(id_bytes.len() as u32).to_be_bytes()); - buf.extend_from_slice(id_bytes); - buf.push(self.proof_type as u8); - buf.extend_from_slice(&self.party_id.to_be_bytes()); - // data - buf.extend_from_slice(&(self.data.len() as u32).to_be_bytes()); - buf.extend_from_slice(&self.data); - // proof bytes - buf.extend_from_slice(&(self.proof.data.len() as u32).to_be_bytes()); - buf.extend_from_slice(&self.proof.data); - // public_signals - buf.extend_from_slice(&(self.proof.public_signals.len() as u32).to_be_bytes()); - buf.extend_from_slice(&self.proof.public_signals); - - keccak256(&buf).into() + let e3_id_u256: U256 = self + .e3_id + .clone() + .try_into() + .expect("E3id should be valid U256"); + + // keccak256(abi.encodePacked(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_packed(); + + keccak256(&encoded).into() } } @@ -154,43 +148,25 @@ pub struct SignedProofPayload { impl SignedProofPayload { /// Sign a [`ProofPayload`] with the node's ECDSA key. - pub fn sign(payload: ProofPayload, signer: &LocalSigner) -> Result { + pub fn sign(payload: ProofPayload, signer: &PrivateKeySigner) -> Result { let digest = payload.digest(); - let sig: Signature = signer + let sig = signer .sign_message_sync(&digest) .map_err(|e| anyhow!("Failed to sign proof payload: {e}"))?; - // Encode as 65-byte r ‖ s ‖ v - let mut sig_bytes = Vec::with_capacity(65); - sig_bytes.extend_from_slice(&sig.r().to_be_bytes::<32>()); - sig_bytes.extend_from_slice(&sig.s().to_be_bytes::<32>()); - sig_bytes.push(sig.v() as u8); - Ok(Self { payload, - signature: ArcBytes::from_bytes(&sig_bytes), + signature: ArcBytes::from_bytes(&sig.as_bytes()), }) } /// Recover the Ethereum address that produced this signature. pub fn recover_signer(&self) -> Result
{ - if self.signature.len() != 65 { - return Err(anyhow!( - "Invalid signature length: expected 65, got {}", - self.signature.len() - )); - } - - let r = alloy::primitives::U256::from_be_slice(&self.signature[..32]); - let s = alloy::primitives::U256::from_be_slice(&self.signature[32..64]); - let v = self.signature[64] != 0; - let sig = Signature::new(r, s, v); + let sig = Signature::try_from(&self.signature[..]) + .map_err(|e| anyhow!("Invalid signature: {e}"))?; let digest = self.payload.digest(); - // EIP-191 personal message hash: "\x19Ethereum Signed Message:\n32" ++ digest - let prefixed = alloy::primitives::eip191_hash_message(digest); - - sig.recover_address_from_prehash(&prefixed) + sig.recover_address_from_msg(&digest) .map_err(|e| anyhow!("Failed to recover signer address: {e}")) } @@ -234,7 +210,6 @@ impl Display for SignedProofFailed { #[cfg(test)] mod tests { use super::*; - use alloy::signers::local::PrivateKeySigner; fn test_signer() -> PrivateKeySigner { // Deterministic test key @@ -247,8 +222,6 @@ mod tests { ProofPayload { e3_id: E3id::new("1", 42), proof_type: ProofType::T0PkBfv, - party_id: 3, - data: ArcBytes::from_bytes(&[1, 2, 3, 4]), proof: Proof::new( CircuitName::PkBfv, ArcBytes::from_bytes(&[10, 20, 30]), @@ -299,7 +272,7 @@ mod tests { fn different_payloads_produce_different_digests() { let p1 = test_payload(); let mut p2 = test_payload(); - p2.party_id = 99; + p2.proof_type = ProofType::T1PkGeneration; assert_ne!(p1.digest(), p2.digest()); } @@ -312,7 +285,7 @@ mod tests { let mut signed = SignedProofPayload::sign(payload, &signer).expect("signing should succeed"); // Tamper with the payload after signing - signed.payload.party_id = 999; + signed.payload.proof_type = ProofType::T1PkGeneration; let recovered = signed.recover_signer().expect("recovery should succeed"); // Recovered address won't match the signer because payload was tampered diff --git a/crates/zk-prover/src/actors/mod.rs b/crates/zk-prover/src/actors/mod.rs index acf12e54f5..6cb30641b2 100644 --- a/crates/zk-prover/src/actors/mod.rs +++ b/crates/zk-prover/src/actors/mod.rs @@ -41,7 +41,7 @@ pub use proof_verification::{ pub use zk_actor::ZkActor; use actix::{Actor, Addr}; -use alloy::signers::{k256::ecdsa::SigningKey, local::LocalSigner}; +use alloy::signers::local::PrivateKeySigner; use e3_events::BusHandle; use crate::ZkBackend; @@ -62,7 +62,7 @@ use crate::ZkBackend; pub fn setup_zk_actors( bus: &BusHandle, backend: Option<&ZkBackend>, - signer: Option>, + signer: Option, ) -> ZkActors { let (zk_actor, verifier) = if let Some(backend) = backend { let zk_actor = ZkActor::new(backend).start(); diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index 5b390e5755..693bef12a1 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -1,212 +1,206 @@ -// 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::{k256::ecdsa::SigningKey, local::LocalSigner}; -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, warn}; - -#[derive(Clone, Debug)] -struct PendingProofRequest { - e3_id: E3id, - key: Arc, -} - -/// Core actor that handles encryption key proof requests. -/// -/// When a signer is provided, proofs are wrapped in a [`SignedProofPayload`] -/// before being published — enabling fault attribution via the signed proof model. -pub struct ProofRequestActor { - bus: BusHandle, - proofs_enabled: bool, - signer: Option>, - pending: HashMap, -} - -impl ProofRequestActor { - pub fn new( - bus: &BusHandle, - proofs_enabled: bool, - signer: Option>, - ) -> Self { - Self { - bus: bus.clone(), - proofs_enabled, - signer, - pending: HashMap::new(), - } - } - - pub fn setup( - bus: &BusHandle, - proofs_enabled: bool, - signer: Option>, - ) -> Addr { - let addr = Self::new(bus, proofs_enabled, 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) { - 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.clone()); - - // Sign the proof payload if we have a signer - if let Some(ref signer) = self.signer { - let payload = ProofPayload { - e3_id: pending.e3_id.clone(), - proof_type: ProofType::T0PkBfv, - party_id: key.party_id, - data: key.pk_bfv.clone(), - proof: resp.proof.clone(), - }; - - match SignedProofPayload::sign(payload, signer) { - Ok(signed) => { - info!( - "Signed T0 proof for party {} (signer: {})", - key.party_id, - signer.address() - ); - key.signed_payload = Some(signed); - } - Err(err) => { - error!("Failed to sign T0 proof payload: {err}"); - // Continue without signature — proof still valid, just not attributable - } - } - } - - 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, warn}; + +#[derive(Clone, Debug)] +struct PendingProofRequest { + e3_id: E3id, + key: Arc, +} + +/// Core actor that handles encryption key proof requests. +/// +/// When a signer is provided, proofs are wrapped in a [`SignedProofPayload`] +/// before being published — enabling fault attribution via the signed proof model. +pub struct ProofRequestActor { + bus: BusHandle, + proofs_enabled: bool, + signer: Option, + pending: HashMap, +} + +impl ProofRequestActor { + pub fn new(bus: &BusHandle, proofs_enabled: bool, signer: Option) -> Self { + Self { + bus: bus.clone(), + proofs_enabled, + signer, + pending: HashMap::new(), + } + } + + pub fn setup( + bus: &BusHandle, + proofs_enabled: bool, + signer: Option, + ) -> Addr { + let addr = Self::new(bus, proofs_enabled, 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) { + 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.clone()); + + // Sign the proof payload if we have a signer + if let Some(ref signer) = self.signer { + let payload = ProofPayload { + e3_id: pending.e3_id.clone(), + proof_type: ProofType::T0PkBfv, + proof: resp.proof.clone(), + }; + + match SignedProofPayload::sign(payload, signer) { + Ok(signed) => { + info!( + "Signed T0 proof for party {} (signer: {})", + key.party_id, + signer.address() + ); + key.signed_payload = Some(signed); + } + Err(err) => { + error!("Failed to sign T0 proof payload: {err}"); + // Continue without signature — proof still valid, just not attributable + } + } + } + + 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) + } +} From 15a3d674358e42204b044b35202d1a9b940e462f Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 11 Feb 2026 05:59:05 +0500 Subject: [PATCH 3/9] fix: review comments --- .../events/src/enclave_event/signed_proof.rs | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index a6c8f78339..1d53433c46 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -26,26 +26,27 @@ use std::fmt::{self, Display}; /// /// 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, + T0PkBfv = 0, /// T1 — TrBFV public key generation proof (Proof 1). - T1PkGeneration, + T1PkGeneration = 1, /// T1 — Secret key share computation proof (Proof 2a). - T1SkShareComputation, + T1SkShareComputation = 2, /// T1 — Smudging noise share computation proof (Proof 2b). - T1ESmShareComputation, + T1ESmShareComputation = 3, /// T1 — Secret key share encryption proof (Proof 3a). - T1SkShareEncryption, + T1SkShareEncryption = 4, /// T1 — Smudging noise share encryption proof (Proof 3b). - T1ESmShareEncryption, + T1ESmShareEncryption = 5, /// T2 — Secret key share decryption proof (Proof 4a). - T2SkShareDecryption, + T2SkShareDecryption = 6, /// T2 — Smudging noise share decryption proof (Proof 4b). - T2ESmShareDecryption, + T2ESmShareDecryption = 7, /// T5 — Share decryption proof (Proof 6). - T5ShareDecryption, + T5ShareDecryption = 8, } impl ProofType { @@ -110,12 +111,12 @@ impl ProofPayload { /// The encoding concatenates all fields as length-prefixed byte arrays /// preceded by fixed-size scalars, matching the structure the on-chain /// verifier will reconstruct. - pub fn digest(&self) -> [u8; 32] { + pub fn digest(&self) -> Result<[u8; 32]> { let e3_id_u256: U256 = self .e3_id .clone() .try_into() - .expect("E3id should be valid U256"); + .map_err(|_| anyhow!("E3id cannot be converted to U256"))?; // keccak256(abi.encodePacked(chainId, e3Id, proofType, proof, publicSignals)) let encoded = ( @@ -127,7 +128,7 @@ impl ProofPayload { ) .abi_encode_packed(); - keccak256(&encoded).into() + Ok(keccak256(&encoded).into()) } } @@ -149,7 +150,7 @@ pub struct SignedProofPayload { impl SignedProofPayload { /// Sign a [`ProofPayload`] with the node's ECDSA key. pub fn sign(payload: ProofPayload, signer: &PrivateKeySigner) -> Result { - let digest = payload.digest(); + let digest = payload.digest()?; let sig = signer .sign_message_sync(&digest) .map_err(|e| anyhow!("Failed to sign proof payload: {e}"))?; @@ -165,7 +166,7 @@ impl SignedProofPayload { let sig = Signature::try_from(&self.signature[..]) .map_err(|e| anyhow!("Invalid signature: {e}"))?; - let digest = self.payload.digest(); + let digest = self.payload.digest()?; sig.recover_address_from_msg(&digest) .map_err(|e| anyhow!("Failed to recover signer address: {e}")) } @@ -274,7 +275,10 @@ mod tests { let mut p2 = test_payload(); p2.proof_type = ProofType::T1PkGeneration; - assert_ne!(p1.digest(), p2.digest()); + assert_ne!( + p1.digest().expect("digest should succeed"), + p2.digest().expect("digest should succeed") + ); } #[test] From 90d8be34e08d07d23e48b79c9ec2661693ecf4e5 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 11 Feb 2026 23:13:08 +0500 Subject: [PATCH 4/9] fix: make signer not optional --- .../src/ciphernode_builder.rs | 8 +- .../events/src/enclave_event/signed_proof.rs | 22 +-- crates/zk-prover/src/actors/mod.rs | 34 +---- crates/zk-prover/src/actors/proof_request.rs | 91 ++++-------- .../src/actors/proof_verification.rs | 138 +++++++++--------- 5 files changed, 121 insertions(+), 172 deletions(-) diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index e9557fc230..b4304107a5 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -434,9 +434,11 @@ impl CiphernodeBuilder { share_enc_preset, )); - info!("Setting up ZK actors"); - let signer = provider_cache.ensure_signer().await.ok(); - setup_zk_actors(&bus, self.zk_backend.as_ref(), signer); + if let Some(ref backend) = self.zk_backend { + info!("Setting up ZK actors"); + let signer = provider_cache.ensure_signer().await?; + setup_zk_actors(&bus, backend, signer); + } } if self.pubkey_agg { diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index 1d53433c46..7dbe1b527a 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -162,18 +162,18 @@ impl SignedProofPayload { } /// Recover the Ethereum address that produced this signature. - pub fn recover_signer(&self) -> Result
{ + 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 signer address: {e}")) + .map_err(|e| anyhow!("Failed to recover address: {e}")) } - /// Verify that the recovered signer matches the expected address. - pub fn verify_signer(&self, expected: &Address) -> Result { - let recovered = self.recover_signer()?; + /// 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) } } @@ -239,23 +239,23 @@ mod tests { let signed = SignedProofPayload::sign(payload.clone(), &signer).expect("signing should succeed"); - let recovered = signed.recover_signer().expect("recovery should succeed"); + let recovered = signed.recover_address().expect("recovery should succeed"); assert_eq!(recovered, signer.address()); } #[test] - fn verify_signer_correct_address() { + 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_signer(&signer.address()) + .verify_address(&signer.address()) .expect("verify should succeed")); } #[test] - fn verify_signer_wrong_address() { + fn verify_address_wrong() { let signer = test_signer(); let payload = test_payload(); @@ -265,7 +265,7 @@ mod tests { .parse() .unwrap(); assert!(!signed - .verify_signer(&wrong_addr) + .verify_address(&wrong_addr) .expect("verify should succeed")); } @@ -291,7 +291,7 @@ mod tests { // Tamper with the payload after signing signed.payload.proof_type = ProofType::T1PkGeneration; - let recovered = signed.recover_signer().expect("recovery should succeed"); + 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()); } diff --git a/crates/zk-prover/src/actors/mod.rs b/crates/zk-prover/src/actors/mod.rs index 6cb30641b2..2c334f87d8 100644 --- a/crates/zk-prover/src/actors/mod.rs +++ b/crates/zk-prover/src/actors/mod.rs @@ -27,7 +27,7 @@ //! let backend = ZkBackend::with_default_dir().await?; //! //! // Setup all actors with proper separation of concerns -//! setup_zk_actors(&bus, Some(&backend)); +//! setup_zk_actors(&bus, &backend); //! ``` pub mod proof_request; @@ -48,31 +48,13 @@ 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 -/// -/// When `signer` is provided: -/// - Proof request actor will sign proofs enabling fault attribution -/// - Without a signer, proofs are still generated but unsigned -pub fn setup_zk_actors( - bus: &BusHandle, - backend: Option<&ZkBackend>, - signer: Option, -) -> 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(), signer); + let proof_request = ProofRequestActor::setup(bus, signer); let proof_verification = ProofVerificationActor::setup(bus, verifier); ZkActors { @@ -84,7 +66,7 @@ pub fn setup_zk_actors( /// 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 693bef12a1..2a7ffe7408 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -16,7 +16,7 @@ use e3_events::{ PkBfvProofRequest, ProofPayload, ProofType, SignedProofPayload, ZkRequest, ZkResponse, }; use e3_utils::NotifySync; -use tracing::{error, info, warn}; +use tracing::{error, info}; #[derive(Clone, Debug)] struct PendingProofRequest { @@ -26,31 +26,26 @@ struct PendingProofRequest { /// Core actor that handles encryption key proof requests. /// -/// When a signer is provided, proofs are wrapped in a [`SignedProofPayload`] -/// before being published — enabling fault attribution via the signed proof model. +/// 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, - proofs_enabled: bool, - signer: Option, + signer: PrivateKeySigner, pending: HashMap, } impl ProofRequestActor { - pub fn new(bus: &BusHandle, proofs_enabled: bool, signer: Option) -> Self { + pub fn new(bus: &BusHandle, signer: PrivateKeySigner) -> Self { Self { bus: bus.clone(), - proofs_enabled, signer, pending: HashMap::new(), } } - pub fn setup( - bus: &BusHandle, - proofs_enabled: bool, - signer: Option, - ) -> Addr { - let addr = Self::new(bus, proofs_enabled, signer).start(); + 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()); @@ -58,21 +53,6 @@ impl ProofRequestActor { } 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, @@ -110,27 +90,25 @@ impl ProofRequestActor { let mut key = (*pending.key).clone(); key.proof = Some(resp.proof.clone()); - // Sign the proof payload if we have a signer - if let Some(ref signer) = self.signer { - let payload = ProofPayload { - e3_id: pending.e3_id.clone(), - proof_type: ProofType::T0PkBfv, - proof: resp.proof.clone(), - }; - - match SignedProofPayload::sign(payload, signer) { - Ok(signed) => { - info!( - "Signed T0 proof for party {} (signer: {})", - key.party_id, - signer.address() - ); - key.signed_payload = Some(signed); - } - Err(err) => { - error!("Failed to sign T0 proof payload: {err}"); - // Continue without signature — proof still valid, just not attributable - } + // 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; } } @@ -149,17 +127,10 @@ impl ProofRequestActor { }; 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}"); - } + error!( + "ZK proof request failed for E3 {}: {err} — key will not be published without proof", + pending.e3_id + ); } } } diff --git a/crates/zk-prover/src/actors/proof_verification.rs b/crates/zk-prover/src/actors/proof_verification.rs index 42d86dc124..5b209d3784 100644 --- a/crates/zk-prover/src/actors/proof_verification.rs +++ b/crates/zk-prover/src/actors/proof_verification.rs @@ -11,13 +11,13 @@ //! //! ## Signature Verification //! -//! When the received key carries a [`SignedProofPayload`], this actor: -//! 1. Recovers the signer address from the ECDSA signature. +//! 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 -//! so the `FaultSubmitter` can submit a slash proposal on-chain. +//! and [`E3Failed`] to stop the E3 computation. //! -//! This is a CORE actor - it delegates IO operations (verification) to ZkActor. +//! Keys without a signed proof are rejected outright. use std::collections::HashMap; use std::sync::Arc; @@ -25,9 +25,9 @@ use std::sync::Arc; use actix::{Actor, Addr, AsyncContext, Context, Handler, Message, Recipient}; use alloy::primitives::Address; use e3_events::{ - BusHandle, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCreated, - EncryptionKeyReceived, Event, EventPublisher, EventSubscriber, EventType, Proof, - SignedProofFailed, SignedProofPayload, + 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}; @@ -55,24 +55,25 @@ pub struct ZkVerificationResponse { /// Tracks a pending verification including the signed payload for fault evidence. #[derive(Clone, Debug)] struct PendingVerification { - signed_payload: Option, - recovered_signer: Option
, + signed_payload: SignedProofPayload, + recovered_signer: Address, } /// Core actor that handles encryption key verification. /// -/// On ZK verification failure, if the key carried a valid [`SignedProofPayload`], -/// emits a [`SignedProofFailed`] event with the signed evidence bundle. +/// 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: Option>, + 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: Option>) -> Self { + pub fn new(bus: &BusHandle, verifier: Recipient) -> Self { Self { bus: bus.clone(), verifier, @@ -80,67 +81,57 @@ impl ProofVerificationActor { } } - pub fn setup( - bus: &BusHandle, - verifier: Option>, - ) -> Addr { + 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 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!( + error!( "External key from party {} is missing T0 proof - rejecting", msg.key.party_id ); return; }; - // Validate the signed payload if present - let (signed_payload, recovered_signer) = if let Some(ref signed) = msg.key.signed_payload { - match signed.recover_signer() { - Ok(addr) => { - info!( - "Recovered signer {} for key from party {}", - addr, msg.key.party_id - ); - (Some(signed.clone()), Some(addr)) - } - Err(err) => { - warn!( - "Invalid signature on key from party {} - proceeding without \ - fault attribution: {err}", - msg.key.party_id - ); - (None, None) - } + // 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; } - } else { - warn!( - "Key from party {} has no signed payload - \ - proof verification will proceed but fault attribution unavailable", - msg.key.party_id - ); - (None, None) }; // 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, - recovered_signer, + signed_payload: signed, + recovered_signer: recovered_address, }, ); @@ -151,7 +142,7 @@ impl ProofVerificationActor { sender: ctx.address().recipient(), }; - verifier.do_send(request); + self.verifier.do_send(request); } fn publish_key_created(&self, e3_id: E3id, key: Arc) { @@ -202,36 +193,39 @@ impl Handler for ProofVerificationActor { ); 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: {}", - msg.key.party_id, - msg.error.unwrap_or_else(|| "unknown error".to_string()) + "T0 proof verification FAILED for party {} - rejecting key and stopping E3: {}", + msg.key.party_id, error_msg ); - // If we have a signed payload, emit SignedProofFailed for fault attribution + // Emit SignedProofFailed for fault attribution if let Some(PendingVerification { - signed_payload: Some(signed), - recovered_signer: Some(signer), + signed_payload, + recovered_signer, }) = pending { warn!( - "Emitting SignedProofFailed for party {} (signer: {signer})", + "Emitting SignedProofFailed for party {} (address: {recovered_signer})", msg.key.party_id ); if let Err(err) = self.bus.publish(SignedProofFailed { - e3_id: msg.e3_id, - faulting_node: signer, - proof_type: signed.payload.proof_type, - signed_payload: signed, + 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}"); } - } else { - warn!( - "No signed payload available for party {} - \ - fault cannot be attributed on-chain", - msg.key.party_id - ); + } + + // 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}"); } } } From 86ff4ce580aea7eb58607eb3edd254b5692f2ee4 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Wed, 11 Feb 2026 23:33:28 +0500 Subject: [PATCH 5/9] feat: add zk verification to integration test --- Cargo.lock | 2 + .../src/ciphernode_builder.rs | 28 +++++-- .../ciphernode-builder/src/provider_caches.rs | 5 ++ crates/tests/Cargo.toml | 2 + crates/tests/tests/integration.rs | 83 ++++++++++++++++++- 5 files changed, 113 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89f21a35c1..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", diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index b4304107a5..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,11 +448,13 @@ impl CiphernodeBuilder { share_enc_preset, )); - if let Some(ref backend) = self.zk_backend { - info!("Setting up ZK actors"); - let signer = provider_cache.ensure_signer().await?; - setup_zk_actors(&bus, backend, signer); - } + let backend = self + .zk_backend + .as_ref() + .ok_or_else(|| anyhow::anyhow!("ZK backend is required for threshold keyshare"))?; + info!("Setting up ZK actors"); + let signer = provider_cache.ensure_signer().await?; + setup_zk_actors(&bus, backend, signer); } 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/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..132a3f7d5e 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,89 @@ 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. +/// Uses the system-installed bb binary (symlinked into a temp dir) and +/// the circuit fixtures from the enclave installation. +async fn setup_test_zk_backend() -> Option<(ZkBackend, tempfile::TempDir)> { + let bb = find_bb().await?; + 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"); + + // Create directories + 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(); + + // Symlink the real bb binary + #[cfg(unix)] + std::os::unix::fs::symlink(&bb, &bb_binary).unwrap(); + + // 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()); + Some((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 +318,11 @@ 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 + .expect("bb binary is required for integration tests"); + let nodes = CiphernodeSystemBuilder::new() // Adding 20 total nodes: 5 for committee + 4 buffer = 9 selected, 11 unselected .add_group(1, || async { @@ -258,6 +335,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 +354,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() From e06f3245290e319d4586ddea7d63404535232fdd Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 12 Feb 2026 03:57:02 +0500 Subject: [PATCH 6/9] fix: ensure installed bb integration --- crates/tests/tests/integration.rs | 75 ++++++++++++++++--------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 132a3f7d5e..1acb559b39 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -69,10 +69,9 @@ async fn find_bb() -> Option { } /// Create a ZkBackend for integration tests. -/// Uses the system-installed bb binary (symlinked into a temp dir) and -/// the circuit fixtures from the enclave installation. -async fn setup_test_zk_backend() -> Option<(ZkBackend, tempfile::TempDir)> { - let bb = find_bb().await?; +/// 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"); @@ -80,34 +79,42 @@ async fn setup_test_zk_backend() -> Option<(ZkBackend, tempfile::TempDir)> { let circuits_dir = noir_dir.join("circuits"); let work_dir = noir_dir.join("work").join("test_node"); - // Create directories - 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(); - - // Symlink the real bb binary - #[cfg(unix)] - std::os::unix::fs::symlink(&bb, &bb_binary).unwrap(); - - // 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()); - Some((backend, temp)) + 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(); + + // 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]) { @@ -319,9 +326,7 @@ async fn test_trbfv_actor() -> Result<()> { 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 - .expect("bb binary is required for integration tests"); + 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 From d7401349dd2cad40fdf363a39a82683ac097e754 Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 12 Feb 2026 04:04:39 +0500 Subject: [PATCH 7/9] fix: use abi encode instead of encode packed --- crates/events/src/enclave_event/signed_proof.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index 7dbe1b527a..37f9572f43 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -108,9 +108,9 @@ pub struct ProofPayload { impl ProofPayload { /// Compute the keccak256 digest of the canonical encoding. /// - /// The encoding concatenates all fields as length-prefixed byte arrays - /// preceded by fixed-size scalars, matching the structure the on-chain - /// verifier will reconstruct. + /// 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 @@ -118,7 +118,7 @@ impl ProofPayload { .try_into() .map_err(|_| anyhow!("E3id cannot be converted to U256"))?; - // keccak256(abi.encodePacked(chainId, e3Id, proofType, proof, publicSignals)) + // keccak256(abi.encode(chainId, e3Id, proofType, proof, publicSignals)) let encoded = ( U256::from(self.e3_id.chain_id()), e3_id_u256, @@ -126,7 +126,7 @@ impl ProofPayload { Bytes::copy_from_slice(&self.proof.data), Bytes::copy_from_slice(&self.proof.public_signals), ) - .abi_encode_packed(); + .abi_encode(); Ok(keccak256(&encoded).into()) } From 53668447340a3664c0480a71f8dc5878a224af9a Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 12 Feb 2026 04:42:05 +0500 Subject: [PATCH 8/9] fix: comments --- crates/tests/tests/integration.rs | 2 ++ crates/zk-prover/src/actors/mod.rs | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 1acb559b39..cfdd240155 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -88,6 +88,8 @@ async fn setup_test_zk_backend() -> (ZkBackend, tempfile::TempDir) { #[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")) diff --git a/crates/zk-prover/src/actors/mod.rs b/crates/zk-prover/src/actors/mod.rs index 2c334f87d8..235cb5fe16 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, &backend); +//! setup_zk_actors(&bus, &backend, signer); //! ``` pub mod proof_request; @@ -50,7 +52,11 @@ use crate::ZkBackend; /// /// 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 { +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(); From 358b0c0c487a76252ea6da49478d8ceb8a12cd7f Mon Sep 17 00:00:00 2001 From: Hamza Khalid Date: Thu, 12 Feb 2026 05:05:23 +0500 Subject: [PATCH 9/9] chore: lint code --- crates/events/src/enclave_event/signed_proof.rs | 2 +- crates/zk-prover/src/actors/mod.rs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index 37f9572f43..690805c1ad 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -92,7 +92,7 @@ impl fmt::Display for ProofType { /// /// Only contains data needed for on-chain fault verification: /// the E3 identifier, proof type, and the ZK proof itself. -/// Encoded via `abi.encodePacked(chainId, e3Id, proofType, proof, publicSignals)` +/// 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)] diff --git a/crates/zk-prover/src/actors/mod.rs b/crates/zk-prover/src/actors/mod.rs index 235cb5fe16..4a589ac764 100644 --- a/crates/zk-prover/src/actors/mod.rs +++ b/crates/zk-prover/src/actors/mod.rs @@ -52,11 +52,7 @@ use crate::ZkBackend; /// /// 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 { +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();