diff --git a/Cargo.lock b/Cargo.lock index f5da183ccd..ac888f026e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3501,6 +3501,7 @@ dependencies = [ "actix", "anyhow", "bincode 1.3.3", + "e3-bfv-client", "e3-crypto", "e3-data", "e3-events", @@ -3511,6 +3512,7 @@ dependencies = [ "e3-zk-helpers", "e3-zk-prover", "fhe", + "fhe-math", "fhe-traits", "ndarray", "num-bigint", diff --git a/crates/aggregator/src/ext.rs b/crates/aggregator/src/ext.rs index 263fe5b55c..90c3ee9657 100644 --- a/crates/aggregator/src/ext.rs +++ b/crates/aggregator/src/ext.rs @@ -147,13 +147,19 @@ fn create_publickey_aggregator( pub struct ThresholdPlaintextAggregatorExtension { bus: BusHandle, sortition: Addr, + params_preset: BfvPreset, } impl ThresholdPlaintextAggregatorExtension { - pub fn create(bus: &BusHandle, sortition: &Addr) -> Box { + pub fn create( + bus: &BusHandle, + sortition: &Addr, + params_preset: BfvPreset, + ) -> Box { Box::new(Self { bus: bus.clone(), sortition: sortition.clone(), + params_preset, }) } } @@ -194,6 +200,7 @@ impl E3Extension for ThresholdPlaintextAggregatorExtension { bus: self.bus.clone(), sortition: self.sortition.clone(), e3_id: e3_id.clone(), + params_preset: self.params_preset.clone(), }, sync_state, ) @@ -222,6 +229,7 @@ impl E3Extension for ThresholdPlaintextAggregatorExtension { bus: self.bus.clone(), sortition: self.sortition.clone(), e3_id: ctx.e3_id.clone(), + params_preset: self.params_preset.clone(), }, sync_state, ) diff --git a/crates/aggregator/src/publickey_aggregator.rs b/crates/aggregator/src/publickey_aggregator.rs index dffe6bad5d..f4dad40569 100644 --- a/crates/aggregator/src/publickey_aggregator.rs +++ b/crates/aggregator/src/publickey_aggregator.rs @@ -9,11 +9,11 @@ use anyhow::Result; use e3_bfv_client::client::compute_pk_commitment; use e3_data::Persistable; use e3_events::{ - prelude::*, BusHandle, ComputeRequest, ComputeResponse, ComputeResponseKind, CorrelationId, - Die, E3id, EnclaveEvent, EnclaveEventData, EventContext, KeyshareCreated, OrderedSet, - PartyProofsToVerify, PkAggregationProofRequest, PkAggregationProofResponse, - PublicKeyAggregated, Seed, Sequenced, SignedProofPayload, TypedEvent, VerifyShareProofsRequest, - VerifyShareProofsResponse, ZkRequest, ZkResponse, + prelude::*, BusHandle, Die, E3id, EnclaveEvent, EnclaveEventData, EventContext, + KeyshareCreated, OrderedSet, PartyProofsToVerify, PkAggregationProofPending, + PkAggregationProofRequest, PkAggregationProofSigned, PublicKeyAggregated, Seed, Sequenced, + ShareVerificationComplete, ShareVerificationDispatched, SignedProofPayload, TypedEvent, + VerificationKind, }; use e3_events::{trap, EType}; use e3_fhe::{Fhe, GetAggregatePublicKey}; @@ -195,47 +195,49 @@ impl PublicKeyAggregator { no_proof_parties.len() ); - let request = ComputeRequest::zk( - ZkRequest::VerifyShareProofs(VerifyShareProofsRequest { party_proofs }), - CorrelationId::new(), - self.e3_id.clone(), - ); - self.bus.publish(request, ec)?; + self.bus.publish( + ShareVerificationDispatched { + e3_id: self.e3_id.clone(), + kind: VerificationKind::PkGenerationProofs, + share_proofs: party_proofs, + decryption_proofs: vec![], + pre_dishonest: no_proof_parties.into_iter().collect(), + }, + ec, + )?; Ok(()) } - fn handle_c1_verification_response( + fn handle_c1_verification_complete( &mut self, - response: VerifyShareProofsResponse, - ec: EventContext, + msg: TypedEvent, ) -> Result<()> { + let (msg, ec) = msg.into_components(); + + if msg.kind != VerificationKind::PkGenerationProofs { + return Ok(()); + } + + if msg.e3_id != self.e3_id { + return Ok(()); + } + let PublicKeyAggregatorState::VerifyingC1 { keyshares, nodes, threshold_m, - no_proof_parties, + .. } = self .state .get() .ok_or_else(|| anyhow::anyhow!("Expected VerifyingC1 state"))? else { return Err(anyhow::anyhow!( - "handle_c1_verification_response called outside VerifyingC1 state" + "handle_c1_verification_complete called outside VerifyingC1 state" )); }; - // Partition honest/dishonest parties — include those that failed verification - // AND those that submitted no C1 proof at all - let mut dishonest_parties: Vec = no_proof_parties; - for result in &response.party_results { - if !result.all_verified { - warn!( - "Party {} failed C1 proof verification (failed payload: {:?})", - result.sender_party_id, result.failed_signed_payload - ); - dishonest_parties.push(result.sender_party_id); - } - } + let dishonest_parties = &msg.dishonest_parties; // Filter out dishonest parties from keyshares and nodes let keyshares_vec: Vec = keyshares.into_iter().collect(); @@ -257,12 +259,12 @@ impl PublicKeyAggregator { ); } - // Check remaining count >= threshold - if honest_keyshares.len() < threshold_m { + // Need at least threshold + 1 honest parties for aggregation + if honest_keyshares.len() <= threshold_m { return Err(anyhow::anyhow!( - "Not enough honest parties after C1 verification: {} < {}", + "Not enough honest parties after C1 verification: {} (need at least {})", honest_keyshares.len(), - threshold_m + threshold_m + 1 )); } @@ -285,41 +287,54 @@ impl PublicKeyAggregator { let honest_nodes_set = OrderedSet::from(honest_nodes); + // Publish pending event before transitioning state so a publish + // failure leaves us in VerifyingC1 (retryable) rather than + // GeneratingC5Proof (no retry path). + let committee_h = honest_keyshares.len(); + + info!("Publishing PkAggregationProofPending for C5 proof generation..."); + self.bus.publish( + PkAggregationProofPending { + e3_id: self.e3_id.clone(), + proof_request: PkAggregationProofRequest { + keyshare_bytes: honest_keyshares.clone(), + aggregated_pk_bytes: ArcBytes::from_bytes(&pubkey), + params_preset: self.params_preset.clone(), + // this field is not really used in the circuit, we only use H + committee_n: committee_h, + committee_h, + // this field is not really used in the circuit, we only use H + committee_threshold: 0, + }, + public_key: pubkey.clone(), + public_key_hash, + nodes: honest_nodes_set.clone(), + }, + ec.clone(), + )?; + // Transition to GeneratingC5Proof self.state.try_mutate(&ec, |_| { Ok(PublicKeyAggregatorState::GeneratingC5Proof { - public_key: pubkey.clone(), + public_key: pubkey, public_key_hash, - keyshare_bytes: honest_keyshares.clone(), - nodes: honest_nodes_set.clone(), + keyshare_bytes: honest_keyshares, + nodes: honest_nodes_set, }) })?; - - // Dispatch C5 proof request - let committee_h = honest_keyshares.len(); - - info!("Dispatching C5 proof generation (pk aggregation)..."); - let request = ComputeRequest::zk( - ZkRequest::PkAggregation(PkAggregationProofRequest { - keyshare_bytes: honest_keyshares, - aggregated_pk_bytes: ArcBytes::from_bytes(&pubkey), - params_preset: self.params_preset.clone(), - committee_n: committee_h, // N = all parties in this aggregation - committee_h, - committee_threshold: 0, // Will be resolved from preset in handler - }), - CorrelationId::new(), - self.e3_id.clone(), - ); - self.bus.publish(request, ec)?; Ok(()) } - fn handle_c5_proof_response( + fn handle_pk_aggregation_proof_signed( &mut self, - response: PkAggregationProofResponse, - ec: EventContext, + msg: TypedEvent, ) -> Result<()> { + let (msg, ec) = msg.into_components(); + + if msg.e3_id != self.e3_id { + return Ok(()); + } + let PublicKeyAggregatorState::GeneratingC5Proof { public_key, public_key_hash, @@ -331,50 +346,36 @@ impl PublicKeyAggregator { .ok_or_else(|| anyhow::anyhow!("Expected GeneratingC5Proof state"))? else { return Err(anyhow::anyhow!( - "handle_c5_proof_response called outside GeneratingC5Proof state" + "handle_pk_aggregation_proof_signed called outside GeneratingC5Proof state" )); }; - info!("C5 proof generated, publishing PublicKeyAggregated..."); + info!("C5 proof signed, publishing PublicKeyAggregated..."); - let proof = response.proof; + let proof = msg.signed_proof.payload.proof; + + // Publish PublicKeyAggregated before transitioning state so a publish + // failure leaves us in GeneratingC5Proof (retryable) rather than + // Complete (no retry path). + let event = PublicKeyAggregated { + pubkey: public_key.clone(), + public_key_hash, + e3_id: self.e3_id.clone(), + nodes: nodes.clone(), + pk_aggregation_proof: Some(proof), + }; + self.bus.publish(event, ec.clone())?; // Transition to Complete self.state.try_mutate(&ec, |_| { Ok(PublicKeyAggregatorState::Complete { - public_key: public_key.clone(), + public_key, keyshares: OrderedSet::new(), - nodes: nodes.clone(), + nodes, }) })?; - - // Publish PublicKeyAggregated with C5 proof - let event = PublicKeyAggregated { - pubkey: public_key, - public_key_hash, - e3_id: self.e3_id.clone(), - nodes, - pk_aggregation_proof: Some(proof), - }; - self.bus.publish(event, ec)?; Ok(()) } - - fn handle_compute_response( - &mut self, - response: ComputeResponse, - ec: EventContext, - ) -> Result<()> { - match response.response { - ComputeResponseKind::Zk(ZkResponse::VerifyShareProofs(data)) => { - self.handle_c1_verification_response(data, ec) - } - ComputeResponseKind::Zk(ZkResponse::PkAggregation(data)) => { - self.handle_c5_proof_response(data, ec) - } - _ => Ok(()), // Ignore other compute responses - } - } } impl Actor for PublicKeyAggregator { @@ -392,7 +393,10 @@ impl Handler for PublicKeyAggregator { EnclaveEventData::KeyshareCreated(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } - EnclaveEventData::ComputeResponse(data) => { + EnclaveEventData::ShareVerificationComplete(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::PkAggregationProofSigned(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } EnclaveEventData::ComputeRequestError(data) => { @@ -451,18 +455,35 @@ impl Handler> for PublicKeyAggregator { } } -impl Handler> for PublicKeyAggregator { +impl Handler> for PublicKeyAggregator { type Result = (); fn handle( &mut self, - msg: TypedEvent, + msg: TypedEvent, _ctx: &mut Self::Context, ) -> Self::Result { - let (msg, ec) = msg.into_components(); - trap(EType::PublickeyAggregation, &self.bus.with_ec(&ec), || { - self.handle_compute_response(msg, ec) - }) + trap( + EType::PublickeyAggregation, + &self.bus.with_ec(msg.get_ctx()), + || self.handle_c1_verification_complete(msg), + ) + } +} + +impl Handler> for PublicKeyAggregator { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + trap( + EType::PublickeyAggregation, + &self.bus.with_ec(msg.get_ctx()), + || self.handle_pk_aggregation_proof_signed(msg), + ) } } diff --git a/crates/aggregator/src/threshold_plaintext_aggregator.rs b/crates/aggregator/src/threshold_plaintext_aggregator.rs index 13aad3b4db..4ce3ff0d84 100644 --- a/crates/aggregator/src/threshold_plaintext_aggregator.rs +++ b/crates/aggregator/src/threshold_plaintext_aggregator.rs @@ -4,16 +4,19 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use actix::prelude::*; use anyhow::{anyhow, bail, ensure, Result}; use e3_data::Persistable; use e3_events::{ - prelude::*, trap, BusHandle, ComputeRequest, ComputeResponse, ComputeResponseKind, - CorrelationId, DecryptionshareCreated, Die, E3id, EType, EnclaveEvent, EnclaveEventData, - EventContext, PlaintextAggregated, Seed, Sequenced, TypedEvent, + prelude::*, trap, AggregationProofPending, AggregationProofSigned, BusHandle, ComputeRequest, + ComputeResponse, ComputeResponseKind, CorrelationId, DecryptedSharesAggregationProofRequest, + DecryptionshareCreated, Die, E3id, EType, EnclaveEvent, EnclaveEventData, EventContext, + PartyProofsToVerify, PlaintextAggregated, Seed, Sequenced, ShareVerificationComplete, + ShareVerificationDispatched, SignedProofPayload, TypedEvent, VerificationKind, }; +use e3_fhe_params::BfvPreset; use e3_sortition::{E3CommitteeContainsRequest, E3CommitteeContainsResponse, Sortition}; use e3_trbfv::{ calculate_threshold_decryption::CalculateThresholdDecryptionRequest, TrBFVConfig, TrBFVRequest, @@ -21,18 +24,29 @@ use e3_trbfv::{ }; use e3_utils::NotifySync; use e3_utils::{utility_types::ArcBytes, MAILBOX_LIMIT}; -use tracing::{debug, info, trace}; +use tracing::{debug, info, trace, warn}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Collecting { threshold_m: u64, threshold_n: u64, shares: HashMap>, + c6_proofs: HashMap>, seed: Seed, ciphertext_output: Vec, params: ArcBytes, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct VerifyingC6 { + threshold_m: u64, + threshold_n: u64, + shares: HashMap>, + c6_proofs: HashMap>, + ciphertext_output: Vec, + params: ArcBytes, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Computing { threshold_m: u64, @@ -42,6 +56,14 @@ pub struct Computing { params: ArcBytes, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GeneratingC7Proof { + threshold_m: u64, + threshold_n: u64, + shares: Vec<(u64, Vec)>, + plaintext: Vec, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Complete { decrypted: Vec, @@ -51,7 +73,9 @@ pub struct Complete { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum ThresholdPlaintextAggregatorState { Collecting(Collecting), + VerifyingC6(VerifyingC6), Computing(Computing), + GeneratingC7Proof(GeneratingC7Proof), Complete(Complete), } @@ -67,6 +91,18 @@ impl TryFrom for Collecting { } } +impl TryFrom for VerifyingC6 { + type Error = anyhow::Error; + fn try_from( + value: ThresholdPlaintextAggregatorState, + ) -> std::result::Result { + match value { + ThresholdPlaintextAggregatorState::VerifyingC6(s) => Ok(s), + _ => bail!("Inconsistent state: expected VerifyingC6"), + } + } +} + impl TryFrom for Computing { type Error = anyhow::Error; fn try_from( @@ -74,7 +110,19 @@ impl TryFrom for Computing { ) -> std::result::Result { match value { ThresholdPlaintextAggregatorState::Computing(s) => Ok(s), - _ => bail!("Inconsistent state"), + _ => bail!("Inconsistent state: expected Computing"), + } + } +} + +impl TryFrom for GeneratingC7Proof { + type Error = anyhow::Error; + fn try_from( + value: ThresholdPlaintextAggregatorState, + ) -> std::result::Result { + match value { + ThresholdPlaintextAggregatorState::GeneratingC7Proof(s) => Ok(s), + _ => bail!("Inconsistent state: expected GeneratingC7Proof"), } } } @@ -86,7 +134,7 @@ impl TryFrom for Complete { ) -> std::result::Result { match value { ThresholdPlaintextAggregatorState::Complete(s) => Ok(s), - _ => bail!("Inconsistent state"), + _ => bail!("Inconsistent state: expected Complete"), } } } @@ -103,6 +151,7 @@ impl ThresholdPlaintextAggregatorState { threshold_m, threshold_n, shares: HashMap::new(), + c6_proofs: HashMap::new(), seed, ciphertext_output, params, @@ -110,19 +159,11 @@ impl ThresholdPlaintextAggregatorState { } } -#[derive(Message)] -#[rtype("()")] -pub struct ComputeAggregate { - pub shares: Vec<(u64, Vec)>, - pub ciphertext_output: Vec, - pub threshold_m: u64, - pub threshold_n: u64, -} - pub struct ThresholdPlaintextAggregator { bus: BusHandle, sortition: Addr, e3_id: E3id, + params_preset: BfvPreset, state: Persistable, } @@ -130,6 +171,7 @@ pub struct ThresholdPlaintextAggregatorParams { pub bus: BusHandle, pub sortition: Addr, pub e3_id: E3id, + pub params_preset: BfvPreset, } impl ThresholdPlaintextAggregator { @@ -141,6 +183,7 @@ impl ThresholdPlaintextAggregator { bus: params.bus, sortition: params.sortition, e3_id: params.e3_id, + params_preset: params.params_preset, state, } } @@ -149,6 +192,7 @@ impl ThresholdPlaintextAggregator { &mut self, party_id: u64, share: Vec, + decryption_proofs: Vec, ec: &EventContext, ) -> Result<()> { self.state.try_mutate(ec, |state| { @@ -159,111 +203,286 @@ impl ThresholdPlaintextAggregator { let threshold_n = current.threshold_n; let params = current.params.clone(); let mut shares = current.shares; + let mut c6_proofs = current.c6_proofs; info!("pushing to share collection {} {:?}", party_id, share); shares.insert(party_id, share); + c6_proofs.insert(party_id, decryption_proofs); - if shares.len() <= threshold_m as usize { + if (shares.len() as u64) < threshold_n { return Ok(ThresholdPlaintextAggregatorState::Collecting(Collecting { params, threshold_n, threshold_m, ciphertext_output, shares, + c6_proofs, seed: current.seed, })); } - info!("Changing state to computing because received enough shares..."); + info!( + "Changing state to VerifyingC6 because received all {} shares...", + threshold_n + ); - Ok(ThresholdPlaintextAggregatorState::Computing(Computing { - shares: shares.into_iter().collect(), - ciphertext_output, - threshold_m, - threshold_n, - params, - })) + Ok(ThresholdPlaintextAggregatorState::VerifyingC6( + VerifyingC6 { + shares, + c6_proofs, + ciphertext_output, + threshold_m, + threshold_n, + params, + }, + )) }) } - pub fn set_decryption( + /// Dispatch C6 proof verification through ShareVerificationActor. + pub fn dispatch_c6_verification( &mut self, - decrypted: Vec, - ec: &EventContext, + c6_proofs: HashMap>, + ec: EventContext, ) -> Result<()> { - self.state.try_mutate(ec, |mut state| { - let ThresholdPlaintextAggregatorState::Computing(Computing { shares, .. }) = &mut state - else { - return Ok(state.clone()); - }; - let shares = shares.to_owned(); - - Ok(ThresholdPlaintextAggregatorState::Complete(Complete { - decrypted, - shares, - })) - }) + let party_proofs: Vec = c6_proofs + .into_iter() + .map(|(party_id, signed_proofs)| PartyProofsToVerify { + sender_party_id: party_id, + signed_proofs, + }) + .collect(); + + self.bus.publish( + ShareVerificationDispatched { + e3_id: self.e3_id.clone(), + kind: VerificationKind::ThresholdDecryptionProofs, + share_proofs: party_proofs, + decryption_proofs: vec![], + pre_dishonest: BTreeSet::new(), + }, + ec, + )?; + Ok(()) } - pub fn handle_compute_aggregate(&mut self, msg: TypedEvent) -> Result<()> { + /// Handle ShareVerificationComplete for C6: filter dishonest parties, transition to Computing. + pub fn handle_c6_verification_complete( + &mut self, + msg: TypedEvent, + ) -> Result<()> { let (msg, ec) = msg.into_components(); - info!("create_calculate_threshold_decryption_event..."); - let e3_id = self.e3_id.clone(); - let state: Computing = self + if msg.kind != VerificationKind::ThresholdDecryptionProofs { + return Ok(()); + } + + if msg.e3_id != self.e3_id { + return Ok(()); + } + + let state: VerifyingC6 = self .state .get() .ok_or(anyhow!("Could not get state"))? .try_into()?; + let dishonest_parties = &msg.dishonest_parties; + if !dishonest_parties.is_empty() { + warn!( + "C6 verification: {} dishonest parties filtered: {:?}", + dishonest_parties.len(), + dishonest_parties + ); + } + + // Filter shares to only honest parties + let honest_shares: Vec<(u64, Vec)> = state + .shares + .iter() + .filter(|(id, _)| !dishonest_parties.contains(id)) + .map(|(id, s)| (*id, s.clone())) + .collect(); + + ensure!( + honest_shares.len() > state.threshold_m as usize, + "Not enough honest shares after C6 verification: {} honest shares, {} required", + honest_shares.len(), + state.threshold_m + 1 + ); + + info!( + "C6 verification passed: {} honest parties, transitioning to Computing", + honest_shares.len() + ); + + // Publish ComputeRequest before transitioning state so a publish + // failure leaves us in VerifyingC6 (retryable) rather than + // Computing (no retry path). let trbfv_config = TrBFVConfig::new(state.params.clone(), state.threshold_n, state.threshold_m); let event = ComputeRequest::trbfv( TrBFVRequest::CalculateThresholdDecryption( CalculateThresholdDecryptionRequest { - ciphertexts: msg.ciphertext_output, + ciphertexts: state.ciphertext_output.clone(), trbfv_config, - d_share_polys: msg.shares, + d_share_polys: honest_shares.clone(), } .into(), ), CorrelationId::new(), - e3_id, + self.e3_id.clone(), ); - self.bus.publish(event, ec)?; + self.bus.publish(event, ec.clone())?; + + self.state.try_mutate(&ec, |_| { + Ok(ThresholdPlaintextAggregatorState::Computing(Computing { + shares: honest_shares, + ciphertext_output: state.ciphertext_output, + threshold_m: state.threshold_m, + threshold_n: state.threshold_n, + params: state.params, + })) + })?; + Ok(()) } - pub fn handle_compute_response(&mut self, msg: TypedEvent) -> Result<()> { + /// Publish AggregationProofPending for C7 proof generation through ProofRequestActor. + pub fn dispatch_c7_proof_request( + &mut self, + shares: Vec<(u64, Vec)>, + plaintext: Vec, + threshold_m: u64, + threshold_n: u64, + ec: EventContext, + ) -> Result<()> { + self.bus.publish( + AggregationProofPending { + e3_id: self.e3_id.clone(), + proof_request: DecryptedSharesAggregationProofRequest { + d_share_polys: shares.clone(), + plaintext: plaintext.clone(), + params_preset: self.params_preset.clone(), + threshold_m, + threshold_n, + }, + plaintext, + shares, + }, + ec, + )?; + Ok(()) + } + + /// Handle AggregationProofSigned: transition to Complete and publish PlaintextAggregated. + pub fn handle_aggregation_proof_signed( + &mut self, + msg: TypedEvent, + ) -> Result<()> { let (msg, ec) = msg.into_components(); - ensure!( - msg.e3_id == self.e3_id, - "PlaintextAggregator should never receive incorrect e3_id msgs" - ); - let ComputeResponseKind::TrBFV(TrBFVResponse::CalculateThresholdDecryption(response)) = - msg.response - else { - // Must be another compute response so ignoring + if msg.e3_id != self.e3_id { return Ok(()); - }; + } - info!("Received response {:?}", response); + let state: GeneratingC7Proof = self + .state + .get() + .ok_or(anyhow!("Could not get state"))? + .try_into()?; - // Update the local state - let plaintext = response.plaintext; + let plaintext = state.plaintext.clone(); + let shares = state.shares.clone(); - self.set_decryption(plaintext.clone(), &ec)?; + // Extract raw proofs from signed payloads for PlaintextAggregated + let proofs: Vec<_> = msg + .signed_proofs + .iter() + .map(|sp| sp.payload.proof.clone()) + .collect(); - // Dispatch the PlaintextAggregated event + ensure!( + proofs.len() == plaintext.len(), + "C7 proof count mismatch: got {} proofs for {} ciphertext indices", + proofs.len(), + plaintext.len() + ); + + // Publish PlaintextAggregated before transitioning state so a publish + // failure leaves us in GeneratingC7Proof (retryable) rather than + // Complete (no retry path). let event = PlaintextAggregated { - decrypted_output: plaintext, // Extracting here for now + decrypted_output: plaintext.clone(), e3_id: self.e3_id.clone(), + aggregation_proofs: proofs, }; - info!("Dispatching plaintext event {:?}", event); - self.bus.publish(event, ec)?; + info!("Dispatching plaintext event with C7 proofs {:?}", event); + self.bus.publish(event, ec.clone())?; + + self.state.try_mutate(&ec, |_| { + Ok(ThresholdPlaintextAggregatorState::Complete(Complete { + decrypted: plaintext, + shares, + })) + })?; + Ok(()) + } + + pub fn handle_compute_response(&mut self, msg: TypedEvent) -> Result<()> { + let (msg, ec) = msg.into_components(); + ensure!( + msg.e3_id == self.e3_id, + "PlaintextAggregator should never receive incorrect e3_id msgs" + ); + + match msg.response { + // TrBFV threshold decryption response -> transition to GeneratingC7Proof + ComputeResponseKind::TrBFV(TrBFVResponse::CalculateThresholdDecryption(response)) => { + info!("Received TrBFV threshold decryption response"); + let plaintext = response.plaintext; + + let state: Computing = self + .state + .get() + .ok_or(anyhow!("Could not get state"))? + .try_into()?; + + let shares = state.shares.clone(); + let threshold_m = state.threshold_m; + let threshold_n = state.threshold_n; + + // Publish pending event before transitioning state so a publish + // failure leaves us in Computing (retryable) rather than + // GeneratingC7Proof (no retry path). + self.dispatch_c7_proof_request( + shares.clone(), + plaintext.clone(), + threshold_m, + threshold_n, + ec.clone(), + )?; + + // Transition to GeneratingC7Proof + self.state.try_mutate(&ec, |_| { + Ok(ThresholdPlaintextAggregatorState::GeneratingC7Proof( + GeneratingC7Proof { + threshold_m, + threshold_n, + shares, + plaintext, + }, + )) + })?; + } + + _ => { + // Not a response we handle — ignore + } + } + Ok(()) } } @@ -285,6 +504,12 @@ impl Handler for ThresholdPlaintextAggregator { EnclaveEventData::ComputeResponse(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::ShareVerificationComplete(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::AggregationProofSigned(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } _ => (), } } @@ -324,7 +549,7 @@ impl Handler>> fn handle( &mut self, msg: E3CommitteeContainsResponse>, - ctx: &mut Self::Context, + _ctx: &mut Self::Context, ) -> Self::Result { trap( EType::PublickeyAggregation, @@ -346,58 +571,65 @@ impl Handler>> DecryptionshareCreated { party_id, decryption_share, + signed_decryption_proofs, .. }, ec, ) = msg.into_inner().into_components(); - self.add_share(party_id, decryption_share, &ec)?; + self.add_share(party_id, decryption_share, signed_decryption_proofs, &ec)?; - if let Some(ThresholdPlaintextAggregatorState::Computing(Computing { - threshold_m, - threshold_n, - shares, - ciphertext_output, - .. - })) = self.state.get() + // If we transitioned to VerifyingC6, dispatch C6 verification + // using the proofs persisted in state + if let Some(ThresholdPlaintextAggregatorState::VerifyingC6(ref state)) = + self.state.get() { - self.notify_sync( - ctx, - TypedEvent::new( - ComputeAggregate { - shares: shares.clone(), - ciphertext_output: ciphertext_output.clone(), - threshold_m, - threshold_n, - }, - ec, - ), - ) + self.dispatch_c6_verification(state.c6_proofs.clone(), ec)?; } + Ok(()) }, ) } } -impl Handler> for ThresholdPlaintextAggregator { +impl Handler> for ThresholdPlaintextAggregator { type Result = (); - fn handle(&mut self, msg: TypedEvent, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: TypedEvent, _: &mut Self::Context) -> Self::Result { trap( EType::PlaintextAggregation, &self.bus.with_ec(msg.get_ctx()), - || self.handle_compute_aggregate(msg), + || self.handle_compute_response(msg), ) } } -impl Handler> for ThresholdPlaintextAggregator { +impl Handler> for ThresholdPlaintextAggregator { type Result = (); - fn handle(&mut self, msg: TypedEvent, _: &mut Self::Context) -> Self::Result { + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { trap( EType::PlaintextAggregation, &self.bus.with_ec(msg.get_ctx()), - || self.handle_compute_response(msg), + || self.handle_c6_verification_complete(msg), + ) + } +} + +impl Handler> for ThresholdPlaintextAggregator { + type Result = (); + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + trap( + EType::PlaintextAggregation, + &self.bus.with_ec(msg.get_ctx()), + || self.handle_aggregation_proof_signed(msg), ) } } diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 598f65df4a..1bd273cede 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -472,14 +472,27 @@ impl CiphernodeBuilder { e3_builder = e3_builder.with(PublicKeyAggregatorExtension::create( &bus, aggregator_preset, - )) + )); + + if self.keyshare.is_none() { + let backend = self + .zk_backend + .as_ref() + .ok_or_else(|| anyhow::anyhow!("ZK backend is required for aggregator"))?; + let signer = provider_cache.ensure_signer().await?; + info!("Setting up ZK actors for aggregator"); + setup_zk_actors(&bus, backend, signer); + } } if self.threshold_plaintext_agg { info!("Setting up ThresholdPlaintextAggregatorExtension"); let _ = self.ensure_multithread(&bus); + let aggregator_preset = DEFAULT_BFV_PRESET; e3_builder = e3_builder.with(ThresholdPlaintextAggregatorExtension::create( - &bus, &sortition, + &bus, + &sortition, + aggregator_preset, )) } diff --git a/crates/events/src/enclave_event/aggregation_proof_pending.rs b/crates/events/src/enclave_event/aggregation_proof_pending.rs new file mode 100644 index 0000000000..fbceb1671e --- /dev/null +++ b/crates/events/src/enclave_event/aggregation_proof_pending.rs @@ -0,0 +1,25 @@ +// 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. + +//! Event for C7 proof generation through ProofRequestActor. +//! +//! `AggregationProofPending` is published by [`ThresholdPlaintextAggregator`] +//! after TrBFV threshold decryption completes. +//! `ProofRequestActor` generates the C7 proof(s), signs them, and publishes +//! `AggregationProofSigned`. + +use crate::{DecryptedSharesAggregationProofRequest, E3id}; +use e3_utils::utility_types::ArcBytes; +use serde::{Deserialize, Serialize}; + +/// ThresholdPlaintextAggregator -> ProofRequestActor: generate and sign C7 proofs. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct AggregationProofPending { + pub e3_id: E3id, + pub proof_request: DecryptedSharesAggregationProofRequest, + pub plaintext: Vec, + pub shares: Vec<(u64, Vec)>, +} diff --git a/crates/events/src/enclave_event/aggregation_proof_signed.rs b/crates/events/src/enclave_event/aggregation_proof_signed.rs new file mode 100644 index 0000000000..559a801a1a --- /dev/null +++ b/crates/events/src/enclave_event/aggregation_proof_signed.rs @@ -0,0 +1,21 @@ +// 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. + +//! Event for C7 proof signing completion. +//! +//! `AggregationProofSigned` is published by [`ProofRequestActor`] after +//! generating and ECDSA-signing the C7 proofs. [`ThresholdPlaintextAggregator`] +//! consumes this to transition to Complete and publish `PlaintextAggregated`. + +use crate::{E3id, SignedProofPayload}; +use serde::{Deserialize, Serialize}; + +/// ProofRequestActor -> ThresholdPlaintextAggregator: signed C7 proofs. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct AggregationProofSigned { + pub e3_id: E3id, + pub signed_proofs: Vec, +} diff --git a/crates/events/src/enclave_event/compute_request/mod.rs b/crates/events/src/enclave_event/compute_request/mod.rs index c307d6e4d6..3d8efabc23 100644 --- a/crates/events/src/enclave_event/compute_request/mod.rs +++ b/crates/events/src/enclave_event/compute_request/mod.rs @@ -89,6 +89,8 @@ impl ToString for ComputeRequest { ZkRequest::VerifyShareProofs(_) => "ZkVerifyShareProofs", ZkRequest::VerifyShareDecryptionProofs(_) => "ZkVerifyShareDecryptionProofs", ZkRequest::PkAggregation(_) => "ZkPkAggregation", + ZkRequest::ThresholdShareDecryption(_) => "ZkThresholdShareDecryption", + ZkRequest::DecryptedSharesAggregation(_) => "ZkDecryptedSharesAggregation", }, } .to_string() diff --git a/crates/events/src/enclave_event/compute_request/zk.rs b/crates/events/src/enclave_event/compute_request/zk.rs index 588e39b9dd..9258b46187 100644 --- a/crates/events/src/enclave_event/compute_request/zk.rs +++ b/crates/events/src/enclave_event/compute_request/zk.rs @@ -32,6 +32,10 @@ pub enum ZkRequest { VerifyShareDecryptionProofs(VerifyShareDecryptionProofsRequest), /// Generate proof for public key aggregation (C5). PkAggregation(PkAggregationProofRequest), + /// Generate proof(s) for threshold share decryption (C6). + ThresholdShareDecryption(ThresholdShareDecryptionProofRequest), + /// Generate proof for decrypted shares aggregation (C7). + DecryptedSharesAggregation(DecryptedSharesAggregationProofRequest), } /// Request to generate a proof for public key aggregation (C5). @@ -199,6 +203,10 @@ pub enum ZkResponse { VerifyShareDecryptionProofs(VerifyShareDecryptionProofsResponse), /// Proof for public key aggregation (C5). PkAggregation(PkAggregationProofResponse), + /// Proof(s) for threshold share decryption (C6). + ThresholdShareDecryption(ThresholdShareDecryptionProofResponse), + /// Proof for decrypted shares aggregation (C7). + DecryptedSharesAggregation(DecryptedSharesAggregationProofResponse), } /// Response containing a generated proof for public key aggregation (C5). @@ -207,6 +215,32 @@ pub struct PkAggregationProofResponse { pub proof: Proof, } +/// Request to generate proof(s) of correct threshold share decryption (C6). +/// One proof is generated per ciphertext index. +#[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derivative(Debug)] +pub struct ThresholdShareDecryptionProofRequest { + /// Serialized ciphertext bytes, one per output index. + pub ciphertext_bytes: Vec, + /// Serialized aggregated PublicKey bytes. + pub aggregated_pk_bytes: ArcBytes, + /// Aggregated secret key polynomial (encrypted at rest). + pub sk_poly_sum: SensitiveBytes, + /// Aggregated smudging error polynomials (encrypted at rest), one per output index. + pub es_poly_sum: Vec, + /// Computed decryption share polynomials, one per output index. + pub d_share_bytes: Vec, + /// BFV preset for parameter resolution. + pub params_preset: BfvPreset, +} + +/// Response containing generated proofs for threshold share decryption (C6). +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ThresholdShareDecryptionProofResponse { + /// One C6 proof per ciphertext index. + pub proofs: Vec, +} + /// Response containing a generated share computation proof. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ShareComputationProofResponse { @@ -341,6 +375,31 @@ pub struct VerifyShareDecryptionProofsResponse { pub party_results: Vec, } +// --- C7 proof generation --- + +/// Request to generate proof(s) for decrypted shares aggregation (C7). +#[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derivative(Debug)] +pub struct DecryptedSharesAggregationProofRequest { + /// Decryption shares per party: (party_id, shares_per_ct_index). + pub d_share_polys: Vec<(u64, Vec)>, + /// Decoded plaintext per ciphertext index. + pub plaintext: Vec, + /// BFV preset (selects BN vs Mod circuit variant). + pub params_preset: BfvPreset, + /// Threshold required for decryption. + pub threshold_m: u64, + /// Committee size. + pub threshold_n: u64, +} + +/// Response containing generated proofs for decrypted shares aggregation (C7). +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DecryptedSharesAggregationProofResponse { + /// One C7 proof per ciphertext index. + pub proofs: Vec, +} + /// ZK-specific error variants. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ZkError { diff --git a/crates/events/src/enclave_event/decryption_share_proof_signed.rs b/crates/events/src/enclave_event/decryption_share_proof_signed.rs new file mode 100644 index 0000000000..5bb5331be7 --- /dev/null +++ b/crates/events/src/enclave_event/decryption_share_proof_signed.rs @@ -0,0 +1,13 @@ +// 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 crate::E3id; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct DecryptionShareProofSigned { + pub e3_id: E3id, +} diff --git a/crates/events/src/enclave_event/decryptionshare_created.rs b/crates/events/src/enclave_event/decryptionshare_created.rs index 1916db7068..e34e096b8f 100644 --- a/crates/events/src/enclave_event/decryptionshare_created.rs +++ b/crates/events/src/enclave_event/decryptionshare_created.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::E3id; +use crate::{E3id, SignedProofPayload}; use actix::Message; use e3_utils::utility_types::ArcBytes; use serde::{Deserialize, Serialize}; @@ -18,6 +18,9 @@ pub struct DecryptionshareCreated { // ciphertext pub e3_id: E3id, pub node: String, + /// C6 proofs: one signed proof of correct decryption per ciphertext index. + #[serde(default)] + pub signed_decryption_proofs: Vec, } impl Display for DecryptionshareCreated { diff --git a/crates/events/src/enclave_event/keyshare_created.rs b/crates/events/src/enclave_event/keyshare_created.rs index 2c2d614c06..19795149d0 100644 --- a/crates/events/src/enclave_event/keyshare_created.rs +++ b/crates/events/src/enclave_event/keyshare_created.rs @@ -19,6 +19,7 @@ pub struct KeyshareCreated { pub pubkey: ArcBytes, pub e3_id: E3id, pub node: String, + #[serde(default)] pub signed_pk_generation_proof: Option, } diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index ccaa29d5fb..e9b4e0a242 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -4,6 +4,8 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +mod aggregation_proof_pending; +mod aggregation_proof_signed; mod ciphernode_added; mod ciphernode_removed; mod ciphernode_selected; @@ -15,6 +17,7 @@ mod committee_requested; mod compute_request; mod configuration_updated; mod decryption_key_shared; +mod decryption_share_proof_signed; mod decryption_share_proofs; mod decryptionshare_created; mod die; @@ -32,6 +35,8 @@ mod keyshare_created; mod net_sync_events_received; mod operator_activation_changed; mod outgoing_sync_requested; +mod pk_aggregation_proof_pending; +mod pk_aggregation_proof_signed; mod pk_generation_proof_signed; mod plaintext_aggregated; mod plaintext_output_published; @@ -39,6 +44,7 @@ mod proof; mod publickey_aggregated; mod publish_document; mod share_computation_proof_signed; +mod share_decryption_proof_pending; mod share_verification; mod shutdown; mod signed_proof; @@ -54,6 +60,8 @@ mod ticket_generated; mod ticket_submitted; mod typed_event; +pub use aggregation_proof_pending::*; +pub use aggregation_proof_signed::*; pub use ciphernode_added::*; pub use ciphernode_removed::*; pub use ciphernode_selected::*; @@ -65,6 +73,7 @@ pub use committee_requested::*; pub use compute_request::*; pub use configuration_updated::*; pub use decryption_key_shared::*; +pub use decryption_share_proof_signed::*; pub use decryption_share_proofs::*; pub use decryptionshare_created::*; pub use die::*; @@ -83,6 +92,8 @@ pub use keyshare_created::*; pub use net_sync_events_received::*; pub use operator_activation_changed::*; pub use outgoing_sync_requested::*; +pub use pk_aggregation_proof_pending::*; +pub use pk_aggregation_proof_signed::*; pub use pk_generation_proof_signed::*; pub use plaintext_aggregated::*; pub use plaintext_output_published::*; @@ -90,6 +101,7 @@ pub use proof::*; pub use publickey_aggregated::*; pub use publish_document::*; pub use share_computation_proof_signed::*; +pub use share_decryption_proof_pending::*; pub use share_verification::*; pub use shutdown::*; pub use signed_proof::*; @@ -252,6 +264,12 @@ pub enum EnclaveEventData { SyncEffect(SyncEffect), SyncEnded(SyncEnded), EffectsEnabled(EffectsEnabled), + DecryptionShareProofSigned(DecryptionShareProofSigned), + ShareDecryptionProofPending(ShareDecryptionProofPending), + PkAggregationProofPending(PkAggregationProofPending), + PkAggregationProofSigned(PkAggregationProofSigned), + AggregationProofPending(AggregationProofPending), + AggregationProofSigned(AggregationProofSigned), /// This is a test event to use in testing TestEvent(TestEvent), } @@ -504,6 +522,12 @@ impl EnclaveEventData { EnclaveEventData::ShareVerificationComplete(ref data) => Some(data.e3_id.clone()), EnclaveEventData::E3Failed(ref data) => Some(data.e3_id.clone()), EnclaveEventData::E3StageChanged(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::DecryptionShareProofSigned(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::ShareDecryptionProofPending(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::PkAggregationProofPending(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::PkAggregationProofSigned(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::AggregationProofPending(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::AggregationProofSigned(ref data) => Some(data.e3_id.clone()), _ => None, } } @@ -579,7 +603,13 @@ impl_event_types!( HistoricalNetSyncStart, SyncEffect, SyncEnded, - EffectsEnabled + EffectsEnabled, + DecryptionShareProofSigned, + ShareDecryptionProofPending, + PkAggregationProofPending, + PkAggregationProofSigned, + AggregationProofPending, + AggregationProofSigned ); impl TryFrom<&EnclaveEvent> for EnclaveError { diff --git a/crates/events/src/enclave_event/pk_aggregation_proof_pending.rs b/crates/events/src/enclave_event/pk_aggregation_proof_pending.rs new file mode 100644 index 0000000000..2ee5b1afe8 --- /dev/null +++ b/crates/events/src/enclave_event/pk_aggregation_proof_pending.rs @@ -0,0 +1,25 @@ +// 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. + +//! Event for C5 proof generation through ProofRequestActor. +//! +//! `PkAggregationProofPending` is published by [`PublicKeyAggregator`] after +//! C1 verification succeeds and synchronous aggregation completes. +//! `ProofRequestActor` generates the C5 proof, signs it, and publishes +//! `PkAggregationProofSigned`. + +use crate::{E3id, OrderedSet, PkAggregationProofRequest}; +use serde::{Deserialize, Serialize}; + +/// PublicKeyAggregator -> ProofRequestActor: generate and sign C5 proof. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PkAggregationProofPending { + pub e3_id: E3id, + pub proof_request: PkAggregationProofRequest, + pub public_key: Vec, + pub public_key_hash: [u8; 32], + pub nodes: OrderedSet, +} diff --git a/crates/events/src/enclave_event/pk_aggregation_proof_signed.rs b/crates/events/src/enclave_event/pk_aggregation_proof_signed.rs new file mode 100644 index 0000000000..bf2d118392 --- /dev/null +++ b/crates/events/src/enclave_event/pk_aggregation_proof_signed.rs @@ -0,0 +1,21 @@ +// 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. + +//! Event for C5 proof signing completion. +//! +//! `PkAggregationProofSigned` is published by [`ProofRequestActor`] after +//! generating and ECDSA-signing the C5 proof. [`PublicKeyAggregator`] +//! consumes this to transition to Complete and publish `PublicKeyAggregated`. + +use crate::{E3id, SignedProofPayload}; +use serde::{Deserialize, Serialize}; + +/// ProofRequestActor -> PublicKeyAggregator: signed C5 proof. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct PkAggregationProofSigned { + pub e3_id: E3id, + pub signed_proof: SignedProofPayload, +} diff --git a/crates/events/src/enclave_event/plaintext_aggregated.rs b/crates/events/src/enclave_event/plaintext_aggregated.rs index fc2a25cd3a..bda9a2f3a0 100644 --- a/crates/events/src/enclave_event/plaintext_aggregated.rs +++ b/crates/events/src/enclave_event/plaintext_aggregated.rs @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -use crate::E3id; +use crate::{E3id, Proof}; use actix::Message; use derivative::Derivative; use e3_utils::ArcBytes; @@ -17,6 +17,9 @@ use std::fmt::{self, Display}; pub struct PlaintextAggregated { pub e3_id: E3id, pub decrypted_output: Vec, + /// C7 proofs: one proof of correct aggregation per ciphertext index. + #[serde(default)] + pub aggregation_proofs: Vec, } impl Display for PlaintextAggregated { diff --git a/crates/events/src/enclave_event/publickey_aggregated.rs b/crates/events/src/enclave_event/publickey_aggregated.rs index df6f2de778..128e8f368f 100644 --- a/crates/events/src/enclave_event/publickey_aggregated.rs +++ b/crates/events/src/enclave_event/publickey_aggregated.rs @@ -20,6 +20,7 @@ pub struct PublicKeyAggregated { pub e3_id: E3id, pub nodes: OrderedSet, /// C5 proof: proof of correct pk aggregation. + #[serde(default)] pub pk_aggregation_proof: Option, } diff --git a/crates/events/src/enclave_event/share_decryption_proof_pending.rs b/crates/events/src/enclave_event/share_decryption_proof_pending.rs new file mode 100644 index 0000000000..ec7134fb19 --- /dev/null +++ b/crates/events/src/enclave_event/share_decryption_proof_pending.rs @@ -0,0 +1,31 @@ +// 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. + +//! Event for C6 proof generation through ProofRequestActor. +//! +//! `ShareDecryptionProofPending` is published by [`ThresholdKeyshare`] when it +//! has computed the decryption share and needs C6 proofs generated and signed. +//! `ProofRequestActor` generates the proofs, signs them, and publishes +//! `DecryptionshareCreated` with signed proofs. + +use crate::{E3id, ThresholdShareDecryptionProofRequest}; +use e3_utils::utility_types::ArcBytes; +use serde::{Deserialize, Serialize}; + +/// ThresholdKeyshare -> ProofRequestActor: generate and sign C6 proofs. +/// +/// Carries the proof generation inputs and the protocol data so that +/// ProofRequestActor can publish `DecryptionshareCreated` directly. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ShareDecryptionProofPending { + pub e3_id: E3id, + pub party_id: u64, + pub node: String, + /// Computed decryption shares, one per ciphertext index. + pub decryption_share: Vec, + /// C6 proof generation request. + pub proof_request: ThresholdShareDecryptionProofRequest, +} diff --git a/crates/events/src/enclave_event/share_verification.rs b/crates/events/src/enclave_event/share_verification.rs index 2ca3c37243..b529c4257e 100644 --- a/crates/events/src/enclave_event/share_verification.rs +++ b/crates/events/src/enclave_event/share_verification.rs @@ -24,6 +24,10 @@ pub enum VerificationKind { ShareProofs, /// C4 share decryption proof verification (after AllDecryptionKeySharesCollected). DecryptionProofs, + /// C6 threshold decryption proof verification (after all DecryptionshareCreated collected). + ThresholdDecryptionProofs, + /// C1 PK generation proof verification (after all KeyshareCreated collected). + PkGenerationProofs, } /// ThresholdKeyshare → ShareVerificationActor: verify party proofs. diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index 0d2234ac30..f7634ffc68 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -22,10 +22,8 @@ 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. +/// Proof type identifier covering all node-generated proofs, including +/// aggregation proofs (C5 pk aggregation and C7 decrypted shares aggregation). #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ProofType { @@ -47,6 +45,8 @@ pub enum ProofType { T5ShareDecryption = 7, /// T6 — Decrypted shares aggregation proof (Proof 7). T6DecryptedSharesAggregation = 8, + /// C5 — Public key aggregation proof (Proof 5). + C5PkAggregation = 9, } impl ProofType { @@ -65,6 +65,7 @@ impl ProofType { CircuitName::DecryptedSharesAggregationBn, CircuitName::DecryptedSharesAggregationMod, ], + ProofType::C5PkAggregation => vec![CircuitName::PkAggregation], } } @@ -80,6 +81,7 @@ impl ProofType { | ProofType::T2DkgShareDecryption => "E3_BAD_DKG_PROOF", ProofType::T5ShareDecryption => "E3_BAD_DECRYPTION_PROOF", ProofType::T6DecryptedSharesAggregation => "E3_BAD_AGGREGATION_PROOF", + ProofType::C5PkAggregation => "E3_BAD_PK_AGGREGATION_PROOF", } } } diff --git a/crates/keyshare/src/decryption_key_shared_collector.rs b/crates/keyshare/src/decryption_key_shared_collector.rs index e6fc4872b3..0795b4124c 100644 --- a/crates/keyshare/src/decryption_key_shared_collector.rs +++ b/crates/keyshare/src/decryption_key_shared_collector.rs @@ -16,7 +16,7 @@ use tracing::{info, warn}; use crate::ThresholdKeyshare; -const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(600); +const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(1200); enum CollectorState { Collecting, diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index 5be5aadebb..86d308ba55 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -11,14 +11,15 @@ use e3_data::Persistable; use e3_events::{ prelude::*, trap, BusHandle, CiphernodeSelected, CiphertextOutputPublished, ComputeRequest, ComputeResponse, ComputeResponseKind, CorrelationId, DecryptionKeyShared, - DecryptionShareProofsPending, DecryptionshareCreated, Die, DkgProofSigned, + DecryptionShareProofSigned, DecryptionShareProofsPending, Die, DkgProofSigned, DkgShareDecryptionProofRequest, E3Failed, E3RequestComplete, E3Stage, E3id, EType, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCollectionFailed, EncryptionKeyCreated, EncryptionKeyPending, EventContext, FailureReason, KeyshareCreated, PartyId, PartyProofsToVerify, PartyShareDecryptionProofsToVerify, PkGenerationProofRequest, PkGenerationProofSigned, ProofType, Sequenced, ShareComputationProofRequest, - ShareEncryptionProofRequest, ShareVerificationComplete, ShareVerificationDispatched, - SignedProofPayload, ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated, + ShareDecryptionProofPending, ShareEncryptionProofRequest, ShareVerificationComplete, + ShareVerificationDispatched, SignedProofPayload, ThresholdShare, + ThresholdShareCollectionFailed, ThresholdShareCreated, ThresholdShareDecryptionProofRequest, ThresholdSharePending, TypedEvent, VerificationKind, }; use e3_fhe_params::create_deterministic_crp_from_default_seed; @@ -155,6 +156,19 @@ pub struct Decrypting { pk_share: ArcBytes, sk_poly_sum: SensitiveBytes, es_poly_sum: Vec, + /// Ciphertext bytes from CiphertextOutputPublished, needed for C6 proof generation. + ciphertext_output: Vec, + signed_pk_generation_proof: Option, + signed_sk_share_computation_proof: Option, + signed_e_sm_share_computation_proof: Option, + signed_sk_share_encryption_proofs: Vec, + signed_e_sm_share_encryption_proofs: Vec, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct GeneratingDecryptionProof { + pk_share: ArcBytes, + decryption_share: Vec, signed_pk_generation_proof: Option, signed_sk_share_computation_proof: Option, signed_e_sm_share_computation_proof: Option, @@ -176,6 +190,8 @@ pub enum KeyshareState { ReadyForDecryption(ReadyForDecryption), // Decrypting something Decrypting(Decrypting), + // Generating C6 proof of correct decryption + GeneratingDecryptionProof(GeneratingDecryptionProof), // Finished Completed, } @@ -195,7 +211,8 @@ impl KeyshareState { (K::GeneratingThresholdShare(_), K::AggregatingDecryptionKey(_)) => true, (K::AggregatingDecryptionKey(_), K::ReadyForDecryption(_)) => true, (K::ReadyForDecryption(_), K::Decrypting(_)) => true, - (K::Decrypting(_), K::Completed) => true, + (K::Decrypting(_), K::GeneratingDecryptionProof(_)) => true, + (K::GeneratingDecryptionProof(_), K::Completed) => true, _ => false, } } @@ -219,6 +236,7 @@ impl KeyshareState { Self::AggregatingDecryptionKey(_) => "AggregatingDecryptionKey", Self::ReadyForDecryption(_) => "ReadyForDecryption", Self::Decrypting(_) => "Decrypting", + Self::GeneratingDecryptionProof(_) => "GeneratingDecryptionProof", Self::Completed => "Completed", } } @@ -233,6 +251,10 @@ pub struct ThresholdKeyshareState { pub threshold_m: u64, pub threshold_n: u64, pub params: ArcBytes, + /// Aggregated public key bytes, captured from PublicKeyAggregated event for C6 proof. + /// Defaults to None for backward compatibility with existing persisted state. + #[serde(default)] + pub aggregated_pk: Option, } impl ThresholdKeyshareState { @@ -253,6 +275,7 @@ impl ThresholdKeyshareState { threshold_m, threshold_n, params, + aggregated_pk: None, } } @@ -347,6 +370,16 @@ impl TryInto for ThresholdKeyshareState { } } +impl TryInto for ThresholdKeyshareState { + type Error = anyhow::Error; + fn try_into(self) -> std::result::Result { + match self.state { + KeyshareState::GeneratingDecryptionProof(s) => Ok(s), + _ => Err(anyhow!("Invalid state: expected GeneratingDecryptionProof")), + } + } +} + pub struct ThresholdKeyshareParams { pub bus: BusHandle, pub cipher: Arc, @@ -372,8 +405,10 @@ pub struct ThresholdKeyshare { )>, /// Honest party IDs determined by C2/C3 verification, narrowed by C4. honest_parties: Option>, - /// DecryptionKeyShared events arriving before ReadyForDecryption. - early_decryption_key_shares: HashMap, + /// Temporarily stores DecryptionKeyShared while C4 verification is in flight. + pending_c4_verification_shares: Option>, + /// Aggregated public key bytes, captured from PublicKeyAggregated event for C6 proof. + aggregated_pk: Option, } impl ThresholdKeyshare { @@ -389,7 +424,8 @@ impl ThresholdKeyshare { pending_shares: Vec::new(), pending_share_decryption_data: None, honest_parties: None, - early_decryption_key_shares: HashMap::new(), + pending_c4_verification_shares: None, + aggregated_pk: None, } } } @@ -608,9 +644,9 @@ impl ThresholdKeyshare { } _ => Ok(()), }, - // ZK responses (C4 proofs, share/decryption verification) are now - // handled by ProofRequestActor and ShareVerificationActor respectively. - _ => Ok(()), + // ZK responses: proofs and verification are handled by + // ProofRequestActor and ShareVerificationActor respectively. + ComputeResponseKind::Zk(_) => Ok(()), } } @@ -1203,10 +1239,10 @@ impl ThresholdKeyshare { let dishonest_count = (pre_dishonest.len() as u64).min(total); let honest_count = total - dishonest_count; - if honest_count < threshold { + if honest_count <= threshold { warn!( - "Too few honest parties for E3 {} ({} honest < {} threshold) after C2/C3 pre-dishonest filtering — cannot proceed", - e3_id, honest_count, threshold + "Too few honest parties for E3 {} ({} honest, need at least {}) after C2/C3 pre-dishonest filtering — cannot proceed", + e3_id, honest_count, threshold + 1 ); self.pending_shares.clear(); self.bus.publish( @@ -1269,10 +1305,10 @@ impl ThresholdKeyshare { let dishonest_count = (msg.dishonest_parties.len() as u64).min(total); let honest_count = total - dishonest_count; - if honest_count < threshold { + if honest_count <= threshold { warn!( - "Too few honest parties for E3 {} ({} honest < {} threshold) — cannot proceed", - e3_id, honest_count, threshold + "Too few honest parties for E3 {} ({} honest, need at least {}) — cannot proceed", + e3_id, honest_count, threshold + 1 ); // Clear pending shares self.pending_shares.clear(); @@ -1311,10 +1347,10 @@ impl ThresholdKeyshare { .map(|h| h.len() as u64) .unwrap_or(0); - if honest_count < threshold { + if honest_count <= threshold { warn!( - "Too few honest parties after C4 for E3 {} ({} honest < {} threshold)", - e3_id, honest_count, threshold + "Too few honest parties after C4 for E3 {} ({} honest, need at least {})", + e3_id, honest_count, threshold + 1 ); self.bus.publish( E3Failed { @@ -1342,6 +1378,7 @@ impl ThresholdKeyshare { self.publish_keyshare_created(ec) } + _ => Ok(()), } } @@ -1485,7 +1522,7 @@ impl ThresholdKeyshare { ); // Re-check threshold after exclusion let threshold = state.threshold_m; - if (honest_shares.len() as u64) < threshold { + if (honest_shares.len() as u64) <= threshold { self.pending_shares.clear(); self.bus.publish( E3Failed { @@ -1653,7 +1690,10 @@ impl ThresholdKeyshare { .ok_or_else(|| anyhow!("No pending share decryption data — CalculateDecryptionKey responded before proof requests were built"))?; // Take early shares from the actor before transitioning - let early_shares = std::mem::take(&mut self.early_decryption_key_shares); + let early_shares = self + .pending_c4_verification_shares + .take() + .unwrap_or_default(); // Transition to ReadyForDecryption self.state.try_mutate(&ec, |s| { @@ -1745,7 +1785,9 @@ impl ThresholdKeyshare { "Storing early DecryptionKeyShared from party {} (state: AggregatingDecryptionKey)", party_id ); - self.early_decryption_key_shares.insert(party_id, data); + self.pending_c4_verification_shares + .get_or_insert_with(HashMap::new) + .insert(party_id, data); Ok(()) } @@ -1809,10 +1851,10 @@ impl ThresholdKeyshare { .map(|h| h.len() as u64) .unwrap_or(0); - if honest_count < threshold { + if honest_count <= threshold { warn!( - "Too few honest parties after C4 pre-filtering for E3 {} ({} honest < {} threshold)", - e3_id, honest_count, threshold + "Too few honest parties after C4 pre-filtering for E3 {} ({} honest, need at least {})", + e3_id, honest_count, threshold + 1 ); self.bus.publish( E3Failed { @@ -1879,7 +1921,9 @@ impl ThresholdKeyshare { msg: TypedEvent, ) -> Result<()> { let (msg, ec) = msg.into_components(); - // Set state to decrypting + let ciphertext_output = msg.ciphertext_output; + + // Set state to decrypting, storing ciphertext for later C6 proof generation self.state.try_mutate(&ec, |s| { use KeyshareState as K; @@ -1889,6 +1933,7 @@ impl ThresholdKeyshare { pk_share: current.pk_share, sk_poly_sum: current.sk_poly_sum, es_poly_sum: current.es_poly_sum, + ciphertext_output: ciphertext_output.clone(), signed_pk_generation_proof: current.signed_pk_generation_proof, signed_sk_share_computation_proof: current.signed_sk_share_computation_proof, signed_e_sm_share_computation_proof: current.signed_e_sm_share_computation_proof, @@ -1899,7 +1944,6 @@ impl ThresholdKeyshare { s.new_state(next) })?; - let ciphertext_output = msg.ciphertext_output; let state = self.state.try_get()?; let e3_id = state.get_e3_id(); let decrypting: Decrypting = state.clone().try_into()?; @@ -1922,7 +1966,8 @@ impl ThresholdKeyshare { Ok(()) } - /// CalculateDecryptionShareResponse + /// CalculateDecryptionShareResponse — publish ShareDecryptionProofPending + /// so ProofRequestActor generates and signs C6 proofs. pub fn handle_calculate_decryption_share_response( &mut self, res: TypedEvent, @@ -1930,26 +1975,83 @@ impl ThresholdKeyshare { let (res, ec) = res.into_components(); let msg: CalculateDecryptionShareResponse = res.try_into()?; let state = self.state.try_get()?; - let party_id = state.party_id; - let node = state.address; - let e3_id = state.e3_id; - let decryption_share = msg.d_share_poly; + let e3_id = state.e3_id.clone(); + let decrypting: Decrypting = state.clone().try_into()?; + let d_share_poly = msg.d_share_poly; - let event = DecryptionshareCreated { - party_id, - node, - e3_id, - decryption_share, - }; + let aggregated_pk_bytes = self + .aggregated_pk + .clone() + .or_else(|| state.aggregated_pk.clone()) + .ok_or_else(|| anyhow!("Aggregated public key not available for C6 proof"))?; - // send the decryption share - self.bus.publish(event, ec.clone())?; + let threshold_preset = self + .share_enc_preset + .clone() + .threshold_counterpart() + .ok_or_else(|| { + anyhow!( + "No threshold counterpart for preset {:?}", + self.share_enc_preset + ) + })?; + + info!("Publishing ShareDecryptionProofPending for C6 proof generation..."); + + // Publish pending event before transitioning state so a publish + // failure leaves us in Decrypting (retryable) rather than + // GeneratingDecryptionProof (no retry path). + self.bus.publish( + ShareDecryptionProofPending { + e3_id: e3_id.clone(), + party_id: state.party_id, + node: state.address.clone(), + decryption_share: d_share_poly.clone(), + proof_request: ThresholdShareDecryptionProofRequest { + ciphertext_bytes: decrypting.ciphertext_output, + aggregated_pk_bytes, + sk_poly_sum: decrypting.sk_poly_sum, + es_poly_sum: decrypting.es_poly_sum, + d_share_bytes: d_share_poly.clone(), + params_preset: threshold_preset, + }, + }, + ec.clone(), + )?; - // mark as complete + // Transition to GeneratingDecryptionProof state self.state.try_mutate(&ec, |s| { use KeyshareState as K; - info!("Decryption share sending process is complete"); + s.new_state(K::GeneratingDecryptionProof(GeneratingDecryptionProof { + pk_share: decrypting.pk_share.clone(), + decryption_share: d_share_poly, + signed_pk_generation_proof: decrypting.signed_pk_generation_proof.clone(), + signed_sk_share_computation_proof: decrypting + .signed_sk_share_computation_proof + .clone(), + signed_e_sm_share_computation_proof: decrypting + .signed_e_sm_share_computation_proof + .clone(), + signed_sk_share_encryption_proofs: decrypting + .signed_sk_share_encryption_proofs + .clone(), + signed_e_sm_share_encryption_proofs: decrypting + .signed_e_sm_share_encryption_proofs + .clone(), + })) + })?; + Ok(()) + } + pub fn handle_decryption_share_proof_signed( + &mut self, + msg: TypedEvent, + ) -> Result<()> { + let (_msg, ec) = msg.into_components(); + + self.state.try_mutate(&ec, |s| { + use KeyshareState as K; + info!("Decryption share sending process is complete"); s.new_state(K::Completed) })?; @@ -1969,6 +2071,14 @@ impl Handler for ThresholdKeyshare { EnclaveEventData::CiphertextOutputPublished(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::PublicKeyAggregated(data) => { + let pk = ArcBytes::from_bytes(&data.pubkey); + self.aggregated_pk = Some(pk.clone()); + let _ = self.state.try_mutate(&ec, |mut s| { + s.aggregated_pk = Some(pk); + Ok(s) + }); + } EnclaveEventData::ThresholdShareCreated(data) => { let _ = self.handle_threshold_share_created(TypedEvent::new(data, ec), ctx.address()); @@ -2066,6 +2176,9 @@ impl Handler for ThresholdKeyshare { } } } + EnclaveEventData::DecryptionShareProofSigned(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } EnclaveEventData::ShareVerificationComplete(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } @@ -2077,6 +2190,21 @@ impl Handler for ThresholdKeyshare { } } +impl Handler> for ThresholdKeyshare { + type Result = (); + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + trap( + EType::KeyGeneration, + &self.bus.with_ec(msg.get_ctx()), + || self.handle_decryption_share_proof_signed(msg), + ) + } +} + impl Handler> for ThresholdKeyshare { type Result = (); fn handle( @@ -2279,7 +2407,7 @@ impl Handler for ThresholdKeyshare { self.pending_shares.clear(); self.pending_share_decryption_data = None; self.honest_parties = None; - self.early_decryption_key_shares.clear(); + self.pending_c4_verification_shares = None; self.notify_sync(ctx, Die); } } diff --git a/crates/keyshare/src/threshold_share_collector.rs b/crates/keyshare/src/threshold_share_collector.rs index 8d097d34c3..edf052b9bd 100644 --- a/crates/keyshare/src/threshold_share_collector.rs +++ b/crates/keyshare/src/threshold_share_collector.rs @@ -34,7 +34,7 @@ pub struct ReceivedShareProofs { pub signed_c3b_proofs: Vec, } -const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(600); +const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(1200); pub(crate) enum CollectorState { Collecting, diff --git a/crates/multithread/Cargo.toml b/crates/multithread/Cargo.toml index 9f414b2ab9..822de62b61 100644 --- a/crates/multithread/Cargo.toml +++ b/crates/multithread/Cargo.toml @@ -10,6 +10,7 @@ repository.workspace = true actix = { workspace = true } anyhow = { workspace = true } bincode = { workspace = true } +e3-bfv-client = { workspace = true } e3-data = { workspace = true } e3-fhe-params = { workspace = true } e3-trbfv = { workspace = true } @@ -20,6 +21,7 @@ e3-zk-helpers = { workspace = true } e3-utils = { workspace = true } e3-zk-prover = { workspace = true } fhe = { workspace = true } +fhe-math = { workspace = true } fhe-traits = { workspace = true } ndarray = { workspace = true } num-bigint = { workspace = true } diff --git a/crates/multithread/src/multithread.rs b/crates/multithread/src/multithread.rs index c89fd1a91e..a27436aed2 100644 --- a/crates/multithread/src/multithread.rs +++ b/crates/multithread/src/multithread.rs @@ -24,16 +24,20 @@ use e3_events::EType; use e3_events::EffectsEnabled; use e3_events::{ BusHandle, ComputeRequest, ComputeRequestError, ComputeRequestErrorKind, ComputeRequestKind, - ComputeResponse, DkgShareDecryptionProofRequest, DkgShareDecryptionProofResponse, EnclaveEvent, - EnclaveEventData, EventPublisher, EventSubscriber, EventType, PartyVerificationResult, - PkAggregationProofRequest, PkAggregationProofResponse, PkBfvProofRequest, PkBfvProofResponse, - PkGenerationProofRequest, PkGenerationProofResponse, ShareComputationProofRequest, - ShareComputationProofResponse, ShareEncryptionProofRequest, ShareEncryptionProofResponse, - TypedEvent, VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, + ComputeResponse, DecryptedSharesAggregationProofRequest, + DecryptedSharesAggregationProofResponse, DkgShareDecryptionProofRequest, + DkgShareDecryptionProofResponse, EnclaveEvent, EnclaveEventData, EventPublisher, + EventSubscriber, EventType, PartyVerificationResult, PkAggregationProofRequest, + PkAggregationProofResponse, PkBfvProofRequest, PkBfvProofResponse, PkGenerationProofRequest, + PkGenerationProofResponse, ShareComputationProofRequest, ShareComputationProofResponse, + ShareEncryptionProofRequest, ShareEncryptionProofResponse, + ThresholdShareDecryptionProofRequest, ThresholdShareDecryptionProofResponse, TypedEvent, + VerifyShareDecryptionProofsRequest, VerifyShareDecryptionProofsResponse, VerifyShareProofsRequest, VerifyShareProofsResponse, ZkError as ZkEventError, ZkRequest, ZkResponse, }; use e3_fhe_params::build_pair_for_preset; +use e3_fhe_params::create_deterministic_crp_from_default_seed; use e3_fhe_params::{BfvParamSet, BfvPreset}; use e3_polynomial::CrtPolynomial; use e3_trbfv::calculate_decryption_key::calculate_decryption_key; @@ -43,12 +47,16 @@ use e3_trbfv::gen_esi_sss::gen_esi_sss; use e3_trbfv::gen_pk_share_and_sk_sss::gen_pk_share_and_sk_sss; use e3_trbfv::helpers::deserialize_secret_key; use e3_trbfv::helpers::try_poly_from_bytes; +use e3_trbfv::helpers::try_poly_from_sensitive_bytes; use e3_trbfv::shares::SharedSecret; use e3_trbfv::{TrBFVError, TrBFVRequest, TrBFVResponse}; use e3_utils::SharedRng; use e3_utils::MAILBOX_LIMIT; use e3_zk_helpers::circuits::dkg::pk::circuit::{PkCircuit, PkCircuitData}; use e3_zk_helpers::circuits::dkg::share_computation::utils::compute_parity_matrix; +use e3_zk_helpers::circuits::threshold::decrypted_shares_aggregation::circuit::{ + DecryptedSharesAggregationCircuit, DecryptedSharesAggregationCircuitData, +}; use e3_zk_helpers::circuits::threshold::pk_generation::circuit::{ PkGenerationCircuit, PkGenerationCircuitData, }; @@ -58,8 +66,10 @@ use e3_zk_helpers::dkg::share_decryption::{ShareDecryptionCircuit, ShareDecrypti use e3_zk_helpers::dkg::share_encryption::{ShareEncryptionCircuit, ShareEncryptionCircuitData}; use e3_zk_helpers::threshold::pk_aggregation::PkAggregationCircuit; use e3_zk_helpers::threshold::pk_aggregation::PkAggregationCircuitData; +use e3_zk_helpers::CiphernodesCommittee; use e3_zk_prover::{Provable, ZkBackend, ZkProver}; use fhe::bfv::{Ciphertext, Encoding, Plaintext, PublicKey, SecretKey}; +use fhe::mbfv::PublicKeyShare; use fhe_traits::{DeserializeParametrized, FheEncoder}; use ndarray::Array2; use num_bigint::BigInt; @@ -271,16 +281,15 @@ fn handle_pk_aggregation_proof( .map_err(|e| make_zk_error(&request, format!("build_pair_for_preset: {}", e)))?; // 2. Create deterministic CRP - let crp = e3_fhe_params::create_deterministic_crp_from_default_seed(&threshold_params); + let crp = create_deterministic_crp_from_default_seed(&threshold_params); // 3. Deserialize each keyshare as PublicKeyShare and extract pk0 let mut pk0_shares = Vec::with_capacity(req.keyshare_bytes.len()); for (i, ks_bytes) in req.keyshare_bytes.iter().enumerate() { - let pk_share = - fhe::mbfv::PublicKeyShare::deserialize(ks_bytes, &threshold_params, crp.clone()) - .map_err(|e| { - make_zk_error(&request, format!("keyshare[{}] deserialize: {:?}", i, e)) - })?; + let pk_share = PublicKeyShare::deserialize(ks_bytes, &threshold_params, crp.clone()) + .map_err(|e| { + make_zk_error(&request, format!("keyshare[{}] deserialize: {:?}", i, e)) + })?; pk0_shares.push(CrtPolynomial::from_fhe_polynomial(&pk_share.p0_share())); } @@ -292,7 +301,7 @@ fn handle_pk_aggregation_proof( let a = CrtPolynomial::from_fhe_polynomial(&crp.poly()); // 6. Build committee and circuit data - let committee = e3_zk_helpers::CiphernodesCommittee { + let committee = CiphernodesCommittee { n: req.committee_n, h: req.committee_h, threshold: req.committee_threshold, @@ -325,6 +334,99 @@ fn handle_pk_aggregation_proof( )) } +fn handle_threshold_share_decryption_proof( + prover: &ZkProver, + cipher: &Cipher, + req: ThresholdShareDecryptionProofRequest, + request: ComputeRequest, +) -> Result { + // 1. Build threshold BFV parameters from preset + let (threshold_params, _dkg_params) = build_pair_for_preset(req.params_preset.clone()) + .map_err(|e| make_zk_error(&request, format!("build_pair_for_preset: {}", e)))?; + + // 2. Deserialize aggregated PublicKey + let public_key = PublicKey::from_bytes(&req.aggregated_pk_bytes, &threshold_params) + .map_err(|e| make_zk_error(&request, format!("aggregated_pk deserialize: {:?}", e)))?; + + // 3. Decrypt sk_poly_sum → Poly → CrtPolynomial (s) + let sk_poly = try_poly_from_sensitive_bytes(req.sk_poly_sum, threshold_params.clone(), cipher) + .map_err(|e| make_zk_error(&request, format!("sk_poly_sum decrypt: {}", e)))?; + let s = CrtPolynomial::from_fhe_polynomial(&sk_poly); + + // 4. For each index, build circuit data and generate proof + let num_indices = req.ciphertext_bytes.len(); + if req.es_poly_sum.is_empty() { + return Err(make_zk_error(&request, "empty es_poly_sum".to_string())); + } + if req.d_share_bytes.len() < num_indices { + return Err(make_zk_error( + &request, + format!( + "d_share_bytes too short: {} < {}", + req.d_share_bytes.len(), + num_indices + ), + )); + } + let mut proofs = Vec::with_capacity(num_indices); + + for i in 0..num_indices { + // Deserialize ciphertext + let ciphertext = Ciphertext::from_bytes(&req.ciphertext_bytes[i], &threshold_params) + .map_err(|e| { + make_zk_error(&request, format!("ciphertext[{}] deserialize: {:?}", i, e)) + })?; + + // Decrypt es_poly_sum → Poly → CrtPolynomial (e) + // Currently there is a single smudging noise polynomial shared across all + // ciphertexts (see calculate_decryption_share.rs). + let es_idx = i % req.es_poly_sum.len(); + let e_poly = try_poly_from_sensitive_bytes( + req.es_poly_sum[es_idx].clone(), + threshold_params.clone(), + cipher, + ) + .map_err(|e| make_zk_error(&request, format!("es_poly_sum[{}] decrypt: {}", i, e)))?; + let e = CrtPolynomial::from_fhe_polynomial(&e_poly); + + // Deserialize d_share → Poly → CrtPolynomial + let d_share_poly = try_poly_from_bytes(&req.d_share_bytes[i], &threshold_params) + .map_err(|e| make_zk_error(&request, format!("d_share[{}] deserialize: {}", i, e)))?; + let d_share = CrtPolynomial::from_fhe_polynomial(&d_share_poly); + + // Build circuit data + let circuit_data = e3_zk_helpers::threshold::share_decryption::ShareDecryptionCircuitData { + ciphertext, + public_key: public_key.clone(), + s: s.clone(), + e, + d_share, + }; + + // Generate proof + let circuit = e3_zk_helpers::threshold::share_decryption::ShareDecryptionCircuit; + let e3_id_str = request.e3_id.to_string(); + let proof = circuit + .prove(prover, &req.params_preset, &circuit_data, &e3_id_str) + .map_err(|e| { + ComputeRequestError::new( + ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(format!( + "C6 proof[{}]: {}", + i, e + ))), + request.clone(), + ) + })?; + proofs.push(proof); + } + + Ok(ComputeResponse::zk( + ZkResponse::ThresholdShareDecryption(ThresholdShareDecryptionProofResponse { proofs }), + request.correlation_id, + request.e3_id, + )) +} + fn timefunc( name: &str, id: u8, @@ -501,6 +603,16 @@ fn handle_zk_request( ZkRequest::PkAggregation(req) => timefunc("zk_pk_aggregation", id, || { handle_pk_aggregation_proof(&prover, req, request.clone()) }), + ZkRequest::ThresholdShareDecryption(req) => { + timefunc("zk_threshold_share_decryption", id, || { + handle_threshold_share_decryption_proof(&prover, &cipher, req, request.clone()) + }) + } + ZkRequest::DecryptedSharesAggregation(req) => { + timefunc("zk_decrypted_shares_aggregation", id, || { + handle_decrypted_shares_aggregation_proof(&prover, req, request.clone()) + }) + } } } @@ -1034,3 +1146,105 @@ fn handle_verify_share_decryption_proofs( request.e3_id, )) } + +fn handle_decrypted_shares_aggregation_proof( + prover: &ZkProver, + mut req: DecryptedSharesAggregationProofRequest, + request: ComputeRequest, +) -> Result { + // 1. Build threshold BFV parameters from preset + let (threshold_params, _dkg_params) = build_pair_for_preset(req.params_preset.clone()) + .map_err(|e| make_zk_error(&request, format!("build_pair_for_preset: {}", e)))?; + + // 2. Sort d_share_polys by party ID — the Noir circuit requires + // party_ids in strictly increasing order for Lagrange sign computation. + req.d_share_polys.sort_by_key(|(id, _)| *id); + + // 3. The circuit expects exactly threshold + 1 shares for Lagrange interpolation. + // We may have more honest parties than needed, so take the first threshold + 1. + let required = req.threshold_m as usize + 1; + if req.d_share_polys.len() > required { + req.d_share_polys.truncate(required); + } + + // 4. Determine dimensions + let num_indices = req.plaintext.len(); + let num_parties = req.d_share_polys.len(); + + for (party_id, shares) in &req.d_share_polys { + if shares.len() < num_indices { + return Err(make_zk_error( + &request, + format!( + "party {} has {} shares but {} expected", + party_id, + shares.len(), + num_indices + ), + )); + } + } + + let mut proofs = Vec::with_capacity(num_indices); + + // 4. For each ciphertext index, build circuit data and generate proof + for i in 0..num_indices { + // a. Extract per-party shares for index i, deserialize to Poly + let d_share_polys: Vec = req + .d_share_polys + .iter() + .map(|(_, shares)| try_poly_from_bytes(&shares[i], &threshold_params)) + .collect::>() + .map_err(|e| { + make_zk_error(&request, format!("d_share_polys[{}] deserialize: {}", i, e)) + })?; + + // b. Get party IDs (convert 0-based to 1-based for circuit) + let reconstructing_parties: Vec = req + .d_share_polys + .iter() + .map(|(id, _)| (*id as usize) + 1) + .collect(); + + // c. Decode plaintext at index i to Vec + let message_vec = e3_bfv_client::decode_bytes_to_vec_u64(&req.plaintext[i].extract_bytes()) + .map_err(|e| make_zk_error(&request, format!("plaintext[{}] decode: {:?}", i, e)))?; + + // d. Build committee + let committee = e3_zk_helpers::CiphernodesCommittee { + n: req.threshold_n as usize, + h: num_parties, + threshold: req.threshold_m as usize, + }; + + // e. Build circuit data and generate proof + let circuit_data = DecryptedSharesAggregationCircuitData { + committee, + d_share_polys, + reconstructing_parties, + message_vec, + }; + + let circuit = DecryptedSharesAggregationCircuit; + let e3_id_str = request.e3_id.to_string(); + let proof = circuit + .prove(prover, &req.params_preset, &circuit_data, &e3_id_str) + .map_err(|e| { + ComputeRequestError::new( + ComputeRequestErrorKind::Zk(ZkEventError::ProofGenerationFailed(format!( + "C7 proof[{}]: {}", + i, e + ))), + request.clone(), + ) + })?; + proofs.push(proof); + } + + // 4. Return response + Ok(ComputeResponse::zk( + ZkResponse::DecryptedSharesAggregation(DecryptedSharesAggregationProofResponse { proofs }), + request.correlation_id, + request.e3_id, + )) +} diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index dab3cb06b9..5fa0fa62a1 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -222,8 +222,44 @@ async fn setup_test_zk_backend() -> (ZkBackend, tempfile::TempDir) { .await .unwrap(); - let backend = ZkBackend::new(BBPath::Default(bb_binary), circuits_dir, work_dir); + // Copy C6 (threshold/share_decryption) circuit for C6 verification + let threshold_share_dec_circuit_dir = + circuits_dir.join("threshold").join("share_decryption"); + tokio::fs::create_dir_all(&threshold_share_dec_circuit_dir) + .await + .unwrap(); + tokio::fs::copy( + threshold_target.join("share_decryption.json"), + threshold_share_dec_circuit_dir.join("share_decryption.json"), + ) + .await + .unwrap(); + tokio::fs::copy( + threshold_target.join("share_decryption.vk"), + threshold_share_dec_circuit_dir.join("share_decryption.vk"), + ) + .await + .unwrap(); + // Copy C7 (decrypted_shares_aggregation_mod) circuit + let dsa_circuit_dir = circuits_dir + .join("threshold") + .join("decrypted_shares_aggregation_mod"); + tokio::fs::create_dir_all(&dsa_circuit_dir).await.unwrap(); + tokio::fs::copy( + threshold_target.join("decrypted_shares_aggregation_mod.json"), + dsa_circuit_dir.join("decrypted_shares_aggregation_mod.json"), + ) + .await + .unwrap(); + tokio::fs::copy( + threshold_target.join("decrypted_shares_aggregation_mod.vk"), + dsa_circuit_dir.join("decrypted_shares_aggregation_mod.vk"), + ) + .await + .unwrap(); + + let backend = ZkBackend::new(BBPath::Default(bb_binary), circuits_dir, work_dir); (backend, temp) } else { println!("bb binary not found locally, downloading via ensure_installed()..."); @@ -627,10 +663,14 @@ async fn test_trbfv_actor() -> Result<()> { // Wait for KeyshareCreated + C1 verification + C5 proof + PublicKeyAggregated // - KeyshareCreated × 5 (forwarded from committee nodes) - // - ComputeRequest (C1 proof verification dispatched by PublicKeyAggregator) - // - ComputeResponse (C1 proof verification result) - // - ComputeRequest (C5 PkAggregation proof dispatched by PublicKeyAggregator) - // - ComputeResponse (C5 PkAggregation proof result) + // - ShareVerificationDispatched (C1 proof verification dispatched by PublicKeyAggregator) + // - ComputeRequest (C1 ZK verification) + // - ComputeResponse (C1 ZK verification result) + // - ShareVerificationComplete (C1 verification done) + // - PkAggregationProofPending (C5 proof requested by PublicKeyAggregator) + // - ComputeRequest (C5 proof generation) + // - ComputeResponse (C5 proof result) + // - PkAggregationProofSigned (C5 proof signed by ProofRequestActor) // - PublicKeyAggregated × 1 let shares_to_pubkey_agg_timer = Instant::now(); let expected = vec![ @@ -639,10 +679,14 @@ async fn test_trbfv_actor() -> Result<()> { "KeyshareCreated", "KeyshareCreated", "KeyshareCreated", + "ShareVerificationDispatched", "ComputeRequest", "ComputeResponse", + "ShareVerificationComplete", + "PkAggregationProofPending", "ComputeRequest", "ComputeResponse", + "PkAggregationProofSigned", "PublicKeyAggregated", ]; let h = nodes @@ -719,13 +763,21 @@ async fn test_trbfv_actor() -> Result<()> { // The collector sees: // - 1 CiphertextOutputPublished (from shared bus) // - 5 DecryptionshareCreated (from simulate_libp2p, passes is_forwardable_event) - // - 1 ComputeRequest (PlaintextAggregation on node 0's own bus) - // - 1 ComputeResponse (PlaintextAggregation on node 0's own bus) - // - 1 PlaintextAggregated (from node 0's own aggregation actor) + // - 1 ShareVerificationDispatched (C6 verification dispatched by ThresholdPlaintextAggregator) + // - 1 ComputeRequest (C6 ZK verification) + // - 1 ComputeResponse (C6 ZK verification result) + // - 1 ShareVerificationComplete (C6 verification done) + // - 1 ComputeRequest (TrBFV CalculateThresholdDecryption) + // - 1 ComputeResponse (TrBFV CalculateThresholdDecryption) + // - 1 AggregationProofPending (C7 proof requested by ThresholdPlaintextAggregator) + // - 1 ComputeRequest (C7 proof generation) + // - 1 ComputeResponse (C7 proof result) + // - 1 AggregationProofSigned (C7 proof signed by ProofRequestActor) + // - 1 PlaintextAggregated (with C7 proofs) // Internal events from committee nodes (ComputeRequest/Response for CalculateDecryptionShare) // stay on their local buses. - // Total: 1 + 5 + 1 + 1 + 1 = 9 events - let expected_count = 1 + 5 + 1 + 1 + 1; + // Total: 1 + 5 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 1 = 17 events + let expected_count = 1 + 5 + 1 + 2 + 1 + 2 + 1 + 2 + 1 + 1; let h = nodes .take_history_with_timeout(0, expected_count, Duration::from_secs(1000)) @@ -738,6 +790,7 @@ async fn test_trbfv_actor() -> Result<()> { let Some(EnclaveEventData::PlaintextAggregated(PlaintextAggregated { decrypted_output: plaintext, + aggregation_proofs, .. })) = h.last().map(|e| e.get_data()) else { @@ -747,6 +800,11 @@ async fn test_trbfv_actor() -> Result<()> { ) }; + assert!( + !aggregation_proofs.is_empty(), + "C7 proofs should be present in PlaintextAggregated" + ); + let results = plaintext .into_iter() .map(|a| decode_bytes_to_vec_u64(&a.extract_bytes()).expect("error decoding bytes")) @@ -835,11 +893,13 @@ async fn test_p2p_actor_forwards_events_to_network() -> Result<()> { let evt_1 = PlaintextAggregated { e3_id: E3id::new("1235", 1), decrypted_output: vec![ArcBytes::from_bytes(&[1, 2, 3, 4])], + aggregation_proofs: vec![], }; let evt_2 = PlaintextAggregated { e3_id: E3id::new("1236", 1), decrypted_output: vec![ArcBytes::from_bytes(&[1, 2, 3, 4])], + aggregation_proofs: vec![], }; let local_evt_3 = CiphernodeSelected { diff --git a/crates/zk-prover/src/actors/proof_request.rs b/crates/zk-prover/src/actors/proof_request.rs index 4b9e07a003..acaf63b3c1 100644 --- a/crates/zk-prover/src/actors/proof_request.rs +++ b/crates/zk-prover/src/actors/proof_request.rs @@ -10,13 +10,15 @@ 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, DecryptionKeyShared, DecryptionShareProofsPending, - DkgProofSigned, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCreated, - EncryptionKeyPending, EventContext, EventPublisher, EventSubscriber, EventType, - PkBfvProofRequest, PkGenerationProofSigned, Proof, ProofPayload, ProofType, Sequenced, - SignedProofPayload, ThresholdShare, ThresholdShareCreated, ThresholdSharePending, TypedEvent, - ZkRequest, ZkResponse, + AggregationProofPending, AggregationProofSigned, BusHandle, ComputeRequest, + ComputeRequestError, ComputeRequestErrorKind, ComputeResponse, ComputeResponseKind, + CorrelationId, DecryptionKeyShared, DecryptionShareProofSigned, DecryptionShareProofsPending, + DecryptionshareCreated, DkgProofSigned, E3Failed, E3Stage, E3id, EnclaveEvent, + EnclaveEventData, EncryptionKey, EncryptionKeyCreated, EncryptionKeyPending, EventContext, + EventPublisher, EventSubscriber, EventType, FailureReason, PkAggregationProofPending, + PkAggregationProofSigned, PkBfvProofRequest, PkGenerationProofSigned, Proof, ProofPayload, + ProofType, Sequenced, ShareDecryptionProofPending, SignedProofPayload, ThresholdShare, + ThresholdShareCreated, ThresholdSharePending, TypedEvent, ZkRequest, ZkResponse, }; use e3_utils::utility_types::ArcBytes; use e3_utils::NotifySync; @@ -159,6 +161,27 @@ impl PendingDecryptionProofs { } } +/// Pending C5 (PkAggregation) proof generation state. +#[derive(Clone, Debug)] +struct PendingPkAggregationProof { + ec: EventContext, +} + +/// Pending C6 (ShareDecryptionProof) proof generation state. +#[derive(Clone, Debug)] +struct PendingShareDecryptionProof { + party_id: u64, + node: String, + decryption_share: Vec, + ec: EventContext, +} + +/// Pending C7 (DecryptedSharesAggregation) proof generation state. +#[derive(Clone, Debug)] +struct PendingAggregationProof { + ec: EventContext, +} + /// Core actor that handles encryption key proof requests. /// /// Proofs are always wrapped in a [`SignedProofPayload`] before being published, @@ -170,10 +193,22 @@ pub struct ProofRequestActor { pending: HashMap, threshold_correlation: HashMap, pending_threshold: HashMap, - /// C4 proof staging: correlation → (e3_id, kind) + /// C4 proof staging: correlation -> (e3_id, kind) decryption_correlation: HashMap, /// C4 pending proofs per E3 pending_decryption: HashMap, + /// C6 proof staging: correlation -> e3_id + share_decryption_correlation: HashMap, + /// C6 pending proofs per E3 + pending_share_decryption: HashMap, + /// C5 proof staging: correlation -> e3_id + pk_aggregation_correlation: HashMap, + /// C5 pending proofs per E3 + pending_pk_aggregation: HashMap, + /// C7 proof staging: correlation -> e3_id + aggregation_correlation: HashMap, + /// C7 pending proofs per E3 + pending_aggregation: HashMap, } impl ProofRequestActor { @@ -186,6 +221,12 @@ impl ProofRequestActor { threshold_correlation: HashMap::new(), decryption_correlation: HashMap::new(), pending_decryption: HashMap::new(), + share_decryption_correlation: HashMap::new(), + pending_share_decryption: HashMap::new(), + pk_aggregation_correlation: HashMap::new(), + pending_pk_aggregation: HashMap::new(), + aggregation_correlation: HashMap::new(), + pending_aggregation: HashMap::new(), } } @@ -196,6 +237,9 @@ impl ProofRequestActor { bus.subscribe(EventType::ComputeRequestError, addr.clone().into()); bus.subscribe(EventType::ThresholdSharePending, addr.clone().into()); bus.subscribe(EventType::DecryptionShareProofsPending, addr.clone().into()); + bus.subscribe(EventType::ShareDecryptionProofPending, addr.clone().into()); + bus.subscribe(EventType::PkAggregationProofPending, addr.clone().into()); + bus.subscribe(EventType::AggregationProofPending, addr.clone().into()); addr } @@ -393,6 +437,18 @@ impl ProofRequestActor { self.handle_threshold_proof_response(&msg.correlation_id, resp.proof.clone()); } } + ComputeResponseKind::Zk(ZkResponse::ThresholdShareDecryption(resp)) => { + self.handle_share_decryption_proof_response( + &msg.correlation_id, + resp.proofs.clone(), + ); + } + ComputeResponseKind::Zk(ZkResponse::PkAggregation(resp)) => { + self.handle_pk_aggregation_proof_response(&msg.correlation_id, resp.proof.clone()); + } + ComputeResponseKind::Zk(ZkResponse::DecryptedSharesAggregation(resp)) => { + self.handle_aggregation_proof_response(&msg.correlation_id, resp.proofs.clone()); + } _ => {} } } @@ -579,6 +635,277 @@ impl ProofRequestActor { } } + /// Handle ShareDecryptionProofPending: dispatch C6 proof generation. + fn handle_share_decryption_proof_pending( + &mut self, + msg: TypedEvent, + ) { + let (msg, ec) = msg.into_components(); + let e3_id = msg.e3_id.clone(); + + if self.pending_share_decryption.contains_key(&e3_id) { + warn!( + "Duplicate ShareDecryptionProofPending for E3 {} — ignoring", + e3_id + ); + return; + } + + self.pending_share_decryption.insert( + e3_id.clone(), + PendingShareDecryptionProof { + party_id: msg.party_id, + node: msg.node, + decryption_share: msg.decryption_share, + ec: ec.clone(), + }, + ); + + let correlation_id = CorrelationId::new(); + self.share_decryption_correlation + .insert(correlation_id, e3_id.clone()); + + info!( + "Requesting C6 ThresholdShareDecryption proof for E3 {}", + e3_id + ); + if let Err(err) = self.bus.publish( + ComputeRequest::zk( + ZkRequest::ThresholdShareDecryption(msg.proof_request), + correlation_id, + e3_id.clone(), + ), + ec, + ) { + error!("Failed to publish C6 proof request: {err}"); + self.share_decryption_correlation.remove(&correlation_id); + self.pending_share_decryption.remove(&e3_id); + } + } + + /// Handle C6 proof response — sign proofs and publish DecryptionshareCreated. + fn handle_share_decryption_proof_response( + &mut self, + correlation_id: &CorrelationId, + proofs: Vec, + ) { + let Some(e3_id) = self.share_decryption_correlation.remove(correlation_id) else { + return; + }; + + let Some(pending) = self.pending_share_decryption.remove(&e3_id) else { + error!( + "No pending share decryption proof for E3 {} — orphan correlation", + e3_id + ); + return; + }; + + // Sign each C6 proof + let mut signed_proofs = Vec::with_capacity(proofs.len()); + for proof in proofs { + let Some(signed) = self.sign_proof(&e3_id, ProofType::T5ShareDecryption, proof) else { + error!("Failed to sign C6 proof — DecryptionshareCreated will not be published"); + return; + }; + signed_proofs.push(signed); + } + + info!( + "All C6 proofs signed for E3 {} party {} (signer: {})", + e3_id, + pending.party_id, + self.signer.address() + ); + + let ec = pending.ec; + + match self.bus.publish( + DecryptionshareCreated { + party_id: pending.party_id, + node: pending.node, + e3_id: e3_id.clone(), + decryption_share: pending.decryption_share, + signed_decryption_proofs: signed_proofs, + }, + ec.clone(), + ) { + Ok(_) => { + if let Err(err) = self.bus.publish( + DecryptionShareProofSigned { + e3_id: e3_id.clone(), + }, + ec, + ) { + error!("Failed to publish DecryptionShareProofSigned: {err}"); + } + } + Err(err) => { + error!("Failed to publish DecryptionshareCreated: {err}"); + } + } + } + + /// Handle PkAggregationProofPending: dispatch C5 proof generation. + fn handle_pk_aggregation_proof_pending(&mut self, msg: TypedEvent) { + let (msg, ec) = msg.into_components(); + let e3_id = msg.e3_id.clone(); + + if self.pending_pk_aggregation.contains_key(&e3_id) { + warn!( + "Duplicate PkAggregationProofPending for E3 {} — ignoring", + e3_id + ); + return; + } + + self.pending_pk_aggregation + .insert(e3_id.clone(), PendingPkAggregationProof { ec: ec.clone() }); + + let correlation_id = CorrelationId::new(); + self.pk_aggregation_correlation + .insert(correlation_id, e3_id.clone()); + + info!("Requesting C5 PkAggregation proof for E3 {}", e3_id); + if let Err(err) = self.bus.publish( + ComputeRequest::zk( + ZkRequest::PkAggregation(msg.proof_request), + correlation_id, + e3_id.clone(), + ), + ec, + ) { + error!("Failed to publish C5 proof request: {err}"); + self.pk_aggregation_correlation.remove(&correlation_id); + self.pending_pk_aggregation.remove(&e3_id); + } + } + + /// Handle C5 proof response — sign proof and publish PkAggregationProofSigned. + fn handle_pk_aggregation_proof_response( + &mut self, + correlation_id: &CorrelationId, + proof: Proof, + ) { + let Some(e3_id) = self.pk_aggregation_correlation.remove(correlation_id) else { + return; + }; + + let Some(pending) = self.pending_pk_aggregation.remove(&e3_id) else { + error!( + "No pending pk aggregation proof for E3 {} — orphan correlation", + e3_id + ); + return; + }; + + let Some(signed) = self.sign_proof(&e3_id, ProofType::C5PkAggregation, proof) else { + error!("Failed to sign C5 proof — PkAggregationProofSigned will not be published"); + return; + }; + + info!( + "C5 proof signed for E3 {} (signer: {})", + e3_id, + self.signer.address() + ); + + if let Err(err) = self.bus.publish( + PkAggregationProofSigned { + e3_id: e3_id.clone(), + signed_proof: signed, + }, + pending.ec, + ) { + error!("Failed to publish PkAggregationProofSigned: {err}"); + } + } + + /// Handle AggregationProofPending: dispatch C7 proof generation. + fn handle_aggregation_proof_pending(&mut self, msg: TypedEvent) { + let (msg, ec) = msg.into_components(); + let e3_id = msg.e3_id.clone(); + + if self.pending_aggregation.contains_key(&e3_id) { + warn!( + "Duplicate AggregationProofPending for E3 {} — ignoring", + e3_id + ); + return; + } + + self.pending_aggregation + .insert(e3_id.clone(), PendingAggregationProof { ec: ec.clone() }); + + let correlation_id = CorrelationId::new(); + self.aggregation_correlation + .insert(correlation_id, e3_id.clone()); + + info!( + "Requesting C7 DecryptedSharesAggregation proof for E3 {}", + e3_id + ); + if let Err(err) = self.bus.publish( + ComputeRequest::zk( + ZkRequest::DecryptedSharesAggregation(msg.proof_request), + correlation_id, + e3_id.clone(), + ), + ec, + ) { + error!("Failed to publish C7 proof request: {err}"); + self.aggregation_correlation.remove(&correlation_id); + self.pending_aggregation.remove(&e3_id); + } + } + + /// Handle C7 proof response — sign proofs and publish AggregationProofSigned. + fn handle_aggregation_proof_response( + &mut self, + correlation_id: &CorrelationId, + proofs: Vec, + ) { + let Some(e3_id) = self.aggregation_correlation.remove(correlation_id) else { + return; + }; + + let Some(pending) = self.pending_aggregation.remove(&e3_id) else { + error!( + "No pending aggregation proof for E3 {} — orphan correlation", + e3_id + ); + return; + }; + + // Sign each C7 proof + let mut signed_proofs = Vec::with_capacity(proofs.len()); + for proof in proofs { + let Some(signed) = + self.sign_proof(&e3_id, ProofType::T6DecryptedSharesAggregation, proof) + else { + error!("Failed to sign C7 proof — AggregationProofSigned will not be published"); + return; + }; + signed_proofs.push(signed); + } + + info!( + "All C7 proofs signed for E3 {} (signer: {})", + e3_id, + self.signer.address() + ); + + if let Err(err) = self.bus.publish( + AggregationProofSigned { + e3_id: e3_id.clone(), + signed_proofs, + }, + pending.ec, + ) { + error!("Failed to publish AggregationProofSigned: {err}"); + } + } + fn handle_threshold_proof_response(&mut self, correlation_id: &CorrelationId, proof: Proof) { let Some((e3_id, kind)) = self.threshold_correlation.remove(correlation_id) else { return; @@ -632,6 +959,20 @@ impl ProofRequestActor { } } + fn sign_and_group_proofs( + &self, + e3_id: &E3id, + proof_type: ProofType, + proofs: impl Iterator, + ) -> Option>> { + let mut map: BTreeMap> = BTreeMap::new(); + for (recipient, proof) in proofs { + let signed = self.sign_proof(e3_id, proof_type, proof)?; + map.entry(recipient).or_default().push(signed); + } + Some(map) + } + fn publish_threshold_share_with_proofs(&mut self, pending: PendingThresholdProofs) { let e3_id = &pending.e3_id; let party_id = pending.full_share.party_id; @@ -667,37 +1008,29 @@ impl ProofRequestActor { return; }; - // Sign C3a proofs (SkShareEncryption) — keyed by (recipient, row) - let mut signed_c3a_map: BTreeMap> = BTreeMap::new(); - for ((_recipient, _row), proof) in &pending.sk_share_encryption_proofs { - if let Some(signed) = - self.sign_proof(e3_id, ProofType::C3aSkShareEncryption, proof.clone()) - { - signed_c3a_map.entry(*_recipient).or_default().push(signed); - } else { - error!( - "Failed to sign C3a proof for recipient {} — shares will not be published", - _recipient - ); - return; - } - } + let Some(signed_c3a_map) = self.sign_and_group_proofs( + e3_id, + ProofType::C3aSkShareEncryption, + pending + .sk_share_encryption_proofs + .iter() + .map(|((recipient, _row), proof)| (*recipient, proof.clone())), + ) else { + error!("Failed to sign C3a proofs — shares will not be published"); + return; + }; - // Sign C3b proofs (ESmShareEncryption) — keyed by (esi_index, recipient, row) - let mut signed_c3b_map: BTreeMap> = BTreeMap::new(); - for ((_esi, _recipient, _row), proof) in &pending.e_sm_share_encryption_proofs { - if let Some(signed) = - self.sign_proof(e3_id, ProofType::C3bESmShareEncryption, proof.clone()) - { - signed_c3b_map.entry(*_recipient).or_default().push(signed); - } else { - error!( - "Failed to sign C3b proof for recipient {} — shares will not be published", - _recipient - ); - return; - } - } + let Some(signed_c3b_map) = self.sign_and_group_proofs( + e3_id, + ProofType::C3bESmShareEncryption, + pending + .e_sm_share_encryption_proofs + .iter() + .map(|((_esi, recipient, _row), proof)| (*recipient, proof.clone())), + ) else { + error!("Failed to sign C3b proofs — shares will not be published"); + return; + }; info!( "All proofs signed for E3 {} party {} (signer: {})", @@ -868,6 +1201,7 @@ impl ProofRequestActor { } fn handle_compute_request_error(&mut self, msg: TypedEvent) { + let (msg, ec) = msg.into_components(); let ComputeRequestErrorKind::Zk(err) = msg.get_err() else { return; }; @@ -877,6 +1211,7 @@ impl ProofRequestActor { "T0 proof request failed for E3 {}: {err} — key will not be published without proof", pending.e3_id ); + return; } if let Some((e3_id, kind)) = self.threshold_correlation.remove(msg.correlation_id()) { @@ -887,6 +1222,7 @@ impl ProofRequestActor { self.threshold_correlation .retain(|_, (eid, _)| *eid != e3_id); self.pending_threshold.remove(&e3_id); + return; } if let Some((e3_id, kind)) = self.decryption_correlation.remove(msg.correlation_id()) { @@ -897,6 +1233,66 @@ impl ProofRequestActor { self.decryption_correlation .retain(|_, (eid, _)| *eid != e3_id); self.pending_decryption.remove(&e3_id); + return; + } + + if let Some(e3_id) = self + .share_decryption_correlation + .remove(msg.correlation_id()) + { + error!( + "C6 proof request failed for E3 {}: {err} — DecryptionshareCreated will not be published", + e3_id + ); + self.pending_share_decryption.remove(&e3_id); + if let Err(e) = self.bus.publish( + E3Failed { + e3_id, + failed_at_stage: E3Stage::CiphertextReady, + reason: FailureReason::DecryptionInvalidShares, + }, + ec.clone(), + ) { + error!("Failed to publish E3Failed for C6 error: {e}"); + } + return; + } + + if let Some(e3_id) = self.pk_aggregation_correlation.remove(msg.correlation_id()) { + error!( + "C5 proof request failed for E3 {}: {err} — PkAggregationProofSigned will not be published", + e3_id + ); + self.pending_pk_aggregation.remove(&e3_id); + if let Err(e) = self.bus.publish( + E3Failed { + e3_id, + failed_at_stage: E3Stage::CommitteeFinalized, + reason: FailureReason::DKGInvalidShares, + }, + ec.clone(), + ) { + error!("Failed to publish E3Failed for C5 error: {e}"); + } + return; + } + + if let Some(e3_id) = self.aggregation_correlation.remove(msg.correlation_id()) { + error!( + "C7 proof request failed for E3 {}: {err} — AggregationProofSigned will not be published", + e3_id + ); + self.pending_aggregation.remove(&e3_id); + if let Err(e) = self.bus.publish( + E3Failed { + e3_id, + failed_at_stage: E3Stage::CiphertextReady, + reason: FailureReason::DecryptionInvalidShares, + }, + ec, + ) { + error!("Failed to publish E3Failed for C7 error: {e}"); + } } } } @@ -927,6 +1323,15 @@ impl Handler for ProofRequestActor { EnclaveEventData::DecryptionShareProofsPending(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::ShareDecryptionProofPending(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::PkAggregationProofPending(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::AggregationProofPending(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } _ => (), } } @@ -991,3 +1396,39 @@ impl Handler> for ProofRequestActor { self.handle_decryption_share_proofs_pending(msg) } } + +impl Handler> for ProofRequestActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_share_decryption_proof_pending(msg) + } +} + +impl Handler> for ProofRequestActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_pk_aggregation_proof_pending(msg) + } +} + +impl Handler> for ProofRequestActor { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + self.handle_aggregation_proof_pending(msg) + } +} diff --git a/crates/zk-prover/src/actors/share_verification.rs b/crates/zk-prover/src/actors/share_verification.rs index 439122f6ef..2e005df4df 100644 --- a/crates/zk-prover/src/actors/share_verification.rs +++ b/crates/zk-prover/src/actors/share_verification.rs @@ -38,7 +38,6 @@ use tracing::{error, info, warn}; /// ECDSA validation result for a single party. struct EcdsaPartyResult { - sender_party_id: u64, passed: bool, /// The pair (signed_payload, recovered_address) of the first failing proof, if any. failed_payload: Option<(SignedProofPayload, Option
)>, @@ -55,9 +54,6 @@ struct PendingVerification { pre_dishonest: BTreeSet, /// Party IDs dispatched for ZK verification (for cross-checking results). dispatched_party_ids: HashSet, - /// Signed payloads for each party, indexed by party_id. - /// Used for SignedProofFailed emission when ZK also fails. - party_signed_payloads: HashMap>, /// Recovered address for each party (from ECDSA step). party_addresses: HashMap, } @@ -97,9 +93,16 @@ impl ShareVerificationActor { let (msg, ec) = msg.into_components(); let e3_id = msg.e3_id.clone(); + info!( + "handling ShareVerificationDispatched {:?}, {:?}", + e3_id, msg.kind + ); + match msg.kind { - VerificationKind::ShareProofs => { - self.verify_share_proofs(e3_id, msg.share_proofs, msg.pre_dishonest, ec); + VerificationKind::ShareProofs + | VerificationKind::ThresholdDecryptionProofs + | VerificationKind::PkGenerationProofs => { + self.verify_share_proofs(e3_id, msg.kind, msg.share_proofs, msg.pre_dishonest, ec); } VerificationKind::DecryptionProofs => { self.verify_decryption_proofs(e3_id, msg.decryption_proofs, msg.pre_dishonest, ec); @@ -107,18 +110,24 @@ impl ShareVerificationActor { } } - /// C2/C3 verification: ECDSA check on each party, then dispatch ZK. + /// C2/C3/C6/C1 verification: ECDSA check on each party, then dispatch ZK. fn verify_share_proofs( &mut self, e3_id: E3id, + kind: VerificationKind, party_proofs: Vec, pre_dishonest: BTreeSet, ec: EventContext, ) { let e3_id_str = e3_id.to_string(); + let label = match &kind { + VerificationKind::ShareProofs => "C2/C3", + VerificationKind::ThresholdDecryptionProofs => "C6", + VerificationKind::PkGenerationProofs => "C1", + VerificationKind::DecryptionProofs => "C4", + }; let mut ecdsa_dishonest = HashSet::new(); let mut ecdsa_passed_parties = Vec::new(); - let mut party_signed_payloads: HashMap> = HashMap::new(); let mut party_addresses: HashMap = HashMap::new(); for party in &party_proofs { @@ -126,9 +135,8 @@ impl ShareVerificationActor { party.sender_party_id, &party.signed_proofs, &e3_id_str, - "C2/C3", + label, ); - party_signed_payloads.insert(party.sender_party_id, party.signed_proofs.clone()); if result.passed { ecdsa_passed_parties.push(party.clone()); } else { @@ -154,7 +162,7 @@ impl ShareVerificationActor { // All parties failed ECDSA — publish result immediately let mut all_dishonest: BTreeSet = pre_dishonest; all_dishonest.extend(ecdsa_dishonest); - self.publish_complete(e3_id, VerificationKind::ShareProofs, all_dishonest, ec); + self.publish_complete(e3_id, kind, all_dishonest, ec); return; } @@ -168,12 +176,11 @@ impl ShareVerificationActor { correlation_id, PendingVerification { e3_id: e3_id.clone(), - kind: VerificationKind::ShareProofs, + kind: kind.clone(), ec: ec.clone(), ecdsa_dishonest, pre_dishonest, dispatched_party_ids, - party_signed_payloads, party_addresses, }, ); @@ -187,13 +194,13 @@ impl ShareVerificationActor { ); if let Err(err) = self.bus.publish(request, ec.clone()) { - error!("Failed to dispatch ZK verification: {err}"); + error!("Failed to dispatch {} ZK verification: {err}", label); if let Some(pending) = self.pending.remove(&correlation_id) { let mut all_dishonest: BTreeSet = pending.pre_dishonest; all_dishonest.extend(pending.ecdsa_dishonest); // Dispatched parties were never ZK-verified — treat as dishonest all_dishonest.extend(pending.dispatched_party_ids); - self.publish_complete(e3_id, VerificationKind::ShareProofs, all_dishonest, ec); + self.publish_complete(e3_id, kind, all_dishonest, ec); } } } @@ -209,7 +216,6 @@ impl ShareVerificationActor { let e3_id_str = e3_id.to_string(); let mut ecdsa_dishonest = HashSet::new(); let mut ecdsa_passed_parties = Vec::new(); - let mut party_signed_payloads: HashMap> = HashMap::new(); let mut party_addresses: HashMap = HashMap::new(); for party in &party_proofs { @@ -227,7 +233,6 @@ impl ShareVerificationActor { &e3_id_str, "C4", ); - party_signed_payloads.insert(party.sender_party_id, all_signed_cloned); if result.passed { ecdsa_passed_parties.push(party.clone()); @@ -269,7 +274,6 @@ impl ShareVerificationActor { ecdsa_dishonest, pre_dishonest, dispatched_party_ids, - party_signed_payloads, party_addresses, }, ); @@ -316,7 +320,6 @@ impl ShareVerificationActor { label, sender_party_id, signed.payload.e3_id, e3_id_str ); return EcdsaPartyResult { - sender_party_id, passed: false, failed_payload: Some((signed.clone(), expected_addr)), }; @@ -333,7 +336,6 @@ impl ShareVerificationActor { label, sender_party_id ); return EcdsaPartyResult { - sender_party_id, passed: false, failed_payload: Some((signed.clone(), Some(addr))), }; @@ -348,7 +350,6 @@ impl ShareVerificationActor { label, sender_party_id, signed.payload.proof_type, e ); return EcdsaPartyResult { - sender_party_id, passed: false, failed_payload: Some((signed.clone(), expected_addr)), }; @@ -363,7 +364,6 @@ impl ShareVerificationActor { label, sender_party_id, expected_circuits, signed.payload.proof.circuit ); return EcdsaPartyResult { - sender_party_id, passed: false, failed_payload: Some((signed.clone(), expected_addr)), }; @@ -371,7 +371,6 @@ impl ShareVerificationActor { } EcdsaPartyResult { - sender_party_id, passed: true, failed_payload: None, } @@ -388,7 +387,9 @@ impl ShareVerificationActor { let zk_results: Vec = match (&pending.kind, msg.response) { ( - VerificationKind::ShareProofs, + VerificationKind::ShareProofs + | VerificationKind::ThresholdDecryptionProofs + | VerificationKind::PkGenerationProofs, ComputeResponseKind::Zk(ZkResponse::VerifyShareProofs(r)), ) => r.party_results, ( diff --git a/templates/default/deployed_contracts.json b/templates/default/deployed_contracts.json index 1f39f056e7..dbb0bb6664 100644 --- a/templates/default/deployed_contracts.json +++ b/templates/default/deployed_contracts.json @@ -21,21 +21,21 @@ }, "localhost": { "PoseidonT3": { - "blockNumber": 9, + "blockNumber": 6, "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" }, "MockUSDC": { "constructorArgs": { "initialSupply": "1000000" }, - "blockNumber": 10, + "blockNumber": 7, "address": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" }, "EnclaveToken": { "constructorArgs": { "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 11, + "blockNumber": 8, "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" }, "EnclaveTicketToken": { @@ -44,14 +44,14 @@ "registry": "0x0000000000000000000000000000000000000001", "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 13, + "blockNumber": 10, "address": "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" }, "SlashingManager": { "constructorArgs": { "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }, - "blockNumber": 14, + "blockNumber": 11, "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" }, "CiphernodeRegistryOwnable": { @@ -66,7 +66,7 @@ "proxyAdminAddress": "0x9bd03768a7DCc129555dE410FF8E85528A4F88b5", "implementationAddress": "0x0165878A594ca255338adfa4d48449f69242Eb8F" }, - "blockNumber": 15, + "blockNumber": 12, "address": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" }, "BondingRegistry": { @@ -88,7 +88,7 @@ "proxyAdminAddress": "0x8aCd85898458400f7Db866d53FCFF6f0D49741FF", "implementationAddress": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" }, - "blockNumber": 16, + "blockNumber": 13, "address": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" }, "Enclave": { @@ -111,7 +111,7 @@ "proxyAdminAddress": "0x8dAF17A20c9DBA35f005b6324F493785D239719d", "implementationAddress": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" }, - "blockNumber": 19, + "blockNumber": 16, "address": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" }, "E3RefundManager": { @@ -127,28 +127,28 @@ "proxyAdminAddress": "0x32467b43BFa67273FC7dDda0999Ee9A12F2AaA08", "implementationAddress": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" }, - "blockNumber": 21, + "blockNumber": 18, "address": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" }, "MockComputeProvider": { - "blockNumber": 25, + "blockNumber": 20, "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d" }, "MockDecryptionVerifier": { - "blockNumber": 26, + "blockNumber": 21, "address": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" }, "MockE3Program": { - "blockNumber": 27, + "blockNumber": 22, "address": "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E" }, "ImageID": { "address": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9", - "blockNumber": 31 + "blockNumber": 26 }, "MyProgram": { "address": "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8", - "blockNumber": 33 + "blockNumber": 28 } } } \ No newline at end of file diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index cbbafd874c..80234fb404 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -4,19 +4,19 @@ chains: contracts: enclave: address: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" - deploy_block: 19 + deploy_block: 16 ciphernode_registry: address: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" - deploy_block: 15 + deploy_block: 12 bonding_registry: address: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" - deploy_block: 16 + deploy_block: 13 fee_token: address: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" - deploy_block: 10 + deploy_block: 7 e3_program: address: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" - deploy_block: 33 + deploy_block: 28 program: dev: true nodes: