diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs index f7a708a2c3..7070b182ce 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs @@ -19,7 +19,7 @@ pub use nym_coconut_dkg_common::{ DealerDetails, DealingIndex, Epoch, EpochId, InitialReplacementData, PartialContractDealing, State, }, - verification_key::{ContractVKShare, PagedVKSharesResponse}, + verification_key::{ContractVKShare, PagedVKSharesResponse, VkShareResponse}, }; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -123,6 +123,15 @@ pub trait DkgQueryClient { self.query_dkg_contract(request).await } + async fn get_vk_share( + &self, + epoch_id: EpochId, + owner: String, + ) -> Result { + let request = DkgQueryMsg::GetVerificationKey { epoch_id, owner }; + self.query_dkg_contract(request).await + } + async fn get_vk_shares_paged( &self, epoch_id: EpochId, @@ -240,6 +249,9 @@ mod tests { } => client .get_dealer_dealings_paged(epoch_id, &dealer, limit, start_after) .ignore(), + DkgQueryMsg::GetVerificationKey { epoch_id, owner } => { + client.get_vk_share(epoch_id, owner).ignore() + } DkgQueryMsg::GetVerificationKeys { epoch_id, limit, diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs index 01dd85b219..f92b8b3ea6 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -15,7 +15,7 @@ use crate::{ PagedDealingsResponse, }, types::{Epoch, InitialReplacementData, State}, - verification_key::PagedVKSharesResponse, + verification_key::{PagedVKSharesResponse, VkShareResponse}, }; use contracts_common::IdentityKey; #[cfg(feature = "schema")] @@ -117,6 +117,9 @@ pub enum QueryMsg { start_after: Option, }, + #[cfg_attr(feature = "schema", returns(VkShareResponse))] + GetVerificationKey { epoch_id: EpochId, owner: String }, + #[cfg_attr(feature = "schema", returns(PagedVKSharesResponse))] GetVerificationKeys { epoch_id: EpochId, diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs index 188215b3b0..de9b4e91d9 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs @@ -20,6 +20,13 @@ pub struct ContractVKShare { pub verified: bool, } +#[cw_serde] +pub struct VkShareResponse { + pub owner: Addr, + pub epoch_id: EpochId, + pub share: Option, +} + #[cw_serde] pub struct PagedVKSharesResponse { pub shares: Vec, diff --git a/common/dkg/src/lib.rs b/common/dkg/src/lib.rs index d16af333bc..4f253418fb 100644 --- a/common/dkg/src/lib.rs +++ b/common/dkg/src/lib.rs @@ -13,7 +13,7 @@ pub mod dealing; pub(crate) mod share; pub(crate) mod utils; -pub use bls12_381::Scalar; +pub use bls12_381::{Scalar, G2Projective}; pub use dealing::*; pub use share::*; diff --git a/nym-api/src/coconut/client.rs b/nym-api/src/coconut/client.rs index bfab9034d6..8ac91460d1 100644 --- a/nym-api/src/coconut/client.rs +++ b/nym-api/src/coconut/client.rs @@ -45,6 +45,12 @@ pub trait Client { epoch_id: EpochId, dealer: &str, ) -> Result>; + + async fn get_verification_key_share( + &self, + epoch_id: EpochId, + dealer: String, + ) -> Result>; async fn get_verification_key_shares(&self, epoch_id: EpochId) -> Result>; async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option) -> Result<()>; diff --git a/nym-api/src/coconut/dkg/client.rs b/nym-api/src/coconut/dkg/client.rs index 5691de1393..d6bbe4202c 100644 --- a/nym-api/src/coconut/dkg/client.rs +++ b/nym-api/src/coconut/dkg/client.rs @@ -79,7 +79,7 @@ impl DkgClient { let address = self.inner.address().await.to_string(); self.inner - .get_dealing_status(epoch_id, address.clone(), dealing_index) + .get_dealing_status(epoch_id, address, dealing_index) .await .map(|r| r.dealing_submitted) } @@ -92,6 +92,18 @@ impl DkgClient { self.inner.get_dealings(epoch_id, &dealer).await } + pub(crate) async fn get_verification_key_share_status( + &self, + epoch_id: EpochId, + ) -> Result, CoconutError> { + let address = self.inner.address().await.to_string(); + + self.inner + .get_verification_key_share(epoch_id, address) + .await + .map(|maybe_share| maybe_share.map(|s| s.verified)) + } + pub(crate) async fn get_verification_key_shares( &self, epoch_id: EpochId, diff --git a/nym-api/src/coconut/dkg/controller/mod.rs b/nym-api/src/coconut/dkg/controller/mod.rs index a245fd380e..2a0b782df7 100644 --- a/nym-api/src/coconut/dkg/controller/mod.rs +++ b/nym-api/src/coconut/dkg/controller/mod.rs @@ -3,11 +3,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::{ +use crate::coconut::dkg::key_derivation::{ verification_key_finalization, verification_key_validation, }; +use crate::coconut::dkg::state::{ConsistentState, PersistentState, State}; use crate::coconut::keys::KeyPair as CoconutKeyPair; use crate::nyxd; use crate::support::config; @@ -18,7 +17,6 @@ use nym_dkg::bte::keys::KeyPair as DkgKeyPair; use nym_task::{TaskClient, TaskManager}; use rand::rngs::OsRng; use rand::{CryptoRng, RngCore}; -use rand_chacha::ChaCha20Rng; use std::path::PathBuf; use std::time::Duration; use time::OffsetDateTime; @@ -153,16 +151,9 @@ impl DkgController { resharing: bool, ) -> Result<(), DkgError> { debug!("DKG: verification key submission (resharing: {resharing})"); - - verification_key_submission( - &self.dkg_client, - &mut self.state, - epoch_id, - &self.coconut_key_path, - resharing, - ) - .await - .map_err(|source| DkgError::VerificationKeySubmissionFailure { source }) + self.verification_key_submission(epoch_id, resharing) + .await + .map_err(|source| DkgError::VerificationKeySubmissionFailure { source }) } async fn handle_verification_key_validation( @@ -244,6 +235,8 @@ impl DkgController { if let Some(epoch_finish) = epoch.finish_timestamp { let now = OffsetDateTime::now_utc(); if now.unix_timestamp() > epoch_finish.seconds() as i64 { + // TODO: make sure to not overload validator in case its running slow + // i.e. send it once at most every X seconds self.try_advance_dkg_state().await? } } @@ -306,10 +299,10 @@ impl DkgController { } pub(crate) fn test_mock_new( - rng: ChaCha20Rng, + rng: rand_chacha::ChaCha20Rng, dkg_client: DkgClient, state: State, - ) -> DkgController { + ) -> DkgController { DkgController { dkg_client, coconut_key_path: Default::default(), diff --git a/nym-api/src/coconut/dkg/dealing.rs b/nym-api/src/coconut/dkg/dealing.rs index 3d6d73fd2d..92072f0fdd 100644 --- a/nym-api/src/coconut/dkg/dealing.rs +++ b/nym-api/src/coconut/dkg/dealing.rs @@ -1,25 +1,20 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::coconut::dkg; -use crate::coconut::dkg::client::DkgClient; -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::dkg::state::ParticipantState; use crate::coconut::error::CoconutError; use crate::coconut::keys::KeyPairWithEpoch; use log::debug; use nym_coconut_dkg_common::types::{ ContractDealing, DealingIndex, EpochId, PartialContractDealing, }; -use nym_dkg::bte::{PublicKey, PublicKeyWithProof}; -use nym_dkg::{Dealing, NodeIndex, Scalar, Threshold}; +use nym_dkg::{Dealing, Scalar}; use rand::{CryptoRng, RngCore}; -use rocket::form::validate::Len; -use std::collections::{BTreeMap, HashMap, VecDeque}; +use std::collections::{BTreeMap, HashMap}; use std::fmt::{Debug, Formatter}; -use zeroize::Zeroize; enum DealingGeneration { Fresh { number: u32 }, @@ -180,6 +175,8 @@ impl DkgController { .any(|d| d.as_str() == address.as_ref())) } + /// Deal with the dealing generation case where the system requests resharing + /// and this node contains an already derived coconut keypair from some previous epoch. async fn handle_resharing_with_prior_key( &mut self, epoch_id: EpochId, @@ -225,6 +222,11 @@ impl DkgController { Ok(()) } + /// Second step of the DKG process during which the nym api will generate appropriate [Dealing] for + /// other parties as indicated by public key registration from the previous step. + /// + /// Before submitting any dealings to the system, the node will persist them locally so that if any failure + /// occurs, it will be possible to recover. pub(crate) async fn dealing_exchange( &mut self, epoch_id: EpochId, @@ -232,6 +234,13 @@ impl DkgController { ) -> Result<(), CoconutError> { let dealing_state = self.state.dealing_exchange_state(epoch_id)?; + // TODO: + /* + let receiver_index = receivers + .keys() + .position(|node_index| *node_index == dealer_index); + */ + // 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, @@ -258,7 +267,8 @@ impl DkgController { let dealers = self.dkg_client.get_current_dealers().await?; // EDGE CASE: - // if there are no dealers for some reason, don't attempt to generate dealings as this will fail with a panic + // if there are no receivers(dealers) in this epoch for some reason, + // don't attempt to generate dealings as this will fail with a panic if dealers.is_empty() { warn!("there are no active dealers/receivers to generate dealings for"); self.state.dealing_exchange_state_mut(epoch_id)?.completed = true; @@ -315,12 +325,18 @@ impl DkgController { } } - // 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(); + + // if we have generated any dealings persist the state in case we crash so that we would still have the data on hand + // for resubmission upon getting back up + if total > 0 { + self.state.persist()?; + } + for (i, (dealing_index, dealing)) in dealings.iter().enumerate() { let i = i + 1; debug!("submitting dealing index {dealing_index} ({i}/{total})"); diff --git a/nym-api/src/coconut/dkg/verification_key.rs b/nym-api/src/coconut/dkg/key_derivation.rs similarity index 75% rename from nym-api/src/coconut/dkg/verification_key.rs rename to nym-api/src/coconut/dkg/key_derivation.rs index 1549554476..41fa8a013b 100644 --- a/nym-api/src/coconut/dkg/verification_key.rs +++ b/nym-api/src/coconut/dkg/key_derivation.rs @@ -1,12 +1,15 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::coconut::dkg; use crate::coconut::dkg::client::DkgClient; use crate::coconut::dkg::complaints::ComplaintReason; +use crate::coconut::dkg::controller::keys::persist_coconut_keypair; +use crate::coconut::dkg::controller::DkgController; use crate::coconut::dkg::state::{ConsistentState, State}; use crate::coconut::error::CoconutError; use crate::coconut::helpers::accepted_vote_err; +use crate::coconut::keys::KeyPairWithEpoch; use crate::coconut::state::BANDWIDTH_CREDENTIAL_PARAMS; use cosmwasm_std::Addr; use cw3::{ProposalResponse, Status}; @@ -14,14 +17,15 @@ use log::debug; use nym_coconut::tests::helpers::transpose_matrix; use nym_coconut::{check_vk_pairing, Base58, KeyPair, SecretKey, VerificationKey}; use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID; -use nym_coconut_dkg_common::types::{EpochId, NodeIndex}; +use nym_coconut_dkg_common::types::{DealingIndex, EpochId, NodeIndex, PartialContractDealing}; use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs; use nym_coconut_interface::KeyPair as CoconutKeyPair; -use nym_dkg::bte::decrypt_share; +use nym_dkg::bte::{decrypt_share, PublicKey}; use nym_dkg::error::DkgError; -use nym_dkg::{combine_shares, try_recover_verification_keys, Dealing, Threshold}; +use nym_dkg::{bte, combine_shares, try_recover_verification_keys, Dealing, Threshold}; use nym_pemstore::KeyPairPath; use nym_validator_client::nyxd::cosmwasm_client::logs::find_attribute; +use rand::{CryptoRng, RngCore}; use std::collections::{BTreeMap, HashMap}; use std::path::PathBuf; @@ -38,6 +42,9 @@ async fn deterministic_filter_dealers( threshold: Threshold, resharing: bool, ) -> Result>, CoconutError> { + // if we're in resharing mode, the contract itself will forbid submission of dealings from + // parties that were not "initial" dealers, so we don't have to worry about it + let mut dealings_maps = Vec::new(); let initial_dealers_by_addr = state.current_dealers_by_addr(); let initial_receivers = state.current_dealers_by_idx(); @@ -204,52 +211,351 @@ fn derive_partial_keypair( Ok(CoconutKeyPair::from_keys(sk, vk)) } -pub(crate) async fn verification_key_submission( - dkg_client: &DkgClient, - state: &mut State, - epoch_id: EpochId, - key_path: &PathBuf, - resharing: bool, -) -> Result<(), CoconutError> { - if state.coconut_keypair_is_some().await { - debug!("Coconut keypair was set previously, nothing to do"); - return Ok(()); +impl DkgController { + fn verified_dealer_dealings( + &self, + epoch_id: EpochId, + dealer: Addr, + epoch_receivers: &BTreeMap, + raw_dealings: Vec, + prior_public_key: Option, + ) -> Result, CoconutError> { + let threshold = self.state.threshold(epoch_id)?; + + // extract G2 elements from the old verification key of the dealer for checking the resharing dealings + let prior_public_components = match prior_public_key { + Some(vk) => { + if vk.beta_g2().len() != raw_dealings.len().saturating_sub(1) { + todo!() + } + + std::iter::once(Some(*vk.alpha())) + .chain(vk.beta_g2().iter().copied().map(Some)) + .collect::>() + } + None => vec![None; raw_dealings.len()], + }; + + let mut temp_verified = Vec::with_capacity(raw_dealings.len()); + // make sure ALL of them verify correctly, we can't have a situation where dealing 2 is valid but dealing 3 is not + for (raw_dealing, prior_public) in raw_dealings + .into_iter() + .zip(prior_public_components.into_iter()) + { + let index = raw_dealing.index; + + // recover the actual dealing from its submitted bytes representation + let dealing = match Dealing::try_from_bytes(&raw_dealing.data) { + Ok(dealing) => dealing, + Err(err) => { + warn!("failed to recover dealing {index} from {dealer}: {err}",); + todo!("blacklist") + } + }; + + // make sure the cryptographic material embedded inside is actually valid + if let Err(err) = + dealing.verify(dkg::params(), threshold, epoch_receivers, prior_public) + { + warn!("dealing {index} from {dealer} is invalid: {err}"); + todo!("blacklist") + } + + temp_verified.push((index, dealing)) + } + + Ok(temp_verified) } - todo!() - // - // let threshold = state.threshold()?; - // let dealings_maps = - // deterministic_filter_dealers(dkg_client, state, epoch_id, threshold, resharing).await?; - // debug!( - // "Filtered dealers to {:?}", - // dealings_maps[0].keys().collect::>() - // ); - // let coconut_keypair = derive_partial_keypair(state, threshold, dealings_maps)?; - // debug!("Derived own coconut keypair"); - // let vk_share = coconut_keypair.verification_key().to_bs58(); - // nym_pemstore::store_keypair(&coconut_keypair, keypair_path)?; - // let res = dkg_client - // .submit_verification_key_share(vk_share, resharing) - // .await?; - // let proposal_id = find_attribute(&res.logs, "wasm", DKG_PROPOSAL_ID) - // .ok_or(CoconutError::ProposalIdError { - // reason: String::from("proposal id not found"), - // })? - // .value - // .parse::() - // .map_err(|_| CoconutError::ProposalIdError { - // reason: String::from("proposal id could not be parsed to u64"), - // })?; - // debug!( - // "Submitted own verification key share, proposal id {} is attached to it", - // proposal_id - // ); - // state.set_proposal_id(proposal_id); - // state.set_coconut_keypair(epoch_id, coconut_keypair).await; - // info!("DKG: Submitted own verification key"); - // - // Ok(()) + /// Attempt to retrieve valid dealings submitted this epoch. + /// + /// For each dealer that submitted a valid public key, query its dealings. + /// Then for each of those dealings, make sure they're cryptographically consistent + pub(crate) async fn get_valid_dealings( + &mut self, + epoch_receivers: &BTreeMap, + epoch_id: EpochId, + resharing: bool, + ) -> Result>, CoconutError> { + let expected_key_size = self.dkg_client.get_contract_state().await?.key_size; + + let mut valid_dealings: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::new(); + + // 1. for every valid dealer in this epoch, obtain its dealings + for (dealer, dealer_index) in self.state.valid_epoch_dealers(epoch_id)? { + // if we're in resharing mode, the contract itself will forbid submission of dealings from + // parties that were not "initial" dealers, so we don't have to worry about it + + // TODO: introduce caching here in case we crash because those queries are EXPENSIVE + let raw_dealings = self + .dkg_client + .get_dealings(epoch_id, dealer.to_string()) + .await?; + + if raw_dealings.is_empty() { + // we might be in resharing mode and this was not in "initial" set + todo!() + } + + // no point in verifying any dealings if we don't have all of them + if raw_dealings.len() != expected_key_size as usize { + todo!("blacklist dealer - incomplete dealing exchange") + } + + // TODO: + // if this is resharing DKG, get the public key of this dealer from the previous epoch + // and use it for dealing(s) verification + let old_public_key = if resharing { + // 1. check state from previous epoch + // 2. if that failed, query the chain + None + } else { + None + }; + + if let Ok(verified_dealings) = self.verified_dealer_dealings( + epoch_id, + dealer, + &epoch_receivers, + raw_dealings, + old_public_key, + ) { + // if we managed to verify ALL the dealings from this dealer, insert them into the map + for (dealing_index, dealing) in verified_dealings { + valid_dealings + .entry(dealing_index) + .or_default() + .insert(dealer_index, dealing); + } + } else { + todo!("blacklist dealer") + } + } + + Ok(valid_dealings) + } + + fn derive_partial_keypair( + &mut self, + epoch_id: EpochId, + epoch_receivers: BTreeMap, + dealings: BTreeMap>, + ) -> Result { + debug!("attempting to derive coconut keypair for epoch {epoch_id}"); + + let threshold = self.state.threshold(epoch_id)?; + let receiver_index = self.state.receiver_index(epoch_id)?; + + // TODO: make sure that each receiver received its dealings + + // SAFETY: + // we have ensured before calling this function that the dealings map is non-empty + // and has exactly 'expected key size' number of entries; + // furthermore each entry has the same number of sub-entries (ALL dealings from given node must be valid) + // + // SAFETY2: + // dealing indexing starts from 0 + if dealings[&0].len() < threshold as usize { + // make sure we have sufficient number of dealings to derive keys for the provided threshold + todo!("fail - can't reach threshold") + } + + let TEMP_TODO_all_indices = Vec::new(); + + let decryption_key = self.state.dkg_keypair().private_key(); + + let mut derived_x = None; + let mut derived_secrets = Vec::new(); + + let total = dealings.len(); + + // for every part of the key + for (dealing_index, dealings) in dealings { + let human_index = dealing_index + 1; + debug!("recovering part {human_index}/{total} of the keys"); + + debug!("recovering the partial verification keys"); + let recovered = try_recover_verification_keys(&[], threshold, &epoch_receivers)?; + // TODO: + + debug!("decrypting received shares"); + // for every received share of the key + let mut shares = Vec::with_capacity(dealings.len()); + for (node_index, dealing) in dealings { + // attempt to decrypt our portion + let share = match decrypt_share( + decryption_key, + receiver_index, + &dealing.ciphertexts, + None, + ) { + Ok(share) => share, + Err(err) => { + error!("failed to decrypt share {human_index}/{total} generated from dealer {node_index}: {err} - can't generate the full key"); + todo!("do something about it") + } + }; + shares.push(share) + } + + debug!("combining the shares into part {human_index}/{total} of the epoch key"); + + // SAFETY: combining shares can only fail if we have different number shares and indices + // however, we returned an error if decryption of any share failed and thus we know those values must match + let secret = combine_shares(shares, &TEMP_TODO_all_indices).unwrap(); + if derived_x.is_none() { + derived_x = Some(secret) + } else { + derived_secrets.push(secret) + } + } + + // SAFETY: + // we know we had a non-empty map of dealings and thus, at the very least, we must have derived a single secret + // (i.e. the x-element) + let sk = SecretKey::create_from_raw(derived_x.unwrap(), derived_secrets); + let derived_vk = sk.verification_key(&BANDWIDTH_CREDENTIAL_PARAMS); + + // TODO: make sure derived_vk matches recovered VK + + Ok(KeyPairWithEpoch { + keys: CoconutKeyPair::from_keys(sk, derived_vk), + issued_for_epoch: epoch_id, + }) + } + + async fn submit_partial_verification_key( + &mut self, + key: &VerificationKey, + resharing: bool, + ) -> Result<(), CoconutError> { + debug!("submitting derived partial verification key to the contract"); + let res = self + .dkg_client + .submit_verification_key_share(key.to_bs58(), resharing) + .await?; + let hash = res.transaction_hash; + let proposal_id = find_attribute(&res.logs, "wasm", DKG_PROPOSAL_ID) + .ok_or(CoconutError::ProposalIdError { + reason: String::from("proposal id not found"), + })? + .value + .parse::() + .map_err(|_| CoconutError::ProposalIdError { + reason: String::from("proposal id could not be parsed to u64"), + })?; + debug!("Submitted own verification key share, proposal id {proposal_id} is attached to it. tx hash: {hash}"); + + // TODO: state.set_proposal_id(proposal_id); + Ok(()) + } + + /// Third step of the DKG process during which the nym api will generate its Coconut keypair + /// with the [Dealing] received from other dealers. It will then submit its verification key + /// to the system so that it could be validated by other participants. + pub(crate) async fn verification_key_submission( + &mut self, + epoch_id: EpochId, + resharing: bool, + ) -> Result<(), CoconutError> { + let key_generation_state = self.state.key_derivation_state(epoch_id)?; + + // check if we have already generated the new keys and submitted verification proposal + if key_generation_state.completed() { + // TODO: ASSERT: we have a VALID key + + // 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 generated key for this epoch and submitted validation proposal" + ); + return Ok(()); + } + + // TODO: make sure we have keys for that lol + // if we have keys and we're still here it means validation failed blah blah + + // FAILURE CASE: + // check if we have already sent the verification key transaction, but it timed out or got stuck in the mempool and + // eventually got executed without us knowing about it, because it's illegal to recommit the key + let maybe_share = self + .dkg_client + .get_verification_key_share_status(epoch_id) + .await?; + if maybe_share.is_some() { + todo!("finalize, we're done") + } + + let epoch_receivers = BTreeMap::new(); + + let dealings = self + .get_valid_dealings(&epoch_receivers, epoch_id, resharing) + .await?; + if dealings.is_empty() { + todo!("not a failure per se but something along the lines: can't continue, won't continue") + } + + let dbg_dealers = dealings[&0].keys().collect::>(); + debug!("going to use dealings generated by {dbg_dealers:?}"); + + let coconut_keypair = self.derive_partial_keypair(epoch_id, epoch_receivers, dealings)?; + + if let Err(err) = persist_coconut_keypair(&coconut_keypair, &self.coconut_key_path) { + todo!() + } + + self.submit_partial_verification_key(coconut_keypair.keys.verification_key(), resharing) + .await?; + + self.state.set_coconut_keypair(coconut_keypair).await; + self.state.key_derivation_state_mut(epoch_id)?.completed = true; + + info!("DKG: Finished key derivation"); + Ok(()) + + // TODO: set completed etc. + + // let dealings = self.dkg_client.get_dealings(); + + // if state.coconut_keypair_is_some().await { + // debug!("Coconut keypair was set previously, nothing to do"); + // return Ok(()); + // } + // + // + // let threshold = state.threshold()?; + // let dealings_maps = + // deterministic_filter_dealers(dkg_client, state, epoch_id, threshold, resharing).await?; + // debug!( + // "Filtered dealers to {:?}", + // dealings_maps[0].keys().collect::>() + // ); + // let coconut_keypair = derive_partial_keypair(state, threshold, dealings_maps)?; + // debug!("Derived own coconut keypair"); + // let vk_share = coconut_keypair.verification_key().to_bs58(); + // nym_pemstore::store_keypair(&coconut_keypair, keypair_path)?; + // let res = dkg_client + // .submit_verification_key_share(vk_share, resharing) + // .await?; + // let proposal_id = find_attribute(&res.logs, "wasm", DKG_PROPOSAL_ID) + // .ok_or(CoconutError::ProposalIdError { + // reason: String::from("proposal id not found"), + // })? + // .value + // .parse::() + // .map_err(|_| CoconutError::ProposalIdError { + // reason: String::from("proposal id could not be parsed to u64"), + // })?; + // debug!( + // "Submitted own verification key share, proposal id {} is attached to it", + // proposal_id + // ); + // state.set_proposal_id(proposal_id); + // state.set_coconut_keypair(epoch_id, coconut_keypair).await; + // info!("DKG: Submitted own verification key"); + // + // Ok(()) + } } fn validate_proposal(proposal: &ProposalResponse) -> Option<(Addr, u64)> { diff --git a/nym-api/src/coconut/dkg/mod.rs b/nym-api/src/coconut/dkg/mod.rs index 362793a67b..ed0509cbdd 100644 --- a/nym-api/src/coconut/dkg/mod.rs +++ b/nym-api/src/coconut/dkg/mod.rs @@ -12,6 +12,6 @@ pub(crate) mod client; pub(crate) mod complaints; pub(crate) mod controller; pub(crate) mod dealing; +pub(crate) mod key_derivation; pub(crate) mod public_key; pub(crate) mod state; -pub(crate) mod verification_key; diff --git a/nym-api/src/coconut/dkg/public_key.rs b/nym-api/src/coconut/dkg/public_key.rs index 1b28bead50..8c0ee74a86 100644 --- a/nym-api/src/coconut/dkg/public_key.rs +++ b/nym-api/src/coconut/dkg/public_key.rs @@ -8,6 +8,18 @@ use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; impl DkgController { + /// First step of the DKG process during which the nym api will register for the key exchange + /// by submitting its: + /// - BTE public key (alongside the proof of discrete log) + /// - ed25519 public key + /// - announce address to be used by clients for obtaining credentials + /// Upon successful registration, the node will receive a unique "NodeIndex" + /// which is the x-coordinate of the to be derived keys. + /// + /// During this step any prior coconut keys will be invalidated, i.e. keys from the previous epoch + /// won't be used for issuing new credentials. + /// + /// Furthermore, if the node experienced any failures during this step, a recovery will be attempted. pub(crate) async fn public_key_submission( &mut self, epoch_id: EpochId, diff --git a/nym-api/src/coconut/dkg/state/dealing_exchange.rs b/nym-api/src/coconut/dkg/state/dealing_exchange.rs index fcc91bdc7c..97aa74751e 100644 --- a/nym-api/src/coconut/dkg/state/dealing_exchange.rs +++ b/nym-api/src/coconut/dkg/state/dealing_exchange.rs @@ -13,6 +13,8 @@ pub struct DealingExchangeState { #[serde(with = "generated_dealings")] pub(crate) generated_dealings: HashMap, + pub(crate) receiver_index: Option, + pub(crate) completed: bool, } diff --git a/nym-api/src/coconut/dkg/state/key_derivation.rs b/nym-api/src/coconut/dkg/state/key_derivation.rs new file mode 100644 index 0000000000..05dd3cc774 --- /dev/null +++ b/nym-api/src/coconut/dkg/state/key_derivation.rs @@ -0,0 +1,20 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_dkg::Threshold; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct KeyDerivationState { + pub(crate) expected_threshold: Option, // pub(crate) completed: bool, + + pub(crate) completed: bool, + // because we couldn't decrypt shares or there were no shares, etc + // failed: +} + +impl KeyDerivationState { + 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 4b31e32c4e..e82f2b4d57 100644 --- a/nym-api/src/coconut/dkg/state/mod.rs +++ b/nym-api/src/coconut/dkg/state/mod.rs @@ -4,6 +4,7 @@ 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::key_derivation::KeyDerivationState; use crate::coconut::dkg::state::registration::RegistrationState; use crate::coconut::error::CoconutError; use crate::coconut::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch}; @@ -27,6 +28,7 @@ use tokio::sync::RwLockReadGuard; use url::Url; mod dealing_exchange; +mod key_derivation; mod registration; mod serde_helpers; @@ -71,8 +73,8 @@ impl DkgParticipant { #[cfg(test)] pub(crate) fn unwrap_key(&self) -> PublicKeyWithProof { if let ParticipantState::VerifiedKey(key) = &self.state { - return key.clone() - } + return key.clone(); + } panic!("no key") } } @@ -238,6 +240,8 @@ pub(crate) struct DkgState { pub(crate) dealers: BTreeMap, pub(crate) dealing_exchange: DealingExchangeState, + + pub(crate) key_generation: KeyDerivationState, } impl DkgState { @@ -348,6 +352,25 @@ impl State { } } + /// Obtain the list of dealers for the provided epoch that have submitted valid public keys. + pub fn valid_epoch_dealers( + &self, + epoch_id: EpochId, + ) -> Result, CoconutError> { + Ok(self + .dkg_state(epoch_id)? + .dealers + .values() + .filter_map(|d| { + if d.state.is_valid() { + Some((d.address.clone(), d.assigned_index)) + } else { + None + } + }) + .collect()) + } + pub fn dkg_state(&self, epoch_id: EpochId) -> Result<&DkgState, CoconutError> { self.dkg_instances .get(&epoch_id) @@ -400,6 +423,44 @@ impl State { .ok_or(CoconutError::MissingDkgState { epoch_id }) } + pub fn key_derivation_state( + &self, + epoch_id: EpochId, + ) -> Result<&KeyDerivationState, CoconutError> { + self.dkg_instances + .get(&epoch_id) + .map(|state| &state.key_generation) + .ok_or(CoconutError::MissingDkgState { epoch_id }) + } + + pub fn key_derivation_state_mut( + &mut self, + epoch_id: EpochId, + ) -> Result<&mut KeyDerivationState, CoconutError> { + self.dkg_instances + .get_mut(&epoch_id) + .map(|state| &mut state.key_generation) + .ok_or(CoconutError::MissingDkgState { epoch_id }) + } + + pub fn threshold(&self, epoch_id: EpochId) -> Result { + self.key_derivation_state(epoch_id)? + .expected_threshold + .ok_or(CoconutError::UnavailableThreshold { epoch_id }) + } + + // pub fn assigned_index(&self, epoch_id: EpochId) -> Result { + // self.registration_state(epoch_id)? + // .assigned_index + // .ok_or(CoconutError::UnavailableAssignedIndex { epoch_id }) + // } + + pub fn receiver_index(&self, epoch_id: EpochId) -> Result { + self.dealing_exchange_state(epoch_id)? + .receiver_index + .ok_or(CoconutError::UnavailableReceiverIndex { epoch_id }) + } + pub fn persistent_state_path(&self) -> &Path { self.persistent_state_path.as_path() } @@ -456,10 +517,6 @@ impl State { self.node_index } - pub fn receiver_index(&self) -> Option { - self.receiver_index - } - pub fn current_dealers_by_addr(&self) -> BTreeMap { self.dealers .iter() @@ -508,12 +565,8 @@ impl State { self.recovered_vks = recovered_vks; } - pub async fn set_coconut_keypair( - &mut self, - epoch_id: EpochId, - coconut_keypair: nym_coconut_interface::KeyPair, - ) { - self.coconut_keypair.set(epoch_id, coconut_keypair).await + pub async fn set_coconut_keypair(&mut self, coconut_keypair: KeyPairWithEpoch) { + self.coconut_keypair.set(coconut_keypair).await } pub fn set_node_index(&mut self, node_index: Option) { diff --git a/nym-api/src/coconut/error.rs b/nym-api/src/coconut/error.rs index 9df4f640f0..2f81583114 100644 --- a/nym-api/src/coconut/error.rs +++ b/nym-api/src/coconut/error.rs @@ -141,6 +141,17 @@ pub enum CoconutError { #[error("the internal dkg state for epoch {epoch_id} is missing - we might have joined mid exchange")] MissingDkgState { epoch_id: EpochId }, + #[error( + "the node index value for {epoch_id} is not available - are you sure we are a dealer?" + )] + UnavailableAssignedIndex { epoch_id: EpochId }, + + #[error("the receiver index value for {epoch_id} is not available - are you sure we are a receiver?")] + UnavailableReceiverIndex { epoch_id: EpochId }, + + #[error("the threshold value for {epoch_id} is not available")] + UnavailableThreshold { 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 bd7dc7a9b4..1d51fd077f 100644 --- a/nym-api/src/coconut/keys/mod.rs +++ b/nym-api/src/coconut/keys/mod.rs @@ -55,7 +55,7 @@ impl KeyPair { // self.keys.read().await } - pub async fn set(&self, epoch_id: EpochId, keypair: nym_coconut_interface::KeyPair) { + pub async fn set(&self, keypair: KeyPairWithEpoch) { todo!() // let mut w_lock = self.keys.write().await; // *w_lock = Some(keypair); diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 78efde83f1..b4779c6d2b 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2021-2023 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::coconut::error::CoconutError; @@ -434,10 +434,18 @@ impl crate::coconut::client::Client for Client { )) } + async fn get_verification_key_share( + &self, + epoch_id: EpochId, + dealer: String, + ) -> Result, CoconutError> { + Ok(nyxd_query!(self, get_vk_share(epoch_id, dealer).await?).share) + } + async fn get_verification_key_shares( &self, epoch_id: EpochId, - ) -> crate::coconut::error::Result> { + ) -> Result, CoconutError> { Ok(nyxd_query!( self, get_all_verification_key_shares(epoch_id).await?