From b0174dcd0b5259e456bf9a7d5dccf732b7deda1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 18 Jan 2024 11:29:40 +0000 Subject: [PATCH] [wip] dealing exchange --- .../contracts-common/src/dealings.rs | 6 + common/dkg/src/lib.rs | 1 + common/nymcoconut/src/scheme/keygen.rs | 4 +- contracts/coconut-dkg/src/dealings/storage.rs | 3 + nym-api/src/coconut/dkg/complaints.rs | 21 + nym-api/src/coconut/dkg/controller/mod.rs | 26 +- nym-api/src/coconut/dkg/dealing.rs | 409 +++++++++++++----- nym-api/src/coconut/dkg/public_key.rs | 65 ++- .../src/coconut/dkg/state/dealing_exchange.rs | 24 + nym-api/src/coconut/dkg/state/mod.rs | 185 +++++--- .../src/coconut/dkg/state/serde_helpers.rs | 33 ++ nym-api/src/coconut/error.rs | 3 + nym-api/src/coconut/keys/mod.rs | 18 + 13 files changed, 592 insertions(+), 206 deletions(-) create mode 100644 nym-api/src/coconut/dkg/state/dealing_exchange.rs diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs b/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs index 8c79ccbcfc..c365d2691f 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs @@ -23,6 +23,12 @@ impl Deref for ContractSafeBytes { } } +impl From> for ContractSafeBytes { + fn from(value: Vec) -> Self { + ContractSafeBytes(value) + } +} + impl Display for ContractSafeBytes { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if !self.0.is_empty() { diff --git a/common/dkg/src/lib.rs b/common/dkg/src/lib.rs index f754b16153..d16af333bc 100644 --- a/common/dkg/src/lib.rs +++ b/common/dkg/src/lib.rs @@ -13,6 +13,7 @@ pub mod dealing; pub(crate) mod share; pub(crate) mod utils; +pub use bls12_381::Scalar; pub use dealing::*; pub use share::*; diff --git a/common/nymcoconut/src/scheme/keygen.rs b/common/nymcoconut/src/scheme/keygen.rs index d033d8f0be..966ebb141c 100644 --- a/common/nymcoconut/src/scheme/keygen.rs +++ b/common/nymcoconut/src/scheme/keygen.rs @@ -99,7 +99,9 @@ impl SecretKey { Self { x, ys } } - pub fn into_raw(&self) -> (Scalar, Vec) { + /// Extract the Scalar copy of the underlying secrets. + /// The caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized + pub fn hazmat_to_raw(&self) -> (Scalar, Vec) { (self.x, self.ys.clone()) } diff --git a/contracts/coconut-dkg/src/dealings/storage.rs b/contracts/coconut-dkg/src/dealings/storage.rs index 2c5cef952a..33ce7262a8 100644 --- a/contracts/coconut-dkg/src/dealings/storage.rs +++ b/contracts/coconut-dkg/src/dealings/storage.rs @@ -22,6 +22,9 @@ type Dealer<'a> = &'a Addr; pub(crate) struct StoredDealing; +// part of `StoredDealing` to make existence lookup cheaper +pub(crate) struct UNIMPLEMENTED_DealingLookup; + impl StoredDealing { const NAMESPACE: &'static [u8] = b"dealing"; diff --git a/nym-api/src/coconut/dkg/complaints.rs b/nym-api/src/coconut/dkg/complaints.rs index 8b397f2564..79953e8f5a 100644 --- a/nym-api/src/coconut/dkg/complaints.rs +++ b/nym-api/src/coconut/dkg/complaints.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub(crate) enum ComplaintReason { @@ -11,3 +12,23 @@ pub(crate) enum ComplaintReason { MalformedDealing, DealingVerificationError, } + +impl Display for ComplaintReason { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ComplaintReason::MalformedBTEPublicKey => { + write!(f, "provided BTE Public key is malformed") + } + ComplaintReason::InvalidBTEPublicKey => { + write!(f, "provided BTE public key does not verify correctly") + } + ComplaintReason::MissingDealing => write!(f, "one of the dealings is missing"), + ComplaintReason::MalformedDealing => { + write!(f, "one of the provided dealings is malformed") + } + ComplaintReason::DealingVerificationError => { + write!(f, "failed to verify one of the dealings") + } + } + } +} diff --git a/nym-api/src/coconut/dkg/controller/mod.rs b/nym-api/src/coconut/dkg/controller/mod.rs index 7af9afb65f..be7fe7c77e 100644 --- a/nym-api/src/coconut/dkg/controller/mod.rs +++ b/nym-api/src/coconut/dkg/controller/mod.rs @@ -4,12 +4,10 @@ use crate::coconut::dkg::client::DkgClient; use crate::coconut::dkg::controller::error::DkgError; use crate::coconut::dkg::state::{ConsistentState, PersistentState, State}; +use crate::coconut::dkg::verification_key::verification_key_submission; use crate::coconut::dkg::verification_key::{ verification_key_finalization, verification_key_validation, }; -use crate::coconut::dkg::{ - dealing::dealing_exchange, verification_key::verification_key_submission, -}; use crate::coconut::keys::KeyPair as CoconutKeyPair; use crate::nyxd; use crate::support::config; @@ -32,7 +30,7 @@ pub(crate) struct DkgController { pub(crate) dkg_client: DkgClient, pub(crate) coconut_key_path: PathBuf, pub(crate) state: State, - rng: R, + pub(super) rng: R, polling_rate: Duration, } @@ -137,16 +135,15 @@ impl DkgController { .map_err(|source| DkgError::PublicKeySubmissionFailure { source }) } - async fn handle_dealing_exchange(&mut self, resharing: bool) -> Result<(), DkgError> { + async fn handle_dealing_exchange( + &mut self, + epoch_id: EpochId, + resharing: bool, + ) -> Result<(), DkgError> { debug!("DKG: dealing exchange (resharing: {resharing})"); - dealing_exchange( - &self.dkg_client, - &mut self.state, - self.rng.clone(), - resharing, - ) - .await - .map_err(|source| DkgError::DealingExchangeFailure { source }) + self.dealing_exchange(epoch_id, resharing) + .await + .map_err(|source| DkgError::DealingExchangeFailure { source }) } async fn handle_verification_key_submission( @@ -221,7 +218,8 @@ impl DkgController { .await? } EpochState::DealingExchange { resharing } => { - self.handle_dealing_exchange(resharing).await? + self.handle_dealing_exchange(epoch.epoch_id, resharing) + .await? } EpochState::VerificationKeySubmission { resharing } => { self.handle_verification_key_submission(epoch.epoch_id, resharing) diff --git a/nym-api/src/coconut/dkg/dealing.rs b/nym-api/src/coconut/dkg/dealing.rs index 7492af8167..35b34105cf 100644 --- a/nym-api/src/coconut/dkg/dealing.rs +++ b/nym-api/src/coconut/dkg/dealing.rs @@ -3,130 +3,331 @@ use crate::coconut::dkg; use crate::coconut::dkg::client::DkgClient; -use crate::coconut::dkg::state::{ConsistentState, State}; +use crate::coconut::dkg::complaints::ComplaintReason; +use crate::coconut::dkg::controller::keys::archive_coconut_keypair; +use crate::coconut::dkg::controller::DkgController; +use crate::coconut::dkg::state::{ConsistentState, ParticipantState, State}; use crate::coconut::error::CoconutError; +use crate::coconut::keys::KeyPairWithEpoch; use log::debug; -use nym_coconut_dkg_common::types::{ContractDealing, PartialContractDealing}; -use nym_dkg::Dealing; -use rand::RngCore; -use std::collections::VecDeque; +use nym_coconut_dkg_common::types::{ + ContractDealing, DealingIndex, EpochId, PartialContractDealing, +}; +use nym_dkg::bte::{PublicKey, PublicKeyWithProof}; +use nym_dkg::{Dealing, NodeIndex, Scalar, Threshold}; +use rand::{CryptoRng, RngCore}; +use rocket::form::validate::Len; +use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::fmt::{Debug, Formatter}; use zeroize::Zeroize; -pub(crate) async fn dealing_exchange( - dkg_client: &DkgClient, - state: &mut State, - rng: impl RngCore + Clone, - resharing: bool, -) -> Result<(), CoconutError> { - if state.receiver_index().is_some() { - debug!("Receiver index was set previously, nothing to do"); - return Ok(()); +enum DealingGeneration { + Fresh { number: u32 }, + Resharing { prior_secrets: Vec }, +} + +impl Debug for DealingGeneration { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + DealingGeneration::Fresh { number } => f + .debug_struct("DealingGeneration::Fresh") + .field("number", number) + .finish(), + DealingGeneration::Resharing { prior_secrets } => f + .debug_struct("DealingGeneration::Resharing") + .field("number", &prior_secrets.len()) + .finish(), + } + } +} + +impl DkgController { + async fn generate_dealings( + &mut self, + epoch_id: EpochId, + spec: DealingGeneration, + ) -> Result, CoconutError> { + let threshold = self.dkg_client.get_current_epoch_threshold().await?.ok_or( + CoconutError::UnrecoverableState { + reason: String::from("Threshold should have been set"), + }, + )?; + + let dealer_index = self + .state + .registration_state(epoch_id)? + .assigned_index + .ok_or(CoconutError::UnrecoverableState { + reason: String::from("Node index should have been set"), + })?; + + // in our case every dealer is also a receiver + + // ASSUMPTION: all dealers see the same contract data, i.e. if one fails to decode and verify the receiver's key, + // all of them will + let filtered_receivers: BTreeMap<_, _> = self + .state + .dkg_state(epoch_id)? + .dealers + .iter() + .filter_map(|(index, dealer)| match &dealer.state { + ParticipantState::Invalid(_) => None, + ParticipantState::VerifiedKey(key) => Some((*index, *key.public_key())), + }) + .collect(); + + let dbg_receivers = filtered_receivers.keys().collect::>(); + debug!("generating dealings with threshold {threshold} for receivers: {dbg_receivers:?} with the following spec: {spec:?}. Our index is {dealer_index}"); + + let mut dealings = HashMap::new(); + match spec { + DealingGeneration::Fresh { number } => { + for i in 0..number { + let dealing = Dealing::create( + &mut self.rng, + dkg::params(), + dealer_index, + threshold, + &filtered_receivers, + None, + ); + dealings.insert(i as DealingIndex, dealing.0); + } + } + DealingGeneration::Resharing { prior_secrets } => { + for (i, secret) in prior_secrets.into_iter().enumerate() { + let dealing = Dealing::create( + &mut self.rng, + dkg::params(), + dealer_index, + threshold, + &filtered_receivers, + Some(secret), + ); + dealings.insert(i as DealingIndex, dealing.0); + } + } + } + + // update the state with the dealing information + self.state + .dealing_exchange_state_mut(epoch_id)? + .generated_dealings = dealings.clone(); + + Ok(dealings) } - let contract_state = dkg_client.get_contract_state().await?; - let expected_key_size = contract_state.key_size; + async fn generate_fresh_dealings( + &mut self, + epoch_id: EpochId, + number: u32, + ) -> Result, CoconutError> { + self.generate_dealings(epoch_id, DealingGeneration::Fresh { number }) + .await + } - let epoch_id = dkg_client.get_current_epoch().await?.epoch_id; + async fn generate_resharing_dealings( + &mut self, + epoch_id: EpochId, + prior_secrets: Vec, + ) -> Result, CoconutError> { + self.generate_dealings(epoch_id, DealingGeneration::Resharing { prior_secrets }) + .await + } - let dealers = dkg_client.get_current_dealers().await?; - let threshold = dkg_client.get_current_epoch_threshold().await?; - let initial_dealers = dkg_client - .get_initial_dealers() - .await? - .map(|d| d.initial_dealers) - .unwrap_or_default(); - let own_address = dkg_client.get_address().await.as_ref().to_string(); - state.set_dealers(dealers); - state.set_threshold(threshold); - let receivers = state.current_dealers_by_idx(); - let dealer_index = state.node_index_value()?; - let receiver_index = receivers - .keys() - .position(|node_index| *node_index == dealer_index); + async fn resubmit_pregenerated_dealings( + &self, + epoch_id: EpochId, + resharing: bool, + ) -> Result<(), CoconutError> { + let dealing_state = self.state.dealing_exchange_state(epoch_id)?; - let prior_resharing_secrets = if let Some(mut keypair) = state.take_coconut_keypair().await { - // Double check that we are in resharing mode - if resharing { - todo!() - // let sk = keypair.secret_key(); - // if sk.size() + 1 != expected_key_size as usize { - // return Err(CoconutError::CorruptedCoconutKeyPair); - // } - // - // let (x, mut scalars) = sk.into_raw(); - // - // // We can now erase the keypair from memory - // debug!("Removing coconut keypair from memory"); - // keypair.zeroize(); - // scalars.push(x); - // scalars - } else { - log::warn!("Coconut key hasn't been reset in memory. The state might be corrupt"); - vec![] - } - } else { - vec![] - }; - let mut prior_resharing_secrets = VecDeque::from(prior_resharing_secrets); - if !resharing || initial_dealers.iter().any(|d| *d == own_address) { - let params = dkg::params(); - for dealing_index in 0..expected_key_size { - debug!( - "dealing {dealing_index} ({} out of {expected_key_size})", - dealing_index + 1 - ); + for (dealing_index, dealing) in &dealing_state.generated_dealings { + // check which dealing is actually present on the chain (some might have gotten stuck in the mempool for quite a while) + let dealing_submitted = self + .dkg_client + .get_dealing_status(epoch_id, *dealing_index) + .await?; - // see if we have already submitted this one (we might have crashed) - if dkg_client - .get_dealing_status(epoch_id, dealing_index) - .await? - { - warn!("we have already submitted dealing {dealing_index} before - we probably crashed!"); + if dealing_submitted { + warn!("we have already submitted dealing {dealing_index} before - we probably crashed or the chain timed out!"); continue; } - - // see if we have already generated, but not submitted this one before (we might have crashed or validator might have had a problem) - let contract_dealing = if let Some(prior_dealing) = - state.get_dealing(epoch_id, dealing_index) - { - warn!("we have already generated dealing {dealing_index} before, but failed to submit it"); - PartialContractDealing::new(dealing_index, ContractDealing::from(prior_dealing)) - } else { - // generate fresh dealing - let (dealing, _) = Dealing::create( - rng.clone(), - params, - dealer_index, - state.threshold()?, - &receivers, - prior_resharing_secrets.pop_front(), - ); - - let contract_dealing = - PartialContractDealing::new(dealing_index, ContractDealing::from(&dealing)); - state.store_dealing(epoch_id, dealing_index, dealing); - - contract_dealing - }; - - debug!( - "Submitting dealing for indexes {:?} with resharing: {}", - receivers.keys().collect::>(), - prior_resharing_secrets.front().is_some() + warn!( + "we have already generated dealing {dealing_index} before, but failed to submit it" ); + let contract_dealing = + PartialContractDealing::new(*dealing_index, ContractDealing::from(dealing)); - dkg_client + self.dkg_client .submit_dealing(contract_dealing, resharing) .await?; } - } else { - debug!("Nothing to do, waiting for initial dealers to submit dealings"); + Ok(()) } - info!("DKG: Finished dealing exchange"); - state.set_receiver_index(receiver_index); + /// Check whether this dealer can participate in the resharing + /// by looking into the contract and ensuring it's in the list of initial dealers for this epoch + async fn can_reshare(&self) -> Result { + let Some(initial_data) = self.dkg_client.get_initial_dealers().await? else { + return Ok(false); + }; - Ok(()) + let address = self.dkg_client.get_address().await; + Ok(initial_data + .initial_dealers + .iter() + .any(|d| d.as_str() == address.as_ref())) + } + + async fn handle_resharing_with_prior_key( + &mut self, + epoch_id: EpochId, + expected_key_size: u32, + old_keypair: KeyPairWithEpoch, + ) -> Result<(), CoconutError> { + // make sure we're allowed to participate in resharing + if !self.can_reshare().await? { + // we have to wait for other dealers to give us the dealings (hopefully) + warn!("we we have an existing coconut keypair, but we're not allowed to participate in resharing"); + return Ok(()); + } + + // EDGE CASE: + // make sure our keypair is from strictly the previous epoch + // because our node might have been offline for multiple epochs and while we do have a coconut keypair, + // it could be outdated and we can't use it for resharing + let previous = epoch_id.saturating_sub(1); + if old_keypair.issued_for_epoch != previous { + warn!("our existing coconut keypair has been generated for an distant epoch ({} vs expected {previous} for resharing)", old_keypair.issued_for_epoch); + // don't participate in resharing + return Ok(()); + } + + // EDGE CASE: + // we have changed the key size (because we wanted to add new attribute to credentials) + // in this instance we can't reuse our key and have to generate brand new dealings + if expected_key_size != 1 + old_keypair.keys.secret_key().size() as u32 { + warn!("our existing coconut keypair has different size than the currently expected value ({expected_key_size} vs {})", old_keypair.keys.secret_key().size() as u32); + self.generate_fresh_dealings(epoch_id, expected_key_size) + .await?; + return Ok(()); + } + + // generate resharing dealings + let prior_secrets = old_keypair.hazmat_into_secrets(); + // safety: + // the prior secrets will be immediately converted into `Polynomial` with the specified coefficient + // that does implement `ZeroizeOnDrop` + self.generate_resharing_dealings(epoch_id, prior_secrets) + .await?; + + Ok(()) + } + + pub(crate) async fn dealing_exchange( + &mut self, + epoch_id: EpochId, + resharing: bool, + ) -> Result<(), CoconutError> { + let dealing_state = self.state.dealing_exchange_state(epoch_id)?; + + // check if we have already submitted the dealings + if dealing_state.completed() { + // the only way this could be a false positive is if the chain forked and blocks got reverted, + // but I don't think we have to worry about that + debug!("we have already submitted all the dealings for this epoch"); + return Ok(()); + } + + // FAILURE CASE: + // check if we have already generated the dealings, but they failed to get sent to the contract for whatever reason + if !dealing_state.generated_dealings.is_empty() { + debug!("we have already generated the dealings for this epoch"); + self.resubmit_pregenerated_dealings(epoch_id, resharing) + .await?; + + // if we managed to resubmit the dealings (i.e. we didn't return an error), + // it means the state is complete now. + self.state.dealing_exchange_state_mut(epoch_id)?.completed = true; + return Ok(()); + } + + // we don't have any prior information - grab, parse and cache it since we will need it in next steps + // and it's not going to change during the epoch + let dealers = self.dkg_client.get_current_dealers().await?; + self.state.dkg_state_mut(epoch_id)?.set_dealers(dealers); + + // get the expected key size which will determine the number of dealings we need to construct + let contract_state = self.dkg_client.get_contract_state().await?; + let expected_key_size = contract_state.key_size; + + // there are few cases to cover here based on the resharing status and presence of coconut keypair: + // - resharing + we have a key => we should use the prior secrets for the resharing dealings generation + // - resharing + we don't have a key => we are a new party that joined the existing setup and we have to wait for others to give us the shares + // - no resharing + we have a key => whole DKG has been restarted (probably enough new parties joined / old parties left) to trigger it + // - no resharing + we don't have a key => either as above (but we're a new party) or it's the very first instance of the DKG + if let Some(old_keypair) = self.state.take_coconut_keypair().await { + let keypair_epoch = old_keypair.issued_for_epoch; + + if resharing { + debug!("resharing + prior key"); + self.handle_resharing_with_prior_key(epoch_id, expected_key_size, old_keypair) + .await?; + } else { + debug!("no resharing + prior key"); + self.generate_fresh_dealings(epoch_id, expected_key_size) + .await?; + } + + // EDGE CASE: + // make sure to persist the state after possibly generating the resharing dealings as we're going to be archiving the keypair + // (so we won't be able to create resharing dealings again if we crashed since we won't be able to load the keys) + self.state.persist()?; + // archive the keypair + if let Err(source) = archive_coconut_keypair(&self.coconut_key_path, keypair_epoch) { + return Err(CoconutError::KeyArchiveFailure { + epoch_id, + path: self.coconut_key_path.clone(), + source, + }); + } + } else { + // sure, the if statements could be collapsed, but i prefer to explicitly repeat the block for readability + if resharing { + debug!("resharing + no prior key -> nothing to do"); + if self.can_reshare().await? { + warn!("this dealer was expected to participate in resharing but it doesn't have any prior keys to use"); + } + } else { + debug!("no resharing + no prior key"); + self.generate_fresh_dealings(epoch_id, expected_key_size) + .await?; + } + } + + // if we have generated any dealings => submit them, otherwise we're done + let dealings = &self + .state + .dealing_exchange_state(epoch_id)? + .generated_dealings; + let total = dealings.len(); + for (i, (dealing_index, dealing)) in dealings.iter().enumerate() { + let i = i + 1; + debug!("submitting dealing index {dealing_index} ({i}/{total})"); + + let contract_dealing = + PartialContractDealing::new(*dealing_index, ContractDealing::from(dealing)); + + self.dkg_client + .submit_dealing(contract_dealing, resharing) + .await?; + } + + self.state.dealing_exchange_state_mut(epoch_id)?.completed = true; + info!("DKG: Finished dealing exchange"); + Ok(()) + } } #[cfg(test)] diff --git a/nym-api/src/coconut/dkg/public_key.rs b/nym-api/src/coconut/dkg/public_key.rs index accf170184..29ca003cae 100644 --- a/nym-api/src/coconut/dkg/public_key.rs +++ b/nym-api/src/coconut/dkg/public_key.rs @@ -8,14 +8,14 @@ use log::debug; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; -impl DkgController { +impl DkgController { pub(crate) async fn public_key_submission( &mut self, epoch_id: EpochId, resharing: bool, ) -> Result<(), CoconutError> { - self.state.init_dkg_state(epoch_id); - let registration_state = self.state.registration_state(epoch_id); + self.state.maybe_init_dkg_state(epoch_id); + let registration_state = self.state.registration_state(epoch_id)?; // check if we have already submitted the key if registration_state.completed() { @@ -25,20 +25,20 @@ impl DkgController { return Ok(()); } - // if we have coconut keys available, it means we have already completed the DKG before (in previous epoch) - // in which case, archive and reset those keys - if let Some(old_keypair) = self.state.take_coconut_keypair().await { - debug!("resetting and archiving old coconut keypair"); - if let Err(source) = - archive_coconut_keypair(&self.coconut_key_path, old_keypair.issued_for_epoch) - { - return Err(CoconutError::KeyArchiveFailure { - epoch_id, - path: self.coconut_key_path.clone(), - source, - }); - } - } + // // if we have coconut keys available, it means we have already completed the DKG before (in previous epoch) + // // in which case, archive and reset those keys + // if let Some(old_keypair) = self.state.take_coconut_keypair().await { + // debug!("resetting and archiving old coconut keypair"); + // if let Err(source) = + // archive_coconut_keypair(&self.coconut_key_path, old_keypair.issued_for_epoch) + // { + // return Err(CoconutError::KeyArchiveFailure { + // epoch_id, + // path: self.coconut_key_path.clone(), + // source, + // }); + // } + // } // FAILURE CASE: // check if we have already sent the registration transaction, but it timed out or got stuck in the mempool and @@ -48,7 +48,7 @@ impl DkgController { if dealer_details.dealer_type.is_current() { if let Some(details) = dealer_details.details { // the tx did actually go through - self.state.registration_state_mut(epoch_id).assigned_index = + self.state.registration_state_mut(epoch_id)?.assigned_index = Some(details.assigned_index); info!("DKG: recovered node index: {}", details.assigned_index); return Ok(()); @@ -64,7 +64,7 @@ impl DkgController { .dkg_client .register_dealer(bte_key, identity_key, announce_address, resharing) .await?; - self.state.registration_state_mut(epoch_id).assigned_index = Some(assigned_index); + self.state.registration_state_mut(epoch_id)?.assigned_index = Some(assigned_index); info!("DKG: Using node index {assigned_index}"); Ok(()) @@ -90,8 +90,7 @@ pub(crate) mod tests { const TEST_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; #[tokio::test] - #[ignore] // expensive test - async fn submit_public_key() { + async fn submit_public_key() -> anyhow::Result<()> { let dkg_client = DkgClient::new(DummyClient::new( AccountId::from_str(TEST_VALIDATOR_ADDRESS).unwrap(), )); @@ -110,26 +109,21 @@ pub(crate) mod tests { assert!(controller .dkg_client .get_self_registered_dealer_details() - .await - .unwrap() + .await? .details .is_none()); - controller - .public_key_submission(epoch, false) - .await - .unwrap(); + controller.public_key_submission(epoch, false).await?; let client_idx = controller .dkg_client .get_self_registered_dealer_details() - .await - .unwrap() + .await? .details .unwrap() .assigned_index; assert_eq!( controller .state - .registration_state(epoch) + .registration_state(epoch)? .assigned_index .unwrap(), client_idx @@ -138,19 +132,18 @@ pub(crate) mod tests { // keeps the same index from chain, not calling register_dealer again controller .state - .registration_state_mut(epoch) + .registration_state_mut(epoch)? .assigned_index = None; - controller - .public_key_submission(epoch, false) - .await - .unwrap(); + controller.public_key_submission(epoch, false).await?; assert_eq!( controller .state - .registration_state(epoch) + .registration_state(epoch)? .assigned_index .unwrap(), client_idx ); + + Ok(()) } } diff --git a/nym-api/src/coconut/dkg/state/dealing_exchange.rs b/nym-api/src/coconut/dkg/state/dealing_exchange.rs new file mode 100644 index 0000000000..fcc91bdc7c --- /dev/null +++ b/nym-api/src/coconut/dkg/state/dealing_exchange.rs @@ -0,0 +1,24 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use super::serde_helpers::generated_dealings; +use nym_coconut_dkg_common::types::DealingIndex; +use nym_dkg::{Dealing, NodeIndex}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct DealingExchangeState { + // pub(crate) assigned_index: Option, + #[serde(with = "generated_dealings")] + pub(crate) generated_dealings: HashMap, + + pub(crate) completed: bool, +} + +impl DealingExchangeState { + /// Specifies whether this dealer has already shared dealings in this DKG epoch + pub fn completed(&self) -> bool { + self.completed + } +} diff --git a/nym-api/src/coconut/dkg/state/mod.rs b/nym-api/src/coconut/dkg/state/mod.rs index cd8aedc7a6..13c26a2a6e 100644 --- a/nym-api/src/coconut/dkg/state/mod.rs +++ b/nym-api/src/coconut/dkg/state/mod.rs @@ -2,57 +2,78 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::coconut::dkg::complaints::ComplaintReason; +use crate::coconut::dkg::controller::keys::archive_coconut_keypair; +use crate::coconut::dkg::state::dealing_exchange::DealingExchangeState; use crate::coconut::dkg::state::registration::RegistrationState; use crate::coconut::error::CoconutError; use crate::coconut::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch}; use cosmwasm_std::Addr; use log::debug; use nym_coconut_dkg_common::dealer::DealerDetails; -use nym_coconut_dkg_common::types::{DealingIndex, EpochId, EpochState}; +use nym_coconut_dkg_common::types::{ + DealingIndex, EncodedBTEPublicKeyWithProof, EpochId, EpochState, +}; use nym_crypto::asymmetric::identity; use nym_dkg::bte::{keys::KeyPair as DkgKeyPair, PublicKey, PublicKeyWithProof}; use nym_dkg::{Dealing, NodeIndex, RecoveredVerificationKeys, Threshold}; use nym_validator_client::nyxd::{tx, Hash}; use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use serde_helpers::{bte_pk_serde, generated_dealings, vks_serde}; +use serde_helpers::{bte_pk_serde, generated_dealings_old, vks_serde}; use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; use time::OffsetDateTime; use tokio::sync::RwLockReadGuard; use url::Url; +mod dealing_exchange; mod registration; mod serde_helpers; +#[derive(Clone, Deserialize, Debug, Serialize)] +pub(crate) enum ParticipantState { + Invalid(ComplaintReason), + VerifiedKey(#[serde(with = "bte_pk_serde")] PublicKeyWithProof), +} + +impl ParticipantState { + pub fn is_valid(&self) -> bool { + matches!(self, ParticipantState::VerifiedKey(..)) + } + + fn from_raw_encoded_key(raw: EncodedBTEPublicKeyWithProof) -> Self { + // TODO: include more error information + let Ok(bytes) = bs58::decode(raw).into_vec() else { + return ParticipantState::Invalid(ComplaintReason::MalformedBTEPublicKey); + }; + + let Ok(key) = PublicKeyWithProof::try_from_bytes(&bytes) else { + return ParticipantState::Invalid(ComplaintReason::MalformedBTEPublicKey); + }; + + if !key.verify() { + return ParticipantState::Invalid(ComplaintReason::InvalidBTEPublicKey); + } + + ParticipantState::VerifiedKey(key) + } +} + // note: each dealer is also a receiver which simplifies some logic significantly #[derive(Clone, Deserialize, Debug, Serialize)] pub(crate) struct DkgParticipant { - pub(crate) _address: Addr, - #[serde(with = "bte_pk_serde")] - pub(crate) bte_public_key_with_proof: PublicKeyWithProof, + pub(crate) address: Addr, pub(crate) assigned_index: NodeIndex, + pub(crate) state: ParticipantState, } -impl TryFrom for DkgParticipant { - type Error = ComplaintReason; - - fn try_from(dealer: DealerDetails) -> Result { - let bte_public_key_with_proof = bs58::decode(dealer.bte_public_key_with_proof) - .into_vec() - .map(|bytes| PublicKeyWithProof::try_from_bytes(&bytes)) - .map_err(|_| ComplaintReason::MalformedBTEPublicKey)? - .map_err(|_| ComplaintReason::MalformedBTEPublicKey)?; - - if !bte_public_key_with_proof.verify() { - return Err(ComplaintReason::InvalidBTEPublicKey); - } - - Ok(DkgParticipant { - _address: dealer.address, - bte_public_key_with_proof, +impl From for DkgParticipant { + fn from(dealer: DealerDetails) -> Self { + DkgParticipant { + address: dealer.address, + state: ParticipantState::from_raw_encoded_key(dealer.bte_public_key_with_proof), assigned_index: dealer.assigned_index, - }) + } } } @@ -139,7 +160,7 @@ pub(crate) struct PersistentState { // node_index: Option, dealers: BTreeMap>, - #[serde(with = "generated_dealings")] + #[serde(with = "generated_dealings_old")] generated_dealings: HashMap>, receiver_index: Option, threshold: Option, @@ -189,6 +210,7 @@ impl From<&State> for PersistentState { impl PersistentState { pub fn save_to_file>(&self, path: P) -> Result<(), CoconutError> { + debug!("persisting the dkg state"); std::fs::write(path, serde_json::to_string(self)?)?; Ok(()) } @@ -201,6 +223,28 @@ impl PersistentState { #[derive(Default)] pub(crate) struct DkgState { pub(crate) registration: RegistrationState, + + // available after registration is finished: + pub(crate) dealers: BTreeMap, + + pub(crate) dealing_exchange: DealingExchangeState, +} + +impl DkgState { + pub(crate) fn set_dealers(&mut self, raw_dealers: Vec) { + assert!(self.dealers.is_empty()); + for raw_dealer in raw_dealers { + let dkg_participant = DkgParticipant::from(raw_dealer); + if let ParticipantState::Invalid(complaint) = &dkg_participant.state { + warn!( + "{} dealer is malformed: {complaint}", + dkg_participant.address + ) + } + self.dealers + .insert(dkg_participant.assigned_index, dkg_participant); + } + } } pub(crate) struct State { @@ -288,24 +332,62 @@ impl State { self.was_in_progress = Default::default(); } - pub fn init_dkg_state(&mut self, epoch_id: EpochId) { + pub fn maybe_init_dkg_state(&mut self, epoch_id: EpochId) { if !self.dkg_instances.contains_key(&epoch_id) { self.dkg_instances.insert(epoch_id, Default::default()); } } - pub fn registration_state(&self, epoch_id: EpochId) -> &RegistrationState { - // safety: before any accessors are called, `init_dkg_state` is used at the beginning of submission handler - &self.dkg_instances[&epoch_id].registration + pub fn dkg_state(&self, epoch_id: EpochId) -> Result<&DkgState, CoconutError> { + self.dkg_instances + .get(&epoch_id) + .ok_or(CoconutError::MissingDkgState { epoch_id }) } - pub fn registration_state_mut(&mut self, epoch_id: EpochId) -> &mut RegistrationState { - // safety: before any accessors are called, `init_dkg_state` is used at the beginning of submission handler - &mut self.dkg_instances.get_mut(&epoch_id).unwrap().registration + pub fn dkg_state_mut(&mut self, epoch_id: EpochId) -> Result<&mut DkgState, CoconutError> { + self.dkg_instances + .get_mut(&epoch_id) + .ok_or(CoconutError::MissingDkgState { epoch_id }) } - pub fn already_registered(&self, epoch_id: EpochId) -> bool { - self.registration_state(epoch_id).completed() + pub fn registration_state( + &self, + epoch_id: EpochId, + ) -> Result<&RegistrationState, CoconutError> { + self.dkg_instances + .get(&epoch_id) + .map(|state| &state.registration) + .ok_or(CoconutError::MissingDkgState { epoch_id }) + } + + pub fn registration_state_mut( + &mut self, + epoch_id: EpochId, + ) -> Result<&mut RegistrationState, CoconutError> { + self.dkg_instances + .get_mut(&epoch_id) + .map(|state| &mut state.registration) + .ok_or(CoconutError::MissingDkgState { epoch_id }) + } + + pub fn dealing_exchange_state( + &self, + epoch_id: EpochId, + ) -> Result<&DealingExchangeState, CoconutError> { + self.dkg_instances + .get(&epoch_id) + .map(|state| &state.dealing_exchange) + .ok_or(CoconutError::MissingDkgState { epoch_id }) + } + + pub fn dealing_exchange_state_mut( + &mut self, + epoch_id: EpochId, + ) -> Result<&mut DealingExchangeState, CoconutError> { + self.dkg_instances + .get_mut(&epoch_id) + .map(|state| &mut state.dealing_exchange) + .ok_or(CoconutError::MissingDkgState { epoch_id }) } pub fn persistent_state_path(&self) -> &Path { @@ -379,17 +461,18 @@ impl State { // FIXME: BUG: if we remove dealers, we won't be able to verify shares of other parties... pub fn current_dealers_by_idx(&self) -> BTreeMap { - self.dealers - .iter() - .filter_map(|(_, dealer)| { - dealer.as_ref().ok().map(|participant| { - ( - participant.assigned_index, - *participant.bte_public_key_with_proof.public_key(), - ) - }) - }) - .collect() + todo!() + // self.dealers + // .iter() + // .filter_map(|(_, dealer)| { + // dealer.as_ref().ok().map(|participant| { + // ( + // participant.assigned_index, + // *participant.bte_public_key_with_proof.public_key(), + // ) + // }) + // }) + // .collect() } pub fn recovered_vks(&self) -> &Vec { @@ -424,13 +507,13 @@ impl State { self.node_index = node_index; } - pub fn set_dealers(&mut self, dealers: Vec) { - self.dealers = BTreeMap::from_iter( - dealers - .into_iter() - .map(|details| (details.address.clone(), DkgParticipant::try_from(details))), - ) - } + // pub fn set_dealers(&mut self, dealers: Vec) { + // self.dealers = BTreeMap::from_iter( + // dealers + // .into_iter() + // .map(|details| (details.address.clone(), DkgParticipant::try_from(details))), + // ) + // } pub fn mark_bad_dealer(&mut self, dealer_addr: &Addr, reason: ComplaintReason) { if let Some((_, value)) = self diff --git a/nym-api/src/coconut/dkg/state/serde_helpers.rs b/nym-api/src/coconut/dkg/state/serde_helpers.rs index f6fda611cd..8db9a3a172 100644 --- a/nym-api/src/coconut/dkg/state/serde_helpers.rs +++ b/nym-api/src/coconut/dkg/state/serde_helpers.rs @@ -51,6 +51,39 @@ pub(super) mod vks_serde { } pub(super) mod generated_dealings { + use nym_coconut_dkg_common::types::DealingIndex; + use nym_dkg::Dealing; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::collections::HashMap; + + pub fn serialize( + dealings: &HashMap, + serializer: S, + ) -> Result { + let mut helper = HashMap::new(); + + for (dealing_index, dealing) in dealings { + helper.insert(*dealing_index, dealing.to_bytes()); + } + + helper.serialize(serializer) + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + >>::deserialize(deserializer)? + .into_iter() + .map(|(index, raw_dealing)| { + Dealing::try_from_bytes(&raw_dealing) + .map_err(serde::de::Error::custom) + .map(|dealing| (index, dealing)) + }) + .collect() + } +} + +pub(super) mod generated_dealings_old { use nym_coconut_dkg_common::types::{DealingIndex, EpochId}; use nym_dkg::Dealing; use serde::{Deserialize, Deserializer, Serialize, Serializer}; diff --git a/nym-api/src/coconut/error.rs b/nym-api/src/coconut/error.rs index 0103acc240..9df4f640f0 100644 --- a/nym-api/src/coconut/error.rs +++ b/nym-api/src/coconut/error.rs @@ -138,6 +138,9 @@ pub enum CoconutError { #[error("the provided query arguments were invalid")] InvalidQueryArguments, + #[error("the internal dkg state for epoch {epoch_id} is missing - we might have joined mid exchange")] + MissingDkgState { epoch_id: EpochId }, + #[error("insufficient number of dealings provided to derive the key")] InsufficientDealings { // TODO: details diff --git a/nym-api/src/coconut/keys/mod.rs b/nym-api/src/coconut/keys/mod.rs index 9c03e9a096..d84a9f6713 100644 --- a/nym-api/src/coconut/keys/mod.rs +++ b/nym-api/src/coconut/keys/mod.rs @@ -4,8 +4,10 @@ mod persistence; use nym_coconut_dkg_common::types::EpochId; +use nym_dkg::Scalar; use std::collections::HashMap; use std::ops::Deref; +use std::sync::atomic::AtomicBool; use std::sync::Arc; use tokio::sync::{RwLock, RwLockReadGuard}; @@ -13,6 +15,7 @@ use tokio::sync::{RwLock, RwLockReadGuard}; pub struct KeyPair { // keys: Arc>>, keys: Arc>>, + valid: Arc, } #[derive(Debug)] @@ -21,10 +24,25 @@ pub struct KeyPairWithEpoch { pub(crate) issued_for_epoch: EpochId, } +impl KeyPairWithEpoch { + // extract underlying secrets from the coconut's secret key. + // the caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized + // `KeyPair` and `SecretKey` implement ZeroizeOnDrop; `Scalar` does not (it implements `Copy` -> important to keep in mind) + pub(crate) fn hazmat_into_secrets(self) -> Vec { + let (x, mut secrets) = self.keys.secret_key().hazmat_to_raw(); + + secrets.insert(0, x); + secrets + // since `nym_coconut_interface::KeyPair` implements `ZeroizeOnDrop` and we took ownership of the keypair, + // it will get zeroized after we exit this scope + } +} + impl KeyPair { pub fn new() -> Self { Self { keys: Arc::new(RwLock::new(None)), + valid: Arc::new(Default::default()), } }