diff --git a/Cargo.lock b/Cargo.lock index 991041a2d5..8fbf7c3276 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2774,6 +2774,7 @@ dependencies = [ "e3-multithread", "e3-request", "e3-sortition", + "e3-trbfv", "e3-utils", "tracing", ] @@ -2940,6 +2941,7 @@ dependencies = [ "e3-trbfv", "e3-utils", "futures-util", + "hex", "once_cell", "proptest", "rand 0.8.5", @@ -3078,6 +3080,7 @@ dependencies = [ "e3-request", "e3-trbfv", "e3-utils", + "fhe", "fhe-traits", "ndarray", "rand 0.8.5", diff --git a/crates/ciphernode-builder/Cargo.toml b/crates/ciphernode-builder/Cargo.toml index 21a7aa068a..2603e56169 100644 --- a/crates/ciphernode-builder/Cargo.toml +++ b/crates/ciphernode-builder/Cargo.toml @@ -22,5 +22,6 @@ e3-keyshare.workspace = true e3-multithread.workspace = true e3-request.workspace = true e3-sortition.workspace = true +e3-trbfv.workspace = true e3-utils.workspace = true tracing.workspace = true diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index 0a88d6793a..54874e5de7 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -438,12 +438,14 @@ impl CiphernodeBuilder { if let Some(KeyshareKind::Threshold) = self.keyshare { let multithread = self.ensure_multithread(); + let share_encryption_params = e3_trbfv::helpers::get_share_encryption_params(); info!("Setting up ThresholdKeyshareExtension"); e3_builder = e3_builder.with(ThresholdKeyshareExtension::create( &bus, &self.cipher, &multithread, &addr, + share_encryption_params, )) } diff --git a/crates/events/Cargo.toml b/crates/events/Cargo.toml index aa58f52e17..0eb2104289 100644 --- a/crates/events/Cargo.toml +++ b/crates/events/Cargo.toml @@ -18,6 +18,7 @@ bs58 = { workspace = true } chrono = { workspace = true } derivative = { workspace = true } futures-util = { workspace = true } +hex = { workspace = true } once_cell = { workspace = true } rand = { workspace = true } serde = { workspace = true } diff --git a/crates/events/src/enclave_event/encryption_key_collection_failed.rs b/crates/events/src/enclave_event/encryption_key_collection_failed.rs new file mode 100644 index 0000000000..da330e93fc --- /dev/null +++ b/crates/events/src/enclave_event/encryption_key_collection_failed.rs @@ -0,0 +1,24 @@ +// 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 actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct EncryptionKeyCollectionFailed { + pub e3_id: E3id, + pub reason: String, + pub missing_parties: Vec, +} + +impl Display for EncryptionKeyCollectionFailed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/events/src/enclave_event/encryption_key_created.rs b/crates/events/src/enclave_event/encryption_key_created.rs new file mode 100644 index 0000000000..e3deb25af7 --- /dev/null +++ b/crates/events/src/enclave_event/encryption_key_created.rs @@ -0,0 +1,37 @@ +// 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 actix::Message; +use derivative::Derivative; +use e3_utils::utility_types::ArcBytes; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{self, Display}, + sync::Arc, +}; + +#[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derivative(Debug)] +pub struct EncryptionKey { + pub party_id: u64, + #[derivative(Debug(format_with = "e3_utils::formatters::hexf"))] + pub pk_bfv: ArcBytes, +} + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct EncryptionKeyCreated { + pub e3_id: E3id, + pub key: Arc, + pub external: bool, +} + +impl Display for EncryptionKeyCreated { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index 0f7f2b07ad..c29cc9a50e 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -19,6 +19,8 @@ mod die; mod e3_request_complete; mod e3_requested; mod enclave_error; +mod encryption_key_collection_failed; +mod encryption_key_created; mod keyshare_created; mod operator_activation_changed; mod plaintext_aggregated; @@ -27,6 +29,7 @@ mod publickey_aggregated; mod publish_document; mod shutdown; mod test_event; +mod threshold_share_collection_failed; mod threshold_share_created; mod ticket_balance_updated; mod ticket_generated; @@ -47,6 +50,8 @@ pub use die::*; pub use e3_request_complete::*; pub use e3_requested::*; pub use enclave_error::*; +pub use encryption_key_collection_failed::*; +pub use encryption_key_created::*; pub use keyshare_created::*; pub use operator_activation_changed::*; pub use plaintext_aggregated::*; @@ -56,6 +61,7 @@ pub use publish_document::*; pub use shutdown::*; use strum::IntoStaticStr; pub use test_event::*; +pub use threshold_share_collection_failed::*; pub use threshold_share_created::*; pub use ticket_balance_updated::*; pub use ticket_generated::*; @@ -112,6 +118,9 @@ pub enum EnclaveEventData { Shutdown(Shutdown), DocumentReceived(DocumentReceived), ThresholdShareCreated(ThresholdShareCreated), + EncryptionKeyCreated(EncryptionKeyCreated), + EncryptionKeyCollectionFailed(EncryptionKeyCollectionFailed), + ThresholdShareCollectionFailed(ThresholdShareCollectionFailed), /// This is a test event to use in testing TestEvent(TestEvent), } @@ -291,6 +300,7 @@ impl EnclaveEvent { EnclaveEventData::CommitteeFinalized(ref data) => Some(data.e3_id.clone()), EnclaveEventData::TicketGenerated(ref data) => Some(data.e3_id.clone()), EnclaveEventData::TicketSubmitted(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::EncryptionKeyCreated(ref data) => Some(data.e3_id.clone()), _ => None, } } @@ -322,7 +332,10 @@ impl_into_event_data!( Shutdown, TestEvent, DocumentReceived, - ThresholdShareCreated + ThresholdShareCreated, + EncryptionKeyCreated, + EncryptionKeyCollectionFailed, + ThresholdShareCollectionFailed ); impl TryFrom<&EnclaveEvent> for EnclaveError { diff --git a/crates/events/src/enclave_event/publish_document/mod.rs b/crates/events/src/enclave_event/publish_document/mod.rs index 91b476a885..f0e3e7e9e0 100644 --- a/crates/events/src/enclave_event/publish_document/mod.rs +++ b/crates/events/src/enclave_event/publish_document/mod.rs @@ -11,7 +11,7 @@ use std::fmt::{self, Display}; use actix::Message; use chrono::{serde::ts_seconds, DateTime, Duration, Utc}; use e3_utils::ArcBytes; -use filter::Filter; +pub use filter::Filter; use serde::{Deserialize, Serialize}; use tracing::warn; diff --git a/crates/events/src/enclave_event/threshold_share_collection_failed.rs b/crates/events/src/enclave_event/threshold_share_collection_failed.rs new file mode 100644 index 0000000000..dd81aac693 --- /dev/null +++ b/crates/events/src/enclave_event/threshold_share_collection_failed.rs @@ -0,0 +1,24 @@ +// 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 actix::Message; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +#[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct ThresholdShareCollectionFailed { + pub e3_id: E3id, + pub reason: String, + pub missing_parties: Vec, +} + +impl Display for ThresholdShareCollectionFailed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} diff --git a/crates/events/src/enclave_event/threshold_share_created.rs b/crates/events/src/enclave_event/threshold_share_created.rs index d07fbd84b1..e9794a0397 100644 --- a/crates/events/src/enclave_event/threshold_share_created.rs +++ b/crates/events/src/enclave_event/threshold_share_created.rs @@ -7,7 +7,7 @@ use crate::E3id; use actix::Message; use derivative::Derivative; -use e3_trbfv::shares::{PvwEncrypted, SharedSecret}; +use e3_trbfv::shares::BfvEncryptedShares; use e3_utils::utility_types::ArcBytes; use serde::{Deserialize, Serialize}; use std::{ @@ -15,22 +15,46 @@ use std::{ sync::Arc, }; -/// Type Representing Pvw encrypted bytes -pub type PvwBytes = Vec; - -/// PVW encrypted shares list for a party in the DKG +/// BFV-encrypted shares list for a party in the DKG. +/// +/// Each party broadcasts their encrypted shares to all other parties. +/// Each recipient can only decrypt the share meant for them using their +/// BFV secret key. #[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derivative(Debug)] pub struct ThresholdShare { - /// The publishers party_id + /// The publisher's party_id pub party_id: u64, - /// The publishers public key share + /// The publisher's TrBFV public key share #[derivative(Debug(format_with = "e3_utils::formatters::hexf"))] pub pk_share: ArcBytes, - /// PVW encrypted sk_sss list with index determining party_id - pub sk_sss: PvwEncrypted, - /// PVW encrypted esi_sss list with index determining party_id - pub esi_sss: Vec>, + /// BFV-encrypted sk_sss - each recipient can decrypt their share + pub sk_sss: BfvEncryptedShares, + /// BFV-encrypted esi_sss - one per secret key (sk), each recipient can decrypt their share + pub esi_sss: Vec, +} + +impl ThresholdShare { + /// Extract only the shares meant for a specific party. + pub fn extract_for_party(&self, recipient_party_id: usize) -> Option { + let sk_sss = self.sk_sss.extract_for_party(recipient_party_id)?; + let esi_sss: Option> = self + .esi_sss + .iter() + .map(|shares| shares.extract_for_party(recipient_party_id)) + .collect(); + + esi_sss.map(|esi_sss| Self { + party_id: self.party_id, + pk_share: self.pk_share.clone(), + sk_sss, + esi_sss, + }) + } + + pub fn num_parties(&self) -> usize { + self.sk_sss.len() + } } #[derive(Message, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -38,6 +62,7 @@ pub struct ThresholdShare { pub struct ThresholdShareCreated { pub e3_id: E3id, pub share: Arc, + pub target_party_id: u64, pub external: bool, } diff --git a/crates/keyshare/Cargo.toml b/crates/keyshare/Cargo.toml index 278044d111..46da25753f 100644 --- a/crates/keyshare/Cargo.toml +++ b/crates/keyshare/Cargo.toml @@ -20,6 +20,7 @@ e3-multithread = { workspace = true } e3-request = { workspace = true } e3-trbfv = { workspace = true } e3-utils = { workspace = true } +fhe = { workspace = true } fhe-traits = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } diff --git a/crates/keyshare/src/encryption_key_collector.rs b/crates/keyshare/src/encryption_key_collector.rs new file mode 100644 index 0000000000..b9bc2db99b --- /dev/null +++ b/crates/keyshare/src/encryption_key_collector.rs @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, + time::{Duration, Instant}, +}; + +use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Message, SpawnHandle}; +use e3_events::{E3id, EncryptionKey, EncryptionKeyCollectionFailed, EncryptionKeyCreated}; +use e3_trbfv::PartyId; +use tracing::{info, warn}; + +const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(60); + +use crate::ThresholdKeyshare; + +/// State of the collector +pub enum CollectorState { + /// Currently collecting keys + Collecting, + /// All keys have been collected + Finished, + /// Collection timed out + TimedOut, +} + +/// Message sent when all encryption keys have been collected. +/// +/// This contains all parties' BFV public keys, sorted by party_id, +/// ready to be used for encrypting shares. +#[derive(Message)] +#[rtype(result = "()")] +pub struct AllEncryptionKeysCollected { + /// All collected encryption keys, sorted by party_id + pub keys: Vec>, +} + +impl From>> for AllEncryptionKeysCollected { + fn from(value: HashMap>) -> Self { + // Sort by party_id for deterministic ordering + let mut keys: Vec<_> = value.into_values().collect(); + keys.sort_by_key(|k| k.party_id); + AllEncryptionKeysCollected { keys } + } +} + +/// Message sent when encryption key collection times out. +#[derive(Message, Clone, Debug)] +#[rtype(result = "()")] +pub struct EncryptionKeyCollectionTimeout; + +/// Actor that collects BFV encryption keys from all parties. +/// +/// Once all keys are collected, it sends `AllEncryptionKeysCollected` to the parent +/// `ThresholdKeyshare` actor. If collection times out, it sends `EncryptionKeyCollectionFailed`. +pub struct EncryptionKeyCollector { + e3_id: E3id, + todo: HashSet, + parent: Addr, + state: CollectorState, + keys: HashMap>, + timeout_handle: Option, +} + +impl EncryptionKeyCollector { + pub fn setup(parent: Addr, total: u64, e3_id: E3id) -> Addr { + let collector = Self { + e3_id, + todo: (0..total).collect(), + parent, + state: CollectorState::Collecting, + keys: HashMap::new(), + timeout_handle: None, + }; + collector.start() + } +} + +impl Actor for EncryptionKeyCollector { + type Context = actix::Context; + + fn started(&mut self, ctx: &mut Self::Context) { + info!( + e3_id = %self.e3_id, + "EncryptionKeyCollector started, scheduling timeout in {:?}", + DEFAULT_COLLECTION_TIMEOUT + ); + + let handle = ctx.notify_later(EncryptionKeyCollectionTimeout, DEFAULT_COLLECTION_TIMEOUT); + self.timeout_handle = Some(handle); + } +} + +impl Handler for EncryptionKeyCollector { + type Result = (); + fn handle(&mut self, msg: EncryptionKeyCreated, ctx: &mut Self::Context) -> Self::Result { + let start = Instant::now(); + info!("EncryptionKeyCollector: EncryptionKeyCreated received"); + + // Ignore if already finished or timed out + if !matches!(self.state, CollectorState::Collecting) { + info!( + "EncryptionKeyCollector is not collecting (state: {:?}), ignoring", + match self.state { + CollectorState::Collecting => "Collecting", + CollectorState::Finished => "Finished", + CollectorState::TimedOut => "TimedOut", + } + ); + return; + } + + let pid = msg.key.party_id; + info!("EncryptionKeyCollector: party_id = {}", pid); + + let Some(_) = self.todo.take(&pid) else { + info!( + "Error: {} was not in encryption key collector's ID list", + pid + ); + return; + }; + + info!( + "Inserting encryption key... waiting on: {}", + self.todo.len() + ); + self.keys.insert(pid, msg.key); + + if self.todo.is_empty() { + info!("All encryption keys collected!"); + self.state = CollectorState::Finished; + + // Cancel the timeout since we're done + if let Some(handle) = self.timeout_handle.take() { + ctx.cancel_future(handle); + } + + let event: AllEncryptionKeysCollected = self.keys.clone().into(); + self.parent.do_send(event); + } + + info!( + "Finished processing EncryptionKeyCreated in {:?}", + start.elapsed() + ); + } +} + +impl Handler for EncryptionKeyCollector { + type Result = (); + fn handle( + &mut self, + _: EncryptionKeyCollectionTimeout, + ctx: &mut Self::Context, + ) -> Self::Result { + // Only handle timeout if we're still collecting + if !matches!(self.state, CollectorState::Collecting) { + return; + } + + warn!( + e3_id = %self.e3_id, + missing_parties = ?self.todo, + "Encryption key collection timed out, {} parties missing", + self.todo.len() + ); + + self.state = CollectorState::TimedOut; + + // Notify parent of failure + let missing_parties: Vec = self.todo.iter().copied().collect(); + self.parent.do_send(EncryptionKeyCollectionFailed { + e3_id: self.e3_id.clone(), + reason: format!( + "Timeout waiting for encryption keys from {} parties", + missing_parties.len() + ), + missing_parties, + }); + + // Stop the actor + ctx.stop(); + } +} diff --git a/crates/keyshare/src/ext.rs b/crates/keyshare/src/ext.rs index 39b8db852a..2c0750224e 100644 --- a/crates/keyshare/src/ext.rs +++ b/crates/keyshare/src/ext.rs @@ -117,6 +117,7 @@ pub struct ThresholdKeyshareExtension { cipher: Arc, address: String, multithread: Addr, + share_encryption_params: Arc, } impl ThresholdKeyshareExtension { @@ -125,12 +126,14 @@ impl ThresholdKeyshareExtension { cipher: &Arc, multithread: &Addr, address: &str, + share_encryption_params: Arc, ) -> Box { Box::new(Self { bus: bus.clone(), cipher: cipher.to_owned(), multithread: multithread.clone(), address: address.to_owned(), + share_encryption_params, }) } } @@ -173,6 +176,7 @@ impl E3Extension for ThresholdKeyshareExtension { cipher: self.cipher.clone(), multithread: self.multithread.clone(), state: container, + share_encryption_params: self.share_encryption_params.clone(), }) .start() .into(), @@ -203,6 +207,7 @@ impl E3Extension for ThresholdKeyshareExtension { cipher: self.cipher.clone(), multithread: self.multithread.clone(), state, + share_encryption_params: self.share_encryption_params.clone(), }) .start() .into(); diff --git a/crates/keyshare/src/lib.rs b/crates/keyshare/src/lib.rs index 42ffcc1c62..98f52ab4a5 100644 --- a/crates/keyshare/src/lib.rs +++ b/crates/keyshare/src/lib.rs @@ -4,11 +4,13 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +mod encryption_key_collector; pub mod ext; mod keyshare; mod repo; mod threshold_keyshare; mod threshold_share_collector; +pub use encryption_key_collector::{AllEncryptionKeysCollected, EncryptionKeyCollector}; pub use keyshare::*; pub use repo::*; pub use threshold_keyshare::*; diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index d79398f13a..e577d81201 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -10,8 +10,10 @@ use e3_crypto::{Cipher, SensitiveBytes}; use e3_data::Persistable; use e3_events::{ prelude::*, BusHandle, CiphernodeSelected, CiphertextOutputPublished, ComputeRequest, - ComputeResponse, DecryptionshareCreated, E3id, EnclaveEvent, EnclaveEventData, KeyshareCreated, - PartyId, ThresholdShare, ThresholdShareCreated, + ComputeResponse, DecryptionshareCreated, Die, E3RequestComplete, E3id, EnclaveEvent, + EnclaveEventData, EncryptionKey, EncryptionKeyCollectionFailed, EncryptionKeyCreated, + KeyshareCreated, PartyId, ThresholdShare, ThresholdShareCollectionFailed, + ThresholdShareCreated, }; use e3_fhe::create_crp; use e3_multithread::Multithread; @@ -22,20 +24,24 @@ use e3_trbfv::{ }, gen_esi_sss::{GenEsiSssRequest, GenEsiSssResponse}, gen_pk_share_and_sk_sss::GenPkShareAndSkSssRequest, - shares::{EncryptableVec, Encrypted, PvwEncrypted, ShamirShare, SharedSecret}, + helpers::{deserialize_secret_key, serialize_secret_key}, + shares::{BfvEncryptedShares, EncryptableVec, Encrypted, ShamirShare, SharedSecret}, TrBFVConfig, TrBFVRequest, TrBFVResponse, }; use e3_utils::{bail, to_ordered_vec, utility_types::ArcBytes}; -use fhe_traits::Serialize; -use rand::SeedableRng; +use fhe::bfv::BfvParameters; +use fhe::bfv::{PublicKey, SecretKey}; +use fhe_traits::{DeserializeParametrized, Serialize}; +use rand::{rngs::OsRng, SeedableRng}; use rand_chacha::ChaCha20Rng; use std::{ collections::HashMap, mem, sync::{Arc, Mutex}, }; -use tracing::{error, info}; +use tracing::{error, info, warn}; +use crate::encryption_key_collector::{AllEncryptionKeysCollected, EncryptionKeyCollector}; use crate::threshold_share_collector::ThresholdShareCollector; #[derive(Message, Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] @@ -68,11 +74,21 @@ impl From>> for AllThresholdSharesCollected { } } -#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct CollectingEncryptionKeysData { + sk_bfv: SensitiveBytes, + pk_bfv: ArcBytes, + ciphernode_selected: CiphernodeSelected, +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct GeneratingThresholdShareData { pk_share: Option, sk_sss: Option>, esi_sss: Option>>, + sk_bfv: SensitiveBytes, + pk_bfv: ArcBytes, + collected_encryption_keys: Vec>, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] @@ -80,6 +96,8 @@ pub struct AggregatingDecryptionKey { pk_share: ArcBytes, sk_sss: Encrypted, esi_sss: Vec>, + sk_bfv: SensitiveBytes, + collected_encryption_keys: Vec>, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] @@ -96,11 +114,12 @@ pub struct Decrypting { es_poly_sum: Vec, } -// TODO: Add GeneratingPvwKey state #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum KeyshareState { // Before anything Init, + // Collecting BFV encryption keys from all parties + CollectingEncryptionKeys(CollectingEncryptionKeysData), // Generating TrBFV share material GeneratingThresholdShare(GeneratingThresholdShareData), // Collecting remaining TrBFV shares to aggregate decryption key @@ -123,7 +142,8 @@ impl KeyshareState { true } else { match (self, &new_state) { - (K::Init, K::GeneratingThresholdShare(_)) => true, + (K::Init, K::CollectingEncryptionKeys(_)) => true, + (K::CollectingEncryptionKeys(_), K::GeneratingThresholdShare(_)) => true, (K::GeneratingThresholdShare(_), K::AggregatingDecryptionKey(_)) => true, (K::AggregatingDecryptionKey(_), K::ReadyForDecryption(_)) => true, (K::ReadyForDecryption(_), K::Decrypting(_)) => true, @@ -146,6 +166,7 @@ impl KeyshareState { pub fn variant_name(&self) -> &'static str { match self { Self::Init => "Init", + Self::CollectingEncryptionKeys(_) => "CollectingEncryptionKeys", Self::GeneratingThresholdShare(_) => "GeneratingThresholdShare", Self::AggregatingDecryptionKey(_) => "AggregatingDecryptionKey", Self::ReadyForDecryption(_) => "ReadyForDecryption", @@ -228,6 +249,16 @@ impl ThresholdKeyshareState { } } +impl TryInto for ThresholdKeyshareState { + type Error = anyhow::Error; + fn try_into(self) -> std::result::Result { + match self.state { + KeyshareState::CollectingEncryptionKeys(s) => Ok(s), + _ => Err(anyhow!("Invalid state")), + } + } +} + impl TryInto for ThresholdKeyshareState { type Error = anyhow::Error; fn try_into(self) -> std::result::Result { @@ -273,14 +304,17 @@ pub struct ThresholdKeyshareParams { pub cipher: Arc, pub multithread: Addr, pub state: Persistable, + pub share_encryption_params: Arc, } pub struct ThresholdKeyshare { bus: BusHandle, cipher: Arc, decryption_key_collector: Option>, + encryption_key_collector: Option>, multithread: Addr, state: Persistable, + share_encryption_params: Arc, } impl ThresholdKeyshare { @@ -289,8 +323,10 @@ impl ThresholdKeyshare { bus: params.bus, cipher: params.cipher, decryption_key_collector: None, + encryption_key_collector: None, multithread: params.multithread, state: params.state, + share_encryption_params: params.share_encryption_params, } } } @@ -312,9 +348,31 @@ impl ThresholdKeyshare { "Setting up key collector for addr: {} and {} nodes", state.address, state.threshold_n ); + let e3_id = state.e3_id.clone(); + let threshold_n = state.threshold_n; let addr = self .decryption_key_collector - .get_or_insert_with(|| ThresholdShareCollector::setup(self_addr, state.threshold_n)); + .get_or_insert_with(|| ThresholdShareCollector::setup(self_addr, threshold_n, e3_id)); + Ok(addr.clone()) + } + + pub fn ensure_encryption_key_collector( + &mut self, + self_addr: Addr, + ) -> Result> { + let Some(state) = self.state.get() else { + bail!("State not found on threshold keyshare. This should not happen."); + }; + + info!( + "Setting up encryption key collector for addr: {} and {} nodes", + state.address, state.threshold_n + ); + let e3_id = state.e3_id.clone(); + let threshold_n = state.threshold_n; + let addr = self + .encryption_key_collector + .get_or_insert_with(|| EncryptionKeyCollector::setup(self_addr, threshold_n, e3_id)); Ok(addr.clone()) } @@ -323,34 +381,105 @@ impl ThresholdKeyshare { msg: ThresholdShareCreated, self_addr: Addr, ) -> Result<()> { - info!("Received ThresholdShareCreated forwarding to collector!"); + let state = self.state.try_get()?; + let my_party_id = state.party_id; + + // Filter: only process shares intended for this party + if msg.target_party_id != my_party_id { + return Ok(()); + } + + info!( + "Received ThresholdShareCreated from party {} for us (party {}), forwarding to collector!", + msg.share.party_id, my_party_id + ); let collector = self.ensure_collector(self_addr)?; info!("got collector address!"); collector.do_send(msg); Ok(()) } - /// 1. CiphernodeSelected + pub fn handle_encryption_key_created( + &mut self, + msg: EncryptionKeyCreated, + self_addr: Addr, + ) -> Result<()> { + info!("Received EncryptionKeyCreated forwarding to encryption key collector!"); + let collector = self.ensure_encryption_key_collector(self_addr)?; + collector.do_send(msg); + Ok(()) + } + + /// 1. CiphernodeSelected - Generate BFV keys and start collecting pub fn handle_ciphernode_selected( &mut self, msg: CiphernodeSelected, address: Addr, ) -> Result<()> { - // Ensure the collector is created let _ = self.ensure_collector(address.clone()); - // Initialize State + let _ = self.ensure_encryption_key_collector(address.clone()); + + let params = self.share_encryption_params.clone(); + let mut rng = OsRng; + let sk_bfv = SecretKey::random(¶ms, &mut rng); + let pk_bfv = PublicKey::new(&sk_bfv, &mut rng); + + let sk_bytes = serialize_secret_key(&sk_bfv)?; + let sk_bfv_encrypted = SensitiveBytes::new(sk_bytes, &self.cipher)?; + let pk_bfv_bytes = ArcBytes::from_bytes(&pk_bfv.to_bytes()); + + self.state.try_mutate(|s| { + s.new_state(KeyshareState::CollectingEncryptionKeys( + CollectingEncryptionKeysData { + sk_bfv: sk_bfv_encrypted.clone(), + pk_bfv: pk_bfv_bytes.clone(), + ciphernode_selected: msg.clone(), + }, + )) + })?; + + let state = self.state.try_get()?; + self.bus.publish(EncryptionKeyCreated { + e3_id: state.e3_id.clone(), + key: Arc::new(EncryptionKey { + party_id: state.party_id, + pk_bfv: pk_bfv_bytes, + }), + external: false, + })?; + + Ok(()) + } + + /// 1a. AllEncryptionKeysCollected - All BFV keys received, start share generation + pub fn handle_all_encryption_keys_collected( + &mut self, + msg: AllEncryptionKeysCollected, + address: Addr, + ) -> Result<()> { + info!( + "AllEncryptionKeysCollected - {} keys received", + msg.keys.len() + ); + + let current: CollectingEncryptionKeysData = self.state.try_get()?.try_into()?; + self.state.try_mutate(|s| { s.new_state(KeyshareState::GeneratingThresholdShare( GeneratingThresholdShareData { sk_sss: None, pk_share: None, esi_sss: None, + sk_bfv: current.sk_bfv, + pk_bfv: current.pk_bfv, + collected_encryption_keys: msg.keys, }, )) })?; - address.do_send(GenEsiSss(msg.clone())); - address.do_send(GenPkShareAndSkSss(msg)); + address.do_send(GenEsiSss(current.ciphernode_selected.clone())); + address.do_send(GenPkShareAndSkSss(current.ciphernode_selected)); + Ok(()) } @@ -398,22 +527,25 @@ impl ThresholdKeyshare { info!("try_store_esi_sss"); let current: GeneratingThresholdShareData = s.clone().try_into()?; - let pk_share = current.pk_share; - let sk_sss = current.sk_sss; - let next = match (pk_share, sk_sss) { + let next = match (current.pk_share, current.sk_sss) { // If the other shares are here then transition to aggregation (Some(pk_share), Some(sk_sss)) => { K::AggregatingDecryptionKey(AggregatingDecryptionKey { esi_sss, pk_share, sk_sss, + sk_bfv: current.sk_bfv, + collected_encryption_keys: current.collected_encryption_keys, }) } - // If the other shares are not here yet then dont transition + // If the other shares are not here yet then don't transition (None, None) => K::GeneratingThresholdShare(GeneratingThresholdShareData { esi_sss: Some(esi_sss), pk_share: None, sk_sss: None, + sk_bfv: current.sk_bfv, + pk_bfv: current.pk_bfv, + collected_encryption_keys: current.collected_encryption_keys, }), _ => bail!("Inconsistent state!"), }; @@ -460,7 +592,7 @@ impl ThresholdKeyshare { Ok(event) } - /// 3a. GenPkShareAndSkSss + /// 3a. GenPkShareAndSkSss result pub fn handle_gen_pk_share_and_sk_sss_response(&mut self, res: ComputeResponse) -> Result<()> { let ComputeResponse::TrBFV(TrBFVResponse::GenPkShareAndSkSss(output)) = res else { bail!("Error extracting data from compute process") @@ -471,14 +603,15 @@ impl ThresholdKeyshare { self.state.try_mutate(|s| { info!("try_store_pk_share_and_sk_sss"); let current: GeneratingThresholdShareData = s.clone().try_into()?; - let esi_sss = current.esi_sss; - let next = match esi_sss { + let next = match current.esi_sss { // If the esi shares are here then transition to aggregation Some(esi_sss) => { KeyshareState::AggregatingDecryptionKey(AggregatingDecryptionKey { esi_sss, pk_share, sk_sss, + sk_bfv: current.sk_bfv, + collected_encryption_keys: current.collected_encryption_keys, }) } // If esi shares are not here yet then don't transition @@ -486,6 +619,9 @@ impl ThresholdKeyshare { esi_sss: None, pk_share: Some(pk_share), sk_sss: Some(sk_sss), + sk_bfv: current.sk_bfv, + pk_bfv: current.pk_bfv, + collected_encryption_keys: current.collected_encryption_keys, }), }; s.new_state(next) @@ -501,7 +637,7 @@ impl ThresholdKeyshare { Ok(()) } - /// 4. SharesGenerated + /// 4. SharesGenerated - Encrypt shares with BFV and publish pub fn handle_shares_generated(&self) -> Result<()> { let Some(ThresholdKeyshareState { state: @@ -509,6 +645,7 @@ impl ThresholdKeyshare { pk_share, sk_sss, esi_sss, + collected_encryption_keys, .. }), party_id, @@ -519,70 +656,120 @@ impl ThresholdKeyshare { bail!("Invalid state!"); }; - let decrypted = sk_sss.decrypt(&self.cipher)?; - let sk_sss: PvwEncrypted = PvwEncrypted::new(decrypted)?; - let esi_sss = esi_sss + // Get collected BFV public keys from all parties (now from persisted state) + let encryption_keys = &collected_encryption_keys; + + // Convert to BFV public keys + let params = self.share_encryption_params.clone(); + let recipient_pks: Vec = encryption_keys + .iter() + .map(|k| { + PublicKey::from_bytes(&k.pk_bfv, ¶ms) + .map_err(|e| anyhow!("Failed to deserialize BFV public key: {:?}", e)) + }) + .collect::>()?; + + // Decrypt our shares from local storage + let decrypted_sk_sss: SharedSecret = sk_sss.decrypt(&self.cipher)?; + let decrypted_esi_sss: Vec = esi_sss .into_iter() - .map(|s| PvwEncrypted::new(s.decrypt(&self.cipher)?)) + .map(|s| s.decrypt(&self.cipher)) .collect::>()?; - self.bus.publish(ThresholdShareCreated { - e3_id, - share: Arc::new(ThresholdShare { - party_id, - esi_sss, - pk_share, - sk_sss, - }), - external: false, - })?; + // Encrypt shares for all recipients using BFV + let mut rng = OsRng; + let encrypted_sk_sss = + BfvEncryptedShares::encrypt_all(&decrypted_sk_sss, &recipient_pks, ¶ms, &mut rng)?; + + let encrypted_esi_sss: Vec = decrypted_esi_sss + .iter() + .map(|esi| BfvEncryptedShares::encrypt_all(esi, &recipient_pks, ¶ms, &mut rng)) + .collect::>()?; + + // Create the full share with all parties' encrypted data + let full_share = ThresholdShare { + party_id, + pk_share, + sk_sss: encrypted_sk_sss, + esi_sss: encrypted_esi_sss, + }; + + // Domain-level splitting: publish one ThresholdShareCreated per recipient party + // Each party only receives the share data meant for them + let num_parties = full_share.num_parties(); + info!( + "Publishing ThresholdShare for E3 {} to {} parties", + e3_id, num_parties + ); + + for recipient_party_id in 0..num_parties { + let party_share = full_share + .extract_for_party(recipient_party_id) + .ok_or_else(|| { + anyhow!("Failed to extract share for party {}", recipient_party_id) + })?; + + self.bus.publish(ThresholdShareCreated { + e3_id: e3_id.clone(), + share: Arc::new(party_share), + target_party_id: recipient_party_id as u64, + external: false, + })?; + } Ok(()) } /// 5. AllThresholdSharesCollected. This is fired after the ThresholdShareCreated events are /// aggregateed in the decryption_key_collector::ThresholdShareCollector + /// 5. AllThresholdSharesCollected - Decrypt received shares using BFV and aggregate pub fn handle_all_threshold_shares_collected( &self, msg: AllThresholdSharesCollected, ) -> Result { info!("AllThresholdSharesCollected"); let cipher = self.cipher.clone(); - let state = self.state.get().ok_or(anyhow!("No state found"))?; + let state = self.state.try_get()?; let party_id = state.party_id as usize; let trbfv_config = state.get_trbfv_config(); - // Shares are in order of party_id - let received_sss: Vec = msg - .shares - .clone() - .into_iter() - .map(|ts| ts.sk_sss.clone().pvw_decrypt()) - .collect::>()?; + // Get our BFV secret key from state + let current: AggregatingDecryptionKey = state.clone().try_into()?; + let sk_bytes = current.sk_bfv.access(&cipher)?; + let params = self.share_encryption_params.clone(); + let sk_bfv = deserialize_secret_key(&sk_bytes, ¶ms)?; + let degree = params.degree(); - let received_esi_sss: Vec> = msg + // Decrypt our share from each sender using BFV + // Local share (from self) has all parties' shares, network shares are pre-extracted + let sk_sss_collected: Vec = msg .shares - .into_iter() + .iter() .map(|ts| { - ts.esi_sss - .clone() - .into_iter() - .map(|s| s.pvw_decrypt()) - .collect() + let idx = if ts.sk_sss.len() == 1 { 0 } else { party_id }; + let encrypted = ts + .sk_sss + .clone_share(idx) + .ok_or(anyhow!("No sk_sss share at index {}", idx))?; + encrypted.decrypt(&sk_bfv, ¶ms, degree) }) .collect::>()?; - let sk_sss_collected: Vec = received_sss - .into_iter() - .map(|s| s.extract_party_share(party_id)) - .collect::>()?; - - let esi_sss_collected: Vec> = received_esi_sss - .into_iter() - .map(|esi| { - esi.into_iter() - .map(|s| s.extract_party_share(party_id)) - .collect() + // Similarly decrypt esi_sss for each ciphertext + let esi_sss_collected: Vec> = msg + .shares + .iter() + .map(|ts| { + ts.esi_sss + .iter() + .map(|esi_shares| { + let idx = if esi_shares.len() == 1 { 0 } else { party_id }; + let encrypted = esi_shares + .clone_share(idx) + .ok_or(anyhow!("No esi_sss share at index {}", idx))?; + encrypted.decrypt(&sk_bfv, ¶ms, degree) + }) + .collect::>>() }) .collect::>()?; @@ -625,7 +812,7 @@ impl ThresholdKeyshare { s.new_state(next) })?; - let state = self.state.get().ok_or(anyhow!("No state found"))?; + let state = self.state.try_get()?; let e3_id = state.get_e3_id().clone(); let address = state.get_address().to_owned(); let current: ReadyForDecryption = state.clone().try_into()?; @@ -660,7 +847,7 @@ impl ThresholdKeyshare { })?; let ciphertext_output = msg.ciphertext_output; - let state = self.state.get().ok_or(anyhow!("No state found"))?; + let state = self.state.try_get()?; let decrypting: Decrypting = state.clone().try_into()?; let trbfv_config = state.get_trbfv_config(); let event = ComputeRequest::TrBFV(TrBFVRequest::CalculateDecryptionShare( @@ -683,7 +870,7 @@ impl ThresholdKeyshare { res: ComputeResponse, ) -> Result<()> { let msg: CalculateDecryptionShareResponse = res.try_into()?; - let state = self.state.get().ok_or(anyhow!("No state found"))?; + let state = self.state.try_get()?; let party_id = state.party_id; let node = state.address; let e3_id = state.e3_id; @@ -760,6 +947,10 @@ impl Handler for ThresholdKeyshare { EnclaveEventData::ThresholdShareCreated(data) => { let _ = self.handle_threshold_share_created(data, ctx.address()); } + EnclaveEventData::EncryptionKeyCreated(data) => { + let _ = self.handle_encryption_key_created(data, ctx.address()); + } + EnclaveEventData::E3RequestComplete(data) => ctx.notify(data), _ => (), } } @@ -795,6 +986,16 @@ impl Handler for ThresholdKeyshare { } } +impl Handler for ThresholdKeyshare { + type Result = (); + fn handle(&mut self, msg: AllEncryptionKeysCollected, ctx: &mut Self::Context) -> Self::Result { + match self.handle_all_encryption_keys_collected(msg, ctx.address()) { + Err(e) => error!("{e}"), + Ok(_) => (), + } + } +} + impl Handler for ThresholdKeyshare { type Result = ResponseActFuture; fn handle(&mut self, msg: AllThresholdSharesCollected, _: &mut Self::Context) -> Self::Result { @@ -814,3 +1015,73 @@ impl Handler for ThresholdKeyshare { ) } } + +impl Handler for ThresholdKeyshare { + type Result = (); + fn handle( + &mut self, + msg: EncryptionKeyCollectionFailed, + ctx: &mut Self::Context, + ) -> Self::Result { + warn!( + e3_id = %msg.e3_id, + missing_parties = ?msg.missing_parties, + "Encryption key collection failed: {}", + msg.reason + ); + + // Clear the collector reference since it's stopped + self.encryption_key_collector = None; + + // Publish failure event to event bus for sync tracking + if let Err(e) = self.bus.publish(msg) { + error!("Failed to publish EncryptionKeyCollectionFailed: {}", e); + } + + // Stop this actor since we can't proceed without all encryption keys + ctx.stop(); + } +} + +impl Handler for ThresholdKeyshare { + type Result = (); + fn handle( + &mut self, + msg: ThresholdShareCollectionFailed, + ctx: &mut Self::Context, + ) -> Self::Result { + warn!( + e3_id = %msg.e3_id, + missing_parties = ?msg.missing_parties, + "Threshold share collection failed: {}", + msg.reason + ); + + // Clear the collector reference since it's stopped + self.decryption_key_collector = None; + + // Publish failure event to event bus for sync tracking + if let Err(e) = self.bus.publish(msg) { + error!("Failed to publish ThresholdShareCollectionFailed: {}", e); + } + + ctx.stop(); + } +} + +impl Handler for ThresholdKeyshare { + type Result = (); + fn handle(&mut self, _: E3RequestComplete, ctx: &mut Self::Context) -> Self::Result { + self.encryption_key_collector = None; + self.decryption_key_collector = None; + ctx.notify(Die); + } +} + +impl Handler for ThresholdKeyshare { + type Result = (); + fn handle(&mut self, _: Die, ctx: &mut Self::Context) -> Self::Result { + warn!("ThresholdKeyshare is shutting down"); + ctx.stop(); + } +} diff --git a/crates/keyshare/src/threshold_share_collector.rs b/crates/keyshare/src/threshold_share_collector.rs index bd71a1f027..d0393c34cf 100644 --- a/crates/keyshare/src/threshold_share_collector.rs +++ b/crates/keyshare/src/threshold_share_collector.rs @@ -7,71 +7,109 @@ use std::{ collections::{HashMap, HashSet}, sync::Arc, - time::Instant, + time::{Duration, Instant}, }; -use actix::{Actor, Addr, Handler}; -use e3_events::{ThresholdShare, ThresholdShareCreated}; +use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Message, SpawnHandle}; +use e3_events::{E3id, ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated}; use e3_trbfv::PartyId; -use tracing::info; +use tracing::{info, warn}; use crate::{AllThresholdSharesCollected, ThresholdKeyshare}; -pub enum CollectorState { +const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(120); + +pub(crate) enum CollectorState { Collecting, Finished, + TimedOut, } +/// Message sent when threshold share collection times out. +#[derive(Message, Clone, Debug)] +#[rtype(result = "()")] +pub struct ThresholdShareCollectionTimeout; + pub struct ThresholdShareCollector { + e3_id: E3id, todo: HashSet, parent: Addr, state: CollectorState, shares: HashMap>, + timeout_handle: Option, } impl ThresholdShareCollector { - pub fn setup(parent: Addr, total: u64) -> Addr { - let addr = Self { + pub fn setup(parent: Addr, total: u64, e3_id: E3id) -> Addr { + let collector = Self { + e3_id, todo: (0..total).collect(), parent, state: CollectorState::Collecting, shares: HashMap::new(), - } - .start(); - addr + timeout_handle: None, + }; + collector.start() } } impl Actor for ThresholdShareCollector { type Context = actix::Context; + + fn started(&mut self, ctx: &mut Self::Context) { + info!( + e3_id = %self.e3_id, + "ThresholdShareCollector started, scheduling timeout in {:?}", + DEFAULT_COLLECTION_TIMEOUT + ); + // Schedule timeout + let handle = ctx.notify_later(ThresholdShareCollectionTimeout, DEFAULT_COLLECTION_TIMEOUT); + self.timeout_handle = Some(handle); + } } impl Handler for ThresholdShareCollector { type Result = (); - fn handle(&mut self, msg: ThresholdShareCreated, _: &mut Self::Context) -> Self::Result { + fn handle(&mut self, msg: ThresholdShareCreated, ctx: &mut Self::Context) -> Self::Result { let start = Instant::now(); info!("ThresholdShareCollector: ThresholdShareCreated received by collector"); - if let CollectorState::Finished = self.state { - info!("ThresholdShareCollector is finished so ignoring!"); + + // Ignore if already finished or timed out + if !matches!(self.state, CollectorState::Collecting) { + info!( + "ThresholdShareCollector is not collecting (state: {:?}), ignoring", + match self.state { + CollectorState::Collecting => "Collecting", + CollectorState::Finished => "Finished", + CollectorState::TimedOut => "TimedOut", + } + ); return; - }; + } let pid = msg.share.party_id; info!("ThresholdShareCollector party id: {}", pid); let Some(_) = self.todo.take(&pid) else { info!( - "Error: {} was not in decryption key collectors ID list", + "Error: {} was not in threshold share collector's ID list", pid ); return; }; info!("Inserting... waiting on: {}", self.todo.len()); self.shares.insert(pid, msg.share); - if self.todo.len() == 0 { - info!("We have recieved all the things"); + + if self.todo.is_empty() { + info!("We have received all threshold shares"); self.state = CollectorState::Finished; + + // Cancel the timeout since we're done + if let Some(handle) = self.timeout_handle.take() { + ctx.cancel_future(handle); + } + let event: AllThresholdSharesCollected = self.shares.clone().into(); - self.parent.do_send(event) + self.parent.do_send(event); } info!( "Finished processing ThresholdShareCreated in {:?}", @@ -79,3 +117,40 @@ impl Handler for ThresholdShareCollector { ); } } + +impl Handler for ThresholdShareCollector { + type Result = (); + fn handle( + &mut self, + _: ThresholdShareCollectionTimeout, + ctx: &mut Self::Context, + ) -> Self::Result { + // Only handle timeout if we're still collecting + if !matches!(self.state, CollectorState::Collecting) { + return; + } + + warn!( + e3_id = %self.e3_id, + missing_parties = ?self.todo, + "Threshold share collection timed out, {} parties missing", + self.todo.len() + ); + + self.state = CollectorState::TimedOut; + + // Notify parent of failure + let missing_parties: Vec = self.todo.iter().copied().collect(); + self.parent.do_send(ThresholdShareCollectionFailed { + e3_id: self.e3_id.clone(), + reason: format!( + "Timeout waiting for threshold shares from {} parties", + missing_parties.len() + ), + missing_parties, + }); + + // Stop the actor + ctx.stop(); + } +} diff --git a/crates/net/src/document_publisher.rs b/crates/net/src/document_publisher.rs index a0aac9e06f..8f46ce7c4b 100644 --- a/crates/net/src/document_publisher.rs +++ b/crates/net/src/document_publisher.rs @@ -15,8 +15,8 @@ use anyhow::Result; use chrono::{DateTime, Utc}; use e3_events::{ prelude::*, BusHandle, CiphernodeSelected, CorrelationId, DocumentKind, DocumentMeta, - DocumentReceived, E3RequestComplete, E3id, EType, EnclaveEvent, EnclaveEventData, Event, - PartyId, PublishDocumentRequested, ThresholdShareCreated, + DocumentReceived, E3RequestComplete, E3id, EType, EnclaveEvent, EnclaveEventData, + EncryptionKeyCreated, Event, Filter, PartyId, PublishDocumentRequested, ThresholdShareCreated, }; use e3_utils::retry::{retry_with_backoff, to_retry}; use e3_utils::ArcBytes; @@ -28,7 +28,7 @@ use std::{ time::{Duration, Instant}, }; use tokio::sync::{broadcast, mpsc}; -use tracing::{debug, error, warn}; +use tracing::{debug, error, info}; const KADEMLIA_PUT_TIMEOUT: Duration = Duration::from_secs(30); const KADEMLIA_GET_TIMEOUT: Duration = Duration::from_secs(30); @@ -75,6 +75,7 @@ impl DocumentPublisher { match event.get_data() { EnclaveEventData::PublishDocumentRequested(_) => true, EnclaveEventData::ThresholdShareCreated(_) => true, + EnclaveEventData::EncryptionKeyCreated(_) => true, _ => false, } } @@ -372,7 +373,13 @@ async fn broadcast_document_published_notification( .await } -/// Convert between ThresholdShareCreated and DocumentPublished events +/// Converts between internal events and network documents. +/// +/// Responsibilities: +/// - Outgoing: Converts ThresholdShareCreated → party-filtered PublishDocumentRequested +/// - Incoming: Converts DocumentReceived → ThresholdShareCreated/EncryptionKeyCreated +/// +/// Note: Party filtering is done by DocumentPublisher BEFORE fetching from DHT. pub struct EventConverter { bus: BusHandle, } @@ -380,15 +387,10 @@ pub struct EventConverter { #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] enum ReceivableDocument { ThresholdShareCreated(ThresholdShareCreated), + EncryptionKeyCreated(EncryptionKeyCreated), } impl ReceivableDocument { - pub fn get_e3_id(&self) -> &E3id { - match self { - ReceivableDocument::ThresholdShareCreated(d) => &d.e3_id, - } - } - pub fn to_bytes(&self) -> Result, bincode::Error> { bincode::serialize(self) } @@ -402,43 +404,97 @@ impl EventConverter { pub fn new(bus: &BusHandle) -> Self { Self { bus: bus.clone() } } + pub fn setup(bus: &BusHandle) -> Addr { let addr = Self::new(bus).start(); bus.subscribe("ThresholdShareCreated", addr.clone().into()); + bus.subscribe("EncryptionKeyCreated", addr.clone().into()); bus.subscribe("DocumentReceived", addr.clone().into()); addr } - /// Local node created a threshold share. Send it as a published document - pub fn handle_threshold_share_created(&self, msg: ThresholdShareCreated) -> Result<()> { - // If this is received from elsewhere - if msg.external { - return Ok(()); - } - let receivable = ReceivableDocument::ThresholdShareCreated(msg); + + /// Publish a receivable document with party filter + fn publish_filtered( + &self, + receivable: ReceivableDocument, + e3_id: &E3id, + party_id: u64, + ) -> Result<()> { let value = ArcBytes::from_bytes(&receivable.to_bytes()?); let meta = DocumentMeta::new( - receivable.get_e3_id().clone(), + e3_id.clone(), DocumentKind::TrBFV, - vec![], + vec![Filter::Item(party_id)], None, ); self.bus .publish(PublishDocumentRequested::new(meta, value))?; Ok(()) } - /// Received document externally - pub fn handle_document_received(&self, msg: DocumentReceived) -> Result<()> { - warn!("Converting DocumentReceived..."); + + /// Local node created a threshold share (already split per-party by ThresholdKeyshare). + /// Publishes the single-party document with appropriate filter. + pub fn handle_threshold_share_created(&self, msg: ThresholdShareCreated) -> Result<()> { + if msg.external { + return Ok(()); + } + let target_party_id = msg.target_party_id; + + info!( + "Publishing ThresholdShare from party {} for target party {} (E3 {})", + msg.share.party_id, target_party_id, msg.e3_id + ); + + self.publish_filtered( + ReceivableDocument::ThresholdShareCreated(msg.clone()), + &msg.e3_id, + target_party_id, + )?; + + Ok(()) + } + fn handle_encryption_key_created(&self, msg: EncryptionKeyCreated) -> Result<()> { + if msg.external { + return Ok(()); + } + let receivable = ReceivableDocument::EncryptionKeyCreated(msg.clone()); + let value = ArcBytes::from_bytes(&receivable.to_bytes()?); + let meta = DocumentMeta::new(msg.e3_id, DocumentKind::TrBFV, vec![], None); + self.bus + .publish(PublishDocumentRequested::new(meta, value))?; + Ok(()) + } + + /// Convert received document to internal events. + /// Note: Filtering already happened in DocumentPublisher before DHT fetch. + fn handle_document_received(&self, msg: DocumentReceived) -> Result<()> { let receivable = ReceivableDocument::from_bytes(&msg.value.extract_bytes())?; - let event = match receivable { - ReceivableDocument::ThresholdShareCreated(evt) => ThresholdShareCreated { - external: true, - e3_id: evt.e3_id, - share: evt.share, - }, - }; - self.bus.publish(event)?; + match receivable { + ReceivableDocument::ThresholdShareCreated(evt) => { + debug!( + "Received ThresholdShareCreated from party {} for target party {}", + evt.share.party_id, evt.target_party_id + ); + self.bus.publish(ThresholdShareCreated { + external: true, + e3_id: evt.e3_id, + share: evt.share, + target_party_id: evt.target_party_id, + })?; + } + ReceivableDocument::EncryptionKeyCreated(evt) => { + debug!( + "Received EncryptionKeyCreated from party {}", + evt.key.party_id + ); + self.bus.publish(EncryptionKeyCreated { + external: true, + e3_id: evt.e3_id, + key: evt.key, + })?; + } + } Ok(()) } } @@ -452,6 +508,7 @@ impl Handler for EventConverter { fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { match msg.into_data() { EnclaveEventData::ThresholdShareCreated(data) => ctx.notify(data), + EnclaveEventData::EncryptionKeyCreated(data) => ctx.notify(data), EnclaveEventData::DocumentReceived(data) => ctx.notify(data), _ => (), } @@ -468,6 +525,16 @@ impl Handler for EventConverter { } } +impl Handler for EventConverter { + type Result = (); + fn handle(&mut self, msg: EncryptionKeyCreated, _ctx: &mut Self::Context) -> Self::Result { + match self.handle_encryption_key_created(msg) { + Ok(_) => (), + Err(err) => error!("{err}"), + } + } +} + impl Handler for EventConverter { type Result = (); fn handle(&mut self, msg: DocumentReceived, _ctx: &mut Self::Context) -> Self::Result { diff --git a/crates/request/src/context.rs b/crates/request/src/context.rs index d45d3b44a0..8a9053ad05 100644 --- a/crates/request/src/context.rs +++ b/crates/request/src/context.rs @@ -80,13 +80,15 @@ impl E3Context { pub fn forward_message(&self, msg: &EnclaveEvent, buffer: &mut EventBuffer) { self.recipients().into_iter().for_each(|(key, recipient)| { + // Use composite key of e3_id:recipient_key to scope buffered events per E3 request + let buffer_key = format!("{}:{}", self.e3_id, key); if let Some(act) = recipient { act.do_send(msg.clone()); - for m in buffer.take(&key) { + for m in buffer.take(&buffer_key) { act.do_send(m); } } else { - buffer.add(&key, msg.clone()); + buffer.add(&buffer_key, msg.clone()); } }); } diff --git a/crates/test-helpers/src/usecase_helpers.rs b/crates/test-helpers/src/usecase_helpers.rs index c5698d7a0e..e1d1506df0 100644 --- a/crates/test-helpers/src/usecase_helpers.rs +++ b/crates/test-helpers/src/usecase_helpers.rs @@ -16,18 +16,27 @@ use e3_trbfv::{ gen_pk_share_and_sk_sss::{ gen_pk_share_and_sk_sss, GenPkShareAndSkSssRequest, GenPkShareAndSkSssResponse, }, - shares::{EncryptableVec, PvwEncrypted, PvwEncryptedVecExt, ShamirShare, SharedSecret}, + helpers::get_share_encryption_params, + shares::{BfvEncryptedShares, EncryptableVec, ShamirShare, SharedSecret}, TrBFVConfig, }; use e3_utils::{ArcBytes, SharedRng}; use fhe::{ - bfv::{BfvParameters, PublicKey}, + bfv::{BfvParameters, PublicKey, SecretKey}, mbfv::{AggregateIter, CommonRandomPoly, PublicKeyShare}, }; use fhe_traits::Serialize; +use rand::rngs::OsRng; // The following functions are designed to aid testing our usecases +/// Result of generating shares - includes the shares plus BFV keys for decryption +pub struct GeneratedShares { + pub shares: HashMap, + /// BFV secret keys for each party (for decryption in tests) + pub bfv_secret_keys: Vec, +} + pub fn generate_shares_hash_map( trbfv_config: &TrBFVConfig, esi_per_ct: u64, @@ -35,11 +44,24 @@ pub fn generate_shares_hash_map( crp: &CommonRandomPoly, rng: &SharedRng, cipher: &Cipher, -) -> Result> { - let threshold_n = trbfv_config.num_parties(); +) -> Result { + let threshold_n = trbfv_config.num_parties() as usize; + + // First, generate BFV encryption keys for all parties + let bfv_params = get_share_encryption_params(); + let mut bfv_rng = OsRng; + let mut bfv_secret_keys = Vec::with_capacity(threshold_n); + let mut bfv_public_keys = Vec::with_capacity(threshold_n); + + for _ in 0..threshold_n { + let sk = SecretKey::random(&bfv_params, &mut bfv_rng); + let pk = fhe::bfv::PublicKey::new(&sk, &mut bfv_rng); + bfv_secret_keys.push(sk); + bfv_public_keys.push(pk); + } let mut shares_hash_map = HashMap::new(); - for party_id in 0u64..threshold_n { + for party_id in 0u64..threshold_n as u64 { let GenEsiSssResponse { esi_sss } = gen_esi_sss( &rng, &cipher, @@ -59,24 +81,42 @@ pub fn generate_shares_hash_map( }, )?; - // Simulate actor boundry and SharesGenerated - let sk_sss = PvwEncrypted::new(sk_sss.decrypt(&cipher)?)?; - let esi_sss: Vec> = esi_sss + // Decrypt locally stored secrets + let decrypted_sk_sss: SharedSecret = sk_sss.decrypt(&cipher)?; + let decrypted_esi_sss: Vec = esi_sss .into_iter() - .map(|s| PvwEncrypted::new(s.decrypt(&cipher)?)) + .map(|s| s.decrypt(&cipher)) + .collect::>()?; + + // Encrypt shares for all recipients using BFV + let encrypted_sk_sss = BfvEncryptedShares::encrypt_all( + &decrypted_sk_sss, + &bfv_public_keys, + &bfv_params, + &mut bfv_rng, + )?; + + let encrypted_esi_sss: Vec = decrypted_esi_sss + .iter() + .map(|esi| { + BfvEncryptedShares::encrypt_all(esi, &bfv_public_keys, &bfv_params, &mut bfv_rng) + }) .collect::>()?; shares_hash_map.insert( party_id, ThresholdShare { party_id, - esi_sss, - sk_sss, + esi_sss: encrypted_esi_sss, + sk_sss: encrypted_sk_sss, pk_share, }, ); } - Ok(shares_hash_map) + Ok(GeneratedShares { + shares: shares_hash_map, + bfv_secret_keys, + }) } pub fn get_public_key( @@ -99,36 +139,44 @@ pub fn get_public_key( pub fn get_decryption_keys( shares: Vec, + bfv_secret_keys: &[SecretKey], cipher: &Cipher, trbfv_config: &TrBFVConfig, ) -> Result, SensitiveBytes)>> { - let threshold_n = trbfv_config.num_parties(); - let received_sss = shares - .iter() - .map(|ts| ts.sk_sss.clone().pvw_decrypt()) - .collect::>>()?; - - let received_esi_sss: Vec> = shares - .iter() - .map(|ts| ts.esi_sss.clone().to_vec_decrypted()) - .collect::>>()?; - - // Individualize based on node + let threshold_n = trbfv_config.num_parties() as usize; + let bfv_params = get_share_encryption_params(); + let degree = bfv_params.degree(); + + // Individualize based on node - each party decrypts their share from each sender let mut decryption_keys = HashMap::new(); - for party_id in 0..threshold_n as usize { - let sk_sss_collected = received_sss - .clone() - .into_iter() - .map(|sss| sss.extract_party_share(party_id)) - .collect::>>()?; + for party_id in 0..threshold_n { + let sk_bfv = &bfv_secret_keys[party_id]; - let esi_sss_collected: Vec> = received_esi_sss - .clone() - .into_iter() - .map(|s| { - s.into_iter() - .map(|ss| ss.extract_party_share(party_id)) - .collect() + // Decrypt sk_sss share from each sender using our BFV secret key + let sk_sss_collected: Vec = shares + .iter() + .map(|ts| { + let encrypted = ts + .sk_sss + .clone_share(party_id) + .ok_or_else(|| anyhow::anyhow!("No sk_sss share for party {}", party_id))?; + encrypted.decrypt(sk_bfv, &bfv_params, degree) + }) + .collect::>()?; + + // Similarly decrypt esi_sss + let esi_sss_collected: Vec> = shares + .iter() + .map(|ts| { + ts.esi_sss + .iter() + .map(|esi_shares| { + let encrypted = esi_shares.clone_share(party_id).ok_or_else(|| { + anyhow::anyhow!("No esi_sss share for party {}", party_id) + })?; + encrypted.decrypt(sk_bfv, &bfv_params, degree) + }) + .collect::>>() }) .collect::>()?; diff --git a/crates/tests/tests/integration.rs b/crates/tests/tests/integration.rs index 0b13895c1c..4e7b924924 100644 --- a/crates/tests/tests/integration.rs +++ b/crates/tests/tests/integration.rs @@ -287,14 +287,28 @@ async fn test_trbfv_actor() -> Result<()> { committee_finalized_timer.elapsed(), )); - let shares_timer = Instant::now(); + // First, wait for all EncryptionKeyCreated events (BFV key exchange) + let encryption_keys_timer = Instant::now(); let expected = vec![ - "ThresholdShareCreated", - "ThresholdShareCreated", - "ThresholdShareCreated", - "ThresholdShareCreated", - "ThresholdShareCreated", + "EncryptionKeyCreated", + "EncryptionKeyCreated", + "EncryptionKeyCreated", + "EncryptionKeyCreated", + "EncryptionKeyCreated", ]; + let _ = nodes + .take_history_with_timeout(0, expected.len(), Duration::from_secs(1000)) + .await?; + report.push(( + "All EncryptionKeyCreated events", + encryption_keys_timer.elapsed(), + )); + + // Then wait for all ThresholdShareCreated events + // With domain-level splitting, each of the 5 parties publishes 5 events (one per target party) + // Total: 5 parties × 5 targets = 25 events + let shares_timer = Instant::now(); + let expected: Vec<&str> = (0..25).map(|_| "ThresholdShareCreated").collect(); let _ = nodes .take_history_with_timeout(0, expected.len(), Duration::from_secs(1000)) .await?; diff --git a/crates/trbfv/Cargo.toml b/crates/trbfv/Cargo.toml index c56f193782..b3a232134c 100644 --- a/crates/trbfv/Cargo.toml +++ b/crates/trbfv/Cargo.toml @@ -24,10 +24,11 @@ serde.workspace = true tracing.workspace = true zeroize.workspace = true +rand.workspace = true + [dev-dependencies] e3-test-helpers.workspace = true e3-fhe.workspace = true e3-events.workspace = true tracing-subscriber.workspace = true tokio.workspace = true -rand.workspace = true diff --git a/crates/trbfv/src/helpers.rs b/crates/trbfv/src/helpers.rs index 7b7f9edf1a..484d5264cd 100644 --- a/crates/trbfv/src/helpers.rs +++ b/crates/trbfv/src/helpers.rs @@ -6,21 +6,49 @@ use crate::shares::ShamirShare; use anyhow::Result; +use e3_bfv_helpers::BfvParamSets; use e3_crypto::{Cipher, SensitiveBytes}; use fhe::mbfv::PublicKeyShare; use fhe::{ - bfv::{self, BfvParameters}, + bfv::{self, BfvParameters, SecretKey}, trbfv::{SmudgingBoundCalculator, SmudgingBoundCalculatorConfig}, }; use fhe_math::rq::Poly; use fhe_traits::{DeserializeWithContext, Serialize}; use num_bigint::BigUint; use petname::Petnames; +use serde::{Deserialize as SerdeDeserialize, Serialize as SerdeSerialize}; use std::{ hash::{DefaultHasher, Hash, Hasher}, sync::Arc, }; +/// Simple serialization wrapper for SecretKey coefficients. +#[derive(SerdeSerialize, SerdeDeserialize)] +struct SecretKeyData { + coeffs: Box<[i64]>, +} + +/// Serialize a BFV SecretKey to bytes. +pub fn serialize_secret_key(sk: &SecretKey) -> Result> { + let data = SecretKeyData { + coeffs: sk.coeffs.clone(), + }; + Ok(bincode::serialize(&data)?) +} + +/// Deserialize a BFV SecretKey from bytes. +pub fn deserialize_secret_key(bytes: &[u8], params: &Arc) -> Result { + let data: SecretKeyData = bincode::deserialize(bytes)?; + Ok(SecretKey::new(data.coeffs.to_vec(), params)) +} + +/// TODO: Make this modular +pub fn get_share_encryption_params() -> Arc { + let param_set: e3_bfv_helpers::BfvParamSet = BfvParamSets::Set8192_144115188075855872_2.into(); + param_set.build_arc() +} + pub fn try_poly_from_bytes(bytes: &[u8], params: &BfvParameters) -> Result { Ok(Poly::from_bytes(bytes, params.ctx_at_level(0)?)?) } diff --git a/crates/trbfv/src/shares/bfv_encrypted.rs b/crates/trbfv/src/shares/bfv_encrypted.rs new file mode 100644 index 0000000000..53cc78b65d --- /dev/null +++ b/crates/trbfv/src/shares/bfv_encrypted.rs @@ -0,0 +1,266 @@ +// 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 anyhow::{Context, Result}; +use derivative::Derivative; +use e3_utils::utility_types::ArcBytes; +use fhe::bfv::{BfvParameters, Ciphertext, Encoding, Plaintext, PublicKey, SecretKey}; +use fhe_traits::{ + DeserializeParametrized, FheDecoder, FheDecrypter, FheEncoder, FheEncrypter, + Serialize as FheSerialize, +}; +use ndarray::Array2; +use rand::{CryptoRng, RngCore}; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use std::sync::Arc; + +use super::{ShamirShare, SharedSecret}; + +// Re-export helper functions from helpers module +pub use crate::helpers::{ + deserialize_secret_key, get_share_encryption_params, serialize_secret_key, +}; + +/// A BFV-encrypted Shamir share for secure transmission. +/// +/// Each share is encrypted as multiple BFV ciphertexts (one per modulus level). +/// The recipient can only decrypt using their corresponding BFV secret key. +#[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derivative(Debug)] +pub struct BfvEncryptedShare { + /// BFV ciphertexts, one per modulus level + #[derivative(Debug(format_with = "debug_vec_arcbytes"))] + ciphertexts: Vec, +} + +/// Debug helper for Vec +fn debug_vec_arcbytes(v: &Vec, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "[{} ciphertexts, total {} bytes]", + v.len(), + v.iter().map(|c| c.len()).sum::() + ) +} + +impl BfvEncryptedShare { + /// Encrypt a Shamir share for a specific recipient. + /// + /// # Arguments + /// * `share` - The Shamir share to encrypt (contains data for all moduli) + /// * `recipient_pk` - The recipient's BFV public key + /// * `params` - BFV parameters for share encryption + /// * `rng` - Random number generator + /// + /// # Returns + /// An encrypted share that can only be decrypted by the recipient + pub fn encrypt( + share: &ShamirShare, + recipient_pk: &PublicKey, + params: &Arc, + rng: &mut R, + ) -> Result { + let data: &Array2 = share.deref(); // Array2 with rows = moduli, cols = coefficients + let num_moduli = data.nrows(); + + let mut ciphertexts = Vec::with_capacity(num_moduli); + + for m in 0..num_moduli { + let row = data.row(m); + let share_vec: Vec = row.to_vec(); + + let pt = Plaintext::try_encode(&share_vec, Encoding::poly(), params) + .context("Failed to encode share as plaintext")?; + + let ct = recipient_pk + .try_encrypt(&pt, rng) + .context("Failed to encrypt share")?; + + ciphertexts.push(ArcBytes::from_bytes(&ct.to_bytes())); + } + + Ok(Self { ciphertexts }) + } + + /// Decrypt an encrypted share using the recipient's secret key. + /// + /// # Arguments + /// * `sk` - The recipient's BFV secret key + /// * `params` - BFV parameters for share encryption + /// * `degree` - Polynomial degree (for reconstructing the share matrix) + /// + /// # Returns + /// The decrypted Shamir share + pub fn decrypt( + self, + sk: &SecretKey, + params: &Arc, + degree: usize, + ) -> Result { + let num_moduli = self.ciphertexts.len(); + let mut data = Array2::zeros((num_moduli, degree)); + + for (m, ct_bytes) in self.ciphertexts.into_iter().enumerate() { + let ct = Ciphertext::from_bytes(&ct_bytes, params) + .context("Failed to deserialize ciphertext")?; + + let pt = sk + .try_decrypt(&ct) + .context("Failed to decrypt ciphertext")?; + + let decrypted: Vec = Vec::::try_decode(&pt, Encoding::poly()) + .context("Failed to decode plaintext")?; + + for (i, val) in decrypted.into_iter().take(degree).enumerate() { + data[[m, i]] = val; + } + } + + Ok(ShamirShare::new(data)) + } +} + +impl Default for BfvEncryptedShare { + fn default() -> Self { + Self { + ciphertexts: Vec::new(), + } + } +} + +/// A collection of BFV-encrypted shares for all recipients. +/// +/// When a party generates Shamir shares, they encrypt each recipient's share +/// with that recipient's public key. This struct holds all encrypted shares +/// from a single sender. +#[derive(Derivative, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derivative(Debug)] +pub struct BfvEncryptedShares { + /// Encrypted shares indexed by recipient party_id (0-based) + shares: Vec, +} + +impl BfvEncryptedShares { + /// Encrypt shares for all recipients. + /// + /// # Arguments + /// * `secret` - The SharedSecret containing shares for all parties + /// * `recipient_pks` - Public keys for all recipients, indexed by party_id + /// * `params` - BFV parameters for share encryption + /// * `rng` - Random number generator + pub fn encrypt_all( + secret: &SharedSecret, + recipient_pks: &[PublicKey], + params: &Arc, + rng: &mut R, + ) -> Result { + let num_parties = recipient_pks.len(); + let mut shares = Vec::with_capacity(num_parties); + + for party_id in 0..num_parties { + let share = secret + .extract_party_share(party_id) + .context(format!("Failed to extract share for party {}", party_id))?; + + let encrypted = + BfvEncryptedShare::encrypt(&share, &recipient_pks[party_id], params, rng)?; + + shares.push(encrypted); + } + + Ok(Self { shares }) + } + + /// Get the encrypted share for a specific recipient. + pub fn get_share(&self, party_id: usize) -> Option<&BfvEncryptedShare> { + self.shares.get(party_id) + } + + /// Clone the encrypted share for a specific recipient. + pub fn clone_share(&self, party_id: usize) -> Option { + self.shares.get(party_id).cloned() + } + + /// Extract only the share for a specific party (for bandwidth optimization) + pub fn extract_for_party(&self, party_id: usize) -> Option { + self.shares.get(party_id).map(|share| Self { + shares: vec![share.clone()], + }) + } + + /// Number of encrypted shares + pub fn len(&self) -> usize { + self.shares.len() + } + + /// Check if empty + pub fn is_empty(&self) -> bool { + self.shares.is_empty() + } +} + +impl Default for BfvEncryptedShares { + fn default() -> Self { + Self { shares: Vec::new() } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::rngs::OsRng; + + #[test] + fn test_encrypt_decrypt_share() { + let params = get_share_encryption_params(); + let mut rng = OsRng; + + // Generate key pair + let sk = SecretKey::random(¶ms, &mut rng); + let pk = PublicKey::new(&sk, &mut rng); + + // Create a test share (1 modulus row, 512 coefficients) + let degree = params.degree(); + let test_data: Vec = (0..degree as u64).collect(); + let mut data = Array2::zeros((1, degree)); + for (i, val) in test_data.iter().enumerate() { + data[[0, i]] = *val; + } + let share = ShamirShare::new(data.clone()); + + // Encrypt + let encrypted = BfvEncryptedShare::encrypt(&share, &pk, ¶ms, &mut rng) + .expect("Encryption should succeed"); + + // Decrypt + let decrypted = encrypted + .decrypt(&sk, ¶ms, degree) + .expect("Decryption should succeed"); + + // Verify + assert_eq!(share.deref(), decrypted.deref()); + } + + #[test] + fn test_secret_key_serialization() { + let params = get_share_encryption_params(); + let mut rng = OsRng; + + // Generate a secret key + let sk = SecretKey::random(¶ms, &mut rng); + + // Serialize + let bytes = serialize_secret_key(&sk).expect("Serialization should succeed"); + + // Deserialize + let sk_restored = + deserialize_secret_key(&bytes, ¶ms).expect("Deserialization should succeed"); + + // Verify coefficients match + assert_eq!(sk.coeffs, sk_restored.coeffs); + } +} diff --git a/crates/trbfv/src/shares/mod.rs b/crates/trbfv/src/shares/mod.rs index e633ad4085..171cdee7da 100644 --- a/crates/trbfv/src/shares/mod.rs +++ b/crates/trbfv/src/shares/mod.rs @@ -3,9 +3,9 @@ // This file is provided WITHOUT ANY WARRANTY; // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +pub mod bfv_encrypted; pub mod encrypted; -pub mod pvw; pub mod shares; +pub use bfv_encrypted::*; pub use encrypted::*; -pub use pvw::*; pub use shares::*; diff --git a/crates/trbfv/src/shares/pvw.rs b/crates/trbfv/src/shares/pvw.rs deleted file mode 100644 index 4600204493..0000000000 --- a/crates/trbfv/src/shares/pvw.rs +++ /dev/null @@ -1,66 +0,0 @@ -// 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 anyhow::Result; -use derivative::Derivative; - -use super::DeserializableValue; - -/// Encrypted version of T for secure storage/transmission -#[derive(Derivative, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] -#[derivative(Debug)] -// TODO: Currently we are simply serializing the data in preparation for PVW encryption -// expecting to pass in keys to encrypt and decrypt as required at a later point -pub struct PvwEncrypted( - #[derivative(Debug(format_with = "e3_utils::formatters::hexf"))] Vec, - std::marker::PhantomData, -); - -impl PvwEncrypted -where - T: DeserializableValue, -{ - /// Create a new encrypted wrapper from data of type T - pub fn new(data: T) -> Result { - Ok(Self(bincode::serialize(&data)?, std::marker::PhantomData)) - } - - /// Decrypt and deserialize back to type T - pub fn pvw_decrypt(self) -> Result { - let value = self.0; - Ok(bincode::deserialize(&value)?) - } -} - -pub trait PvwEncryptedVecExt { - fn to_vec_decrypted(self) -> Result>; -} - -impl PvwEncryptedVecExt for Vec> -where - T: DeserializableValue, -{ - fn to_vec_decrypted(self) -> Result> { - self.into_iter().map(|s| s.pvw_decrypt()).collect() - } -} - -/// Trait to add decrypt functionality to Vec> -pub trait PvwEncryptableVec { - /// Decrypt all encrypted values in the vector - /// Returns a vector of decrypted values or the first error encountered - fn pvw_encrypt(self) -> Result>>; -} - -impl PvwEncryptableVec for Vec -where - T: DeserializableValue, -{ - fn pvw_encrypt(self) -> Result>> { - self.into_iter() - .map(|encryptable| PvwEncrypted::new(encryptable)) - .collect() - } -} diff --git a/crates/trbfv/tests/integration.rs b/crates/trbfv/tests/integration.rs index b08af1a2af..addc5575d0 100644 --- a/crates/trbfv/tests/integration.rs +++ b/crates/trbfv/tests/integration.rs @@ -83,7 +83,7 @@ async fn test_trbfv_isolation() -> Result<()> { ); // let crp = ArcBytes::from_bytes(crp_raw.to_bytes()); - let shares_hash_map = usecase_helpers::generate_shares_hash_map( + let generated = usecase_helpers::generate_shares_hash_map( &trbfv_config, esi_per_ct as u64, &error_size, @@ -93,9 +93,14 @@ async fn test_trbfv_isolation() -> Result<()> { )?; let pubkey = - usecase_helpers::get_public_key(&shares_hash_map, trbfv_config.params(), &crp_raw)?; - let shares = to_ordered_vec(shares_hash_map); - let decryption_keys = usecase_helpers::get_decryption_keys(shares, &cipher, &trbfv_config)?; + usecase_helpers::get_public_key(&generated.shares, trbfv_config.params(), &crp_raw)?; + let shares = to_ordered_vec(generated.shares); + let decryption_keys = usecase_helpers::get_decryption_keys( + shares, + &generated.bfv_secret_keys, + &cipher, + &trbfv_config, + )?; // Create the inputs let num_votes_per_voter = 3; let num_voters = 30; diff --git a/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json b/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json index 17018ad9f1..32e4ef8771 100644 --- a/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json +++ b/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json @@ -1148,7 +1148,7 @@ "linkReferences": {}, "deployedLinkReferences": {}, "immutableReferences": { - "3415": [ + "1944": [ { "length": 32, "start": 872 @@ -1174,43 +1174,43 @@ "start": 3711 } ], - "6684": [ + "4931": [ { "length": 32, "start": 3942 } ], - "6686": [ + "4933": [ { "length": 32, "start": 3900 } ], - "6688": [ + "4935": [ { "length": 32, "start": 3858 } ], - "6690": [ + "4937": [ { "length": 32, "start": 4023 } ], - "6692": [ + "4939": [ { "length": 32, "start": 4063 } ], - "6695": [ + "4942": [ { "length": 32, "start": 4727 } ], - "6698": [ + "4945": [ { "length": 32, "start": 4772 @@ -1218,5 +1218,5 @@ ] }, "inputSourceName": "project/contracts/token/EnclaveTicketToken.sol", - "buildInfoId": "solc-0_8_28-572e77328edd43ea3643262ca5f82babf769c493" + "buildInfoId": "solc-0_8_28-bef1dec98cb6cc37e15bd06abee5a353862d85f4" } \ No newline at end of file