diff --git a/Cargo.lock b/Cargo.lock index f6b7be01c1..18e96326fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6309,6 +6309,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-utils", + "cw4", "nym-contracts-common", "nym-multisig-contract-common", ] diff --git a/nym-api/src/coconut/client.rs b/nym-api/src/coconut/client.rs index 8ff1f5ed79..5638bb8300 100644 --- a/nym-api/src/coconut/client.rs +++ b/nym-api/src/coconut/client.rs @@ -5,12 +5,12 @@ use crate::coconut::error::Result; use cw3::ProposalResponse; use cw4::MemberResponse; use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; -use nym_coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; +use nym_coconut_dkg_common::dealer::{DealerDetails, DealerDetailsResponse}; use nym_coconut_dkg_common::types::{ - EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, + EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, PartialContractDealing, + State, }; use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; -use nym_contracts_common::dealings::ContractSafeBytes; use nym_dkg::Threshold; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; use nym_validator_client::nyxd::{AccountId, Fee, Hash, TxResponse}; @@ -25,13 +25,19 @@ pub trait Client { &self, blinded_serial_number: String, ) -> Result; + + async fn contract_state(&self) -> Result; async fn get_current_epoch(&self) -> Result; async fn group_member(&self, addr: String) -> Result; async fn get_current_epoch_threshold(&self) -> Result>; async fn get_initial_dealers(&self) -> Result>; async fn get_self_registered_dealer_details(&self) -> Result; async fn get_current_dealers(&self) -> Result>; - async fn get_dealings(&self, idx: usize) -> Result>; + async fn get_dealings( + &self, + epoch_id: EpochId, + dealer: &str, + ) -> 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<()>; @@ -45,7 +51,7 @@ pub trait Client { ) -> Result; async fn submit_dealing( &self, - dealing_bytes: ContractSafeBytes, + dealing: PartialContractDealing, resharing: bool, ) -> Result; async fn submit_verification_key_share( diff --git a/nym-api/src/coconut/dkg/client.rs b/nym-api/src/coconut/dkg/client.rs index 0c62b6e8a4..e4a104a195 100644 --- a/nym-api/src/coconut/dkg/client.rs +++ b/nym-api/src/coconut/dkg/client.rs @@ -5,22 +5,24 @@ use crate::coconut::client::Client; use crate::coconut::error::CoconutError; use cw3::ProposalResponse; use cw4::MemberResponse; -use nym_coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; +use nym_coconut_dkg_common::dealer::{DealerDetails, DealerDetailsResponse}; use nym_coconut_dkg_common::types::{ EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, NodeIndex, + PartialContractDealing, State as ContractState, }; use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; -use nym_contracts_common::dealings::ContractSafeBytes; use nym_dkg::Threshold; use nym_validator_client::nyxd::cosmwasm_client::logs::{find_attribute, NODE_INDEX}; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; use nym_validator_client::nyxd::AccountId; +use std::time::Duration; pub(crate) struct DkgClient { inner: Box, } impl DkgClient { + // FIXME: // Some queries simply don't work the first time // Until we determine why that is, retry the query a few more times const RETRIES: usize = 3; @@ -44,11 +46,24 @@ impl DkgClient { if ret.is_ok() { return ret; } + tokio::time::sleep(Duration::from_millis(200)).await; ret = self.inner.get_current_epoch().await; } ret } + pub(crate) async fn get_contract_state(&self) -> Result { + let mut ret = self.inner.contract_state().await; + for _ in 0..Self::RETRIES { + if ret.is_ok() { + return ret; + } + tokio::time::sleep(Duration::from_millis(200)).await; + ret = self.inner.contract_state().await; + } + ret + } + pub(crate) async fn group_member(&self) -> Result { self.inner .group_member(self.get_address().await.to_string()) @@ -79,14 +94,16 @@ impl DkgClient { pub(crate) async fn get_dealings( &self, - idx: usize, - ) -> Result, CoconutError> { - let mut ret = self.inner.get_dealings(idx).await; + epoch_id: EpochId, + dealer: String, + ) -> Result, CoconutError> { + let mut ret = self.inner.get_dealings(epoch_id, &dealer).await; for _ in 0..Self::RETRIES { if ret.is_ok() { return ret; } - ret = self.inner.get_dealings(idx).await; + tokio::time::sleep(Duration::from_millis(200)).await; + ret = self.inner.get_dealings(epoch_id, &dealer).await; } ret } @@ -131,10 +148,10 @@ impl DkgClient { pub(crate) async fn submit_dealing( &self, - dealing_bytes: ContractSafeBytes, + dealing: PartialContractDealing, resharing: bool, ) -> Result<(), CoconutError> { - self.inner.submit_dealing(dealing_bytes, resharing).await?; + self.inner.submit_dealing(dealing, resharing).await?; Ok(()) } @@ -151,6 +168,7 @@ impl DkgClient { if let Ok(res) = ret { return Ok(res); } + tokio::time::sleep(Duration::from_millis(200)).await; ret = self .inner .submit_verification_key_share(share.clone(), resharing) diff --git a/nym-api/src/coconut/dkg/controller.rs b/nym-api/src/coconut/dkg/controller.rs index 89275400ca..506655f6d8 100644 --- a/nym-api/src/coconut/dkg/controller.rs +++ b/nym-api/src/coconut/dkg/controller.rs @@ -140,6 +140,7 @@ impl DkgController { verification_key_submission( &self.dkg_client, &mut self.state, + epoch.epoch_id, &keypair_path, resharing, ) diff --git a/nym-api/src/coconut/dkg/dealing.rs b/nym-api/src/coconut/dkg/dealing.rs index 266c6db66e..aa1c0961d3 100644 --- a/nym-api/src/coconut/dkg/dealing.rs +++ b/nym-api/src/coconut/dkg/dealing.rs @@ -1,13 +1,12 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::coconut::dkg; use crate::coconut::dkg::client::DkgClient; use crate::coconut::dkg::state::{ConsistentState, State}; use crate::coconut::error::CoconutError; use log::debug; -use nym_coconut_dkg_common::types::TOTAL_DEALINGS; -use nym_contracts_common::dealings::ContractSafeBytes; -use nym_dkg::bte::setup; +use nym_coconut_dkg_common::types::{ContractDealing, PartialContractDealing}; use nym_dkg::Dealing; use rand::RngCore; use std::collections::VecDeque; @@ -24,6 +23,9 @@ pub(crate) async fn dealing_exchange( return Ok(()); } + let contract_state = dkg_client.get_contract_state().await?; + let expected_key_size = contract_state.key_size; + let dealers = dkg_client.get_current_dealers().await?; let threshold = dkg_client.get_current_epoch_threshold().await?; let initial_dealers = dkg_client @@ -44,7 +46,7 @@ pub(crate) async fn dealing_exchange( // Double check that we are in resharing mode if resharing { let sk = keypair.secret_key(); - if sk.size() + 1 != TOTAL_DEALINGS { + if sk.size() + 1 != expected_key_size as usize { return Err(CoconutError::CorruptedCoconutKeyPair); } @@ -64,8 +66,13 @@ pub(crate) async fn dealing_exchange( }; let mut prior_resharing_secrets = VecDeque::from(prior_resharing_secrets); if !resharing || initial_dealers.iter().any(|d| *d == own_address) { - let params = setup(); - for _ in 0..TOTAL_DEALINGS { + let params = dkg::params(); + for dealing_index in 0..expected_key_size { + debug!( + "dealing {dealing_index} ({} out of {expected_key_size})", + dealing_index + 1 + ); + debug!( "Submitting dealing for indexes {:?} with resharing: {}", receivers.keys().collect::>(), @@ -73,14 +80,19 @@ pub(crate) async fn dealing_exchange( ); let (dealing, _) = Dealing::create( rng.clone(), - ¶ms, + params, dealer_index, state.threshold()?, &receivers, prior_resharing_secrets.pop_front(), ); + + let contract_dealing = + PartialContractDealing::new(dealing_index, ContractDealing::from(&dealing)); + + println!("submit dealing"); dkg_client - .submit_dealing(ContractSafeBytes::from(&dealing), resharing) + .submit_dealing(contract_dealing, resharing) .await?; } } else { @@ -160,7 +172,7 @@ pub(crate) mod tests { .with_dealings(&dealings_db) .with_threshold(&threshold_db), ); - let params = setup(); + let params = dkg::params(); let mut state = State::new( PathBuf::default(), PersistentState::default(), @@ -171,6 +183,8 @@ pub(crate) mod tests { state.set_node_index(Some(self_index)); let keypairs = insert_dealers(¶ms, &dealer_details_db); + let contract_state = dkg_client.get_contract_state().await.unwrap(); + dealing_exchange(&dkg_client, &mut state, OsRng, false) .await .unwrap(); @@ -187,10 +201,12 @@ pub(crate) mod tests { let dealings = dealings_db .read() .unwrap() + .get(&0) + .unwrap() .get(TEST_VALIDATORS_ADDRESS[0]) .unwrap() .clone(); - assert_eq!(dealings.len(), TOTAL_DEALINGS); + assert_eq!(dealings.len(), contract_state.key_size as usize); dealing_exchange(&dkg_client, &mut state, OsRng, false) .await @@ -198,6 +214,8 @@ pub(crate) mod tests { let new_dealings = dealings_db .read() .unwrap() + .get(&0) + .unwrap() .get(TEST_VALIDATORS_ADDRESS[0]) .unwrap() .clone(); @@ -217,7 +235,7 @@ pub(crate) mod tests { .with_dealings(&dealings_db) .with_threshold(&threshold_db), ); - let params = setup(); + let params = dkg::params(); let mut state = State::new( PathBuf::default(), PersistentState::default(), @@ -285,7 +303,9 @@ pub(crate) mod tests { .with_threshold(&threshold_db) .with_initial_dealers_db(&initial_dealers_db), ); - let params = setup(); + let contract_state = dkg_client.get_contract_state().await.unwrap(); + + let params = dkg::params(); let mut keys = ttp_keygen(&Parameters::new(4).unwrap(), 3, 4).unwrap(); let coconut_keypair = KeyPair::new(); coconut_keypair.set(Some(keys.pop().unwrap())).await; @@ -294,11 +314,11 @@ pub(crate) mod tests { PathBuf::default(), PersistentState::default(), Url::parse("localhost:8000").unwrap(), - DkgKeyPair::new(¶ms, OsRng), + DkgKeyPair::new(params, OsRng), coconut_keypair.clone(), ); state.set_node_index(Some(self_index)); - let keypairs = insert_dealers(¶ms, &dealer_details_db); + let keypairs = insert_dealers(params, &dealer_details_db); dealing_exchange(&dkg_client, &mut state, OsRng, true) .await @@ -313,14 +333,16 @@ pub(crate) mod tests { ); assert_eq!(state.threshold().unwrap(), 3); assert_eq!(state.receiver_index().unwrap(), 1); - let addr = dkg_client.get_address().await; - assert!(dealings_db.read().unwrap().get(addr.as_ref()).is_none()); + // let addr = dkg_client.get_address().await; + + // no dealings submitted for the first (zeroth) epoch + assert!(dealings_db.read().unwrap().get(&0).is_none()); let mut state = State::new( PathBuf::default(), PersistentState::default(), Url::parse("localhost:8000").unwrap(), - DkgKeyPair::new(¶ms, OsRng), + DkgKeyPair::new(params, OsRng), coconut_keypair, ); state.set_node_index(Some(self_index)); @@ -340,9 +362,11 @@ pub(crate) mod tests { let dealings = dealings_db .read() .unwrap() + .get(&0) + .unwrap() .get(TEST_VALIDATORS_ADDRESS[0]) .unwrap() .clone(); - assert_eq!(dealings.len(), TOTAL_DEALINGS); + assert_eq!(dealings.len(), contract_state.key_size as usize); } } diff --git a/nym-api/src/coconut/dkg/mod.rs b/nym-api/src/coconut/dkg/mod.rs index ac82cf97c3..362793a67b 100644 --- a/nym-api/src/coconut/dkg/mod.rs +++ b/nym-api/src/coconut/dkg/mod.rs @@ -1,6 +1,13 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use std::sync::OnceLock; + +pub(crate) fn params() -> &'static nym_dkg::bte::Params { + static PARAMS: OnceLock = OnceLock::new(); + PARAMS.get_or_init(nym_dkg::bte::setup) +} + pub(crate) mod client; pub(crate) mod complaints; pub(crate) mod controller; diff --git a/nym-api/src/coconut/dkg/state.rs b/nym-api/src/coconut/dkg/state.rs index b22cd81f50..8bb054a6b5 100644 --- a/nym-api/src/coconut/dkg/state.rs +++ b/nym-api/src/coconut/dkg/state.rs @@ -304,6 +304,7 @@ impl State { .collect() } + // 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() @@ -364,8 +365,7 @@ impl State { .find(|(addr, _)| *addr == dealer_addr) { debug!( - "Dealer {} misbehaved: {:?}. It will be marked locally as bad dealer and ignored", - dealer_addr, reason + "Dealer {dealer_addr} misbehaved: {reason:?}. It will be marked locally as bad dealer and ignored", ); *value = Err(reason); } @@ -395,7 +395,6 @@ impl State { self.was_in_progress = true; } - #[cfg(test)] pub fn all_dealers(&self) -> &BTreeMap> { &self.dealers } diff --git a/nym-api/src/coconut/dkg/verification_key.rs b/nym-api/src/coconut/dkg/verification_key.rs index 78e490af73..7a8ec41ef0 100644 --- a/nym-api/src/coconut/dkg/verification_key.rs +++ b/nym-api/src/coconut/dkg/verification_key.rs @@ -1,6 +1,7 @@ // Copyright 2022 - 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::state::{ConsistentState, State}; @@ -13,24 +14,30 @@ 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::{NodeIndex, TOTAL_DEALINGS}; +use nym_coconut_dkg_common::types::{EpochId, NodeIndex}; use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs; use nym_coconut_interface::KeyPair as CoconutKeyPair; -use nym_dkg::bte::{decrypt_share, setup}; +use nym_dkg::bte::decrypt_share; use nym_dkg::error::DkgError; use nym_dkg::{combine_shares, try_recover_verification_keys, Dealing, Threshold}; use nym_pemstore::KeyPairPath; use nym_validator_client::nyxd::cosmwasm_client::logs::find_attribute; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; // Filter the dealers based on what dealing they posted (or not) in the contract + +// TODO: change the return type to make sure that: +// - each entry has the same number of dealings +// - dealer data is not duplicated +// - each dealer has submitted all or nothing async fn deterministic_filter_dealers( dkg_client: &DkgClient, state: &mut State, + epoch_id: EpochId, threshold: Threshold, resharing: bool, ) -> Result>, CoconutError> { - let mut dealings_maps = vec![]; + let mut dealings_maps = Vec::new(); let initial_dealers_by_addr = state.current_dealers_by_addr(); let initial_receivers = state.current_dealers_by_idx(); let initial_resharing_dealers = if resharing { @@ -43,36 +50,46 @@ async fn deterministic_filter_dealers( vec![] }; - let params = setup(); + let params = dkg::params(); - for idx in 0..TOTAL_DEALINGS { - let dealings = dkg_client.get_dealings(idx).await?; + // note: this is a temporary solution to replicate the behaviour of the old code so that I wouldn't need to + // fix the filtering in this PR, because the old code is quite buggy and misses few edge cases + let mut raw_dealings = HashMap::new(); + for dealer in state.all_dealers().keys() { + let dealer_dealings = dkg_client + .get_dealings(epoch_id, dealer.to_string()) + .await?; + for dealing in dealer_dealings { + let old_contract_dealing = raw_dealings.entry(dealing.index).or_insert(Vec::new()); + old_contract_dealing.push((dealer.clone(), dealing.data)) + } + } + + // this is a temporary thing to reintroduce the bug to make sure tests still pass : ) + // i will fix it properly in next PR + for dealing_index in 0..5 { + let dealings = raw_dealings.remove(&dealing_index).unwrap_or_default(); let dealings_map = - BTreeMap::from_iter(dealings.into_iter().filter_map(|contract_dealing| { - match Dealing::try_from(&contract_dealing.dealing) { + BTreeMap::from_iter(dealings.into_iter().filter_map(|(dealer, dealing)| { + match Dealing::try_from(&dealing) { Ok(dealing) => { if dealing - .verify(¶ms, threshold, &initial_receivers, None) + .verify(params, threshold, &initial_receivers, None) .is_err() { state.mark_bad_dealer( - &contract_dealing.dealer, + &dealer, ComplaintReason::DealingVerificationError, ); None - } else if let Some(idx) = - initial_dealers_by_addr.get(&contract_dealing.dealer) - { - Some((*idx, (contract_dealing.dealer, dealing))) } else { - None + initial_dealers_by_addr + .get(&dealer) + .map(|idx| (*idx, (dealer, dealing))) } } Err(_) => { - state.mark_bad_dealer( - &contract_dealing.dealer, - ComplaintReason::MalformedDealing, - ); + state.mark_bad_dealer(&dealer, ComplaintReason::MalformedDealing); None } } @@ -80,6 +97,40 @@ async fn deterministic_filter_dealers( dealings_maps.push(dealings_map); } + // + // + // for dealer in initial_dealers_by_addr.keys() { + // let Some(dealer_index) = initial_dealers_by_addr.get(dealer) else { + // warn!("could not obtain dealer index of {dealer}"); + // continue; + // }; + // + // let dealer_dealings = dkg_client + // .get_dealings(epoch_id, dealer.to_string()) + // .await?; + // + // for contract_dealing in dealer_dealings { + // match Dealing::try_from(&contract_dealing.data) { + // // FIXME: bug: this doesn't check resharing + // Ok(dealing) => { + // if let Err(err) = dealing.verify(params, threshold, &initial_receivers, None) { + // println!("dealing verification failure from {dealer}: {err}"); + // state.mark_bad_dealer(dealer, ComplaintReason::DealingVerificationError); + // } else { + // let entry = dealings_maps + // .entry(contract_dealing.index) + // .or_insert(BTreeMap::new()); + // entry.insert(*dealer_index, (dealer.clone(), dealing)); + // } + // } + // Err(err) => { + // warn!("malformed dealing from {dealer}: {err}"); + // state.mark_bad_dealer(dealer, ComplaintReason::MalformedDealing); + // } + // } + // } + // } + for (addr, _) in initial_dealers_by_addr.iter() { // in resharing mode, we don't commit dealings from dealers outside the initial set if !resharing || initial_resharing_dealers.contains(addr) { @@ -155,6 +206,7 @@ fn derive_partial_keypair( pub(crate) async fn verification_key_submission( dkg_client: &DkgClient, state: &mut State, + epoch_id: EpochId, keypair_path: &KeyPairPath, resharing: bool, ) -> Result<(), CoconutError> { @@ -165,7 +217,7 @@ pub(crate) async fn verification_key_submission( let threshold = state.threshold()?; let dealings_maps = - deterministic_filter_dealers(dkg_client, state, threshold, resharing).await?; + deterministic_filter_dealers(dkg_client, state, epoch_id, threshold, resharing).await?; debug!( "Filtered dealers to {:?}", dealings_maps[0].keys().collect::>() @@ -307,9 +359,8 @@ pub(crate) mod tests { use crate::coconut::KeyPair; use nym_coconut::aggregate_verification_keys; use nym_coconut_dkg_common::dealer::DealerDetails; - use nym_coconut_dkg_common::types::InitialReplacementData; + use nym_coconut_dkg_common::types::{EpochId, InitialReplacementData, PartialContractDealing}; use nym_coconut_dkg_common::verification_key::ContractVKShare; - use nym_contracts_common::dealings::ContractSafeBytes; use nym_dkg::bte::keys::KeyPair as DkgKeyPair; use nym_validator_client::nyxd::AccountId; use rand::rngs::OsRng; @@ -323,7 +374,9 @@ pub(crate) mod tests { struct MockContractDb { dealer_details_db: Arc>>, - dealings_db: Arc>>>, + // it's a really bad practice, but I'm not going to be changing it now... + #[allow(clippy::type_complexity)] + dealings_db: Arc>>>>, proposal_db: Arc>>, verification_share_db: Arc>>, threshold_db: Arc>>, @@ -351,7 +404,7 @@ pub(crate) mod tests { ]; async fn prepare_clients_and_states(db: &MockContractDb) -> Vec<(DkgClient, State)> { - let params = setup(); + let params = dkg::params(); let mut clients_and_states = vec![]; for addr in TEST_VALIDATORS_ADDRESS { @@ -364,7 +417,7 @@ pub(crate) mod tests { .with_threshold(&db.threshold_db) .with_initial_dealers_db(&db.initial_dealers_db), ); - let keypair = DkgKeyPair::new(¶ms, OsRng); + let keypair = DkgKeyPair::new(params, OsRng); let state = State::new( PathBuf::default(), PersistentState::default(), @@ -403,7 +456,7 @@ pub(crate) mod tests { let private_key_path = temp_dir().join(format!("private{}.pem", random_file)); let public_key_path = temp_dir().join(format!("public{}.pem", random_file)); let keypair_path = KeyPairPath::new(private_key_path.clone(), public_key_path.clone()); - verification_key_submission(dkg_client, state, &keypair_path, false) + verification_key_submission(dkg_client, state, 0, &keypair_path, false) .await .unwrap(); std::fs::remove_file(private_key_path).unwrap(); @@ -441,11 +494,13 @@ pub(crate) mod tests { async fn check_dealers_filter_all_good() { let db = MockContractDb::new(); let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await; + let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap(); + for (dkg_client, state) in clients_and_states.iter_mut() { - let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) + let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false) .await .unwrap(); - assert_eq!(filtered.len(), TOTAL_DEALINGS); + assert_eq!(filtered.len(), contract_state.key_size as usize); for mapping in filtered.iter() { assert_eq!(mapping.len(), 4); } @@ -457,23 +512,27 @@ pub(crate) mod tests { async fn check_dealers_filter_one_bad_dealing() { let db = MockContractDb::new(); let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await; + let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap(); // corrupt just one dealing db.dealings_db .write() .unwrap() - .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) - .and_modify(|dealings| { - let mut last = dealings.pop().unwrap(); - last.0.pop(); - dealings.push(last); + .entry(0) + .and_modify(|epoch_dealings| { + let validator_dealings = epoch_dealings + .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + .or_default(); + let mut last = validator_dealings.pop().unwrap(); + last.data.0.pop(); + validator_dealings.push(last); }); for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { - let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) + let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false) .await .unwrap(); - assert_eq!(filtered.len(), TOTAL_DEALINGS); + assert_eq!(filtered.len(), contract_state.key_size as usize); let corrupted_status = state .all_dealers() .get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])) @@ -489,6 +548,7 @@ pub(crate) mod tests { async fn check_dealers_resharing_filter_one_missing_dealing() { let db = MockContractDb::new(); let mut clients_and_states = prepare_clients_and_states(&db).await; + let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap(); // add all but the first dealing for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { @@ -502,10 +562,12 @@ pub(crate) mod tests { initial_dealers: vec![Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])], initial_height: 1, }); - let filtered = deterministic_filter_dealers(dkg_client, state, 2, true) + println!("filter"); + let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, true) .await .unwrap(); - assert_eq!(filtered.len(), TOTAL_DEALINGS); + println!("filtered"); + assert_eq!(filtered.len(), contract_state.key_size as usize); let corrupted_status = state .all_dealers() .get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])) @@ -522,6 +584,7 @@ pub(crate) mod tests { async fn check_dealers_resharing_filter_one_noninitial_missing_dealing() { let db = MockContractDb::new(); let mut clients_and_states = prepare_clients_and_states(&db).await; + let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap(); // add all but the first dealing for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { @@ -535,10 +598,10 @@ pub(crate) mod tests { initial_dealers: vec![], initial_height: 1, }); - let filtered = deterministic_filter_dealers(dkg_client, state, 2, true) + let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, true) .await .unwrap(); - assert_eq!(filtered.len(), TOTAL_DEALINGS); + assert_eq!(filtered.len(), contract_state.key_size as usize); assert!(state .all_dealers() .get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])) @@ -553,23 +616,27 @@ pub(crate) mod tests { async fn check_dealers_filter_all_bad_dealings() { let db = MockContractDb::new(); let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await; + let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap(); // corrupt all dealings of one address db.dealings_db .write() .unwrap() - .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) - .and_modify(|dealings| { - dealings.iter_mut().for_each(|dealing| { - dealing.0.pop(); + .entry(0) + .and_modify(|epoch_dealings| { + let validator_dealings = epoch_dealings + .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + .or_default(); + validator_dealings.iter_mut().for_each(|dealing| { + dealing.data.0.pop(); }); }); for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { - let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) + let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false) .await .unwrap(); - assert_eq!(filtered.len(), TOTAL_DEALINGS); + assert_eq!(filtered.len(), contract_state.key_size as usize); for mapping in filtered.iter() { assert_eq!(mapping.len(), 3); } @@ -588,28 +655,32 @@ pub(crate) mod tests { async fn check_dealers_filter_malformed_dealing() { let db = MockContractDb::new(); let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await; + let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap(); // corrupt just one dealing db.dealings_db .write() .unwrap() - .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) - .and_modify(|dealings| { - let mut last = dealings.pop().unwrap(); - last.0.pop(); - dealings.push(last); + .entry(0) + .and_modify(|epoch_dealings| { + let validator_dealings = epoch_dealings + .get_mut(TEST_VALIDATORS_ADDRESS[0]) + .expect("no dealing"); + let mut last = validator_dealings.pop().unwrap(); + last.data.0.pop(); + validator_dealings.push(last); }); for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { - deterministic_filter_dealers(dkg_client, state, 2, false) + deterministic_filter_dealers(dkg_client, state, 0, 2, false) .await .unwrap(); // second filter will leave behind the bad dealer and surface why it was left out // in the first place - let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) + let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false) .await .unwrap(); - assert_eq!(filtered.len(), TOTAL_DEALINGS); + assert_eq!(filtered.len(), contract_state.key_size as usize); let corrupted_status = state .all_dealers() .get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])) @@ -625,33 +696,37 @@ pub(crate) mod tests { async fn check_dealers_filter_dealing_verification_error() { let db = MockContractDb::new(); let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await; + let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap(); // corrupt just one dealing db.dealings_db .write() .unwrap() - .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) - .and_modify(|dealings| { - let mut last = dealings.pop().unwrap(); - let value = last.0.pop().unwrap(); + .entry(0) + .and_modify(|epoch_dealings| { + let validator_dealings = epoch_dealings + .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + .or_default(); + let mut last = validator_dealings.pop().unwrap(); + let value = last.data.0.pop().unwrap(); if value == 42 { - last.0.push(43); + last.data.0.push(43); } else { - last.0.push(42); + last.data.0.push(42); } - dealings.push(last); + validator_dealings.push(last); }); for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { - deterministic_filter_dealers(dkg_client, state, 2, false) + deterministic_filter_dealers(dkg_client, state, 0, 2, false) .await .unwrap(); // second filter will leave behind the bad dealer and surface why it was left out // in the first place - let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) + let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false) .await .unwrap(); - assert_eq!(filtered.len(), TOTAL_DEALINGS); + assert_eq!(filtered.len(), contract_state.key_size as usize); let corrupted_status = state .all_dealers() .get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])) @@ -668,7 +743,7 @@ pub(crate) mod tests { let db = MockContractDb::new(); let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await; for (dkg_client, state) in clients_and_states.iter_mut() { - let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) + let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false) .await .unwrap(); assert!(derive_partial_keypair(state, 2, filtered).is_ok()); @@ -685,15 +760,18 @@ pub(crate) mod tests { db.dealings_db .write() .unwrap() - .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) - .and_modify(|dealings| { - let mut last = dealings.pop().unwrap(); - last.0.pop(); - dealings.push(last); + .entry(0) + .and_modify(|epoch_dealings| { + let validator_dealings = epoch_dealings + .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + .or_default(); + let mut last = validator_dealings.pop().unwrap(); + last.data.0.pop(); + validator_dealings.push(last); }); for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { - let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) + let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false) .await .unwrap(); assert!(derive_partial_keypair(state, 2, filtered).is_ok()); @@ -863,7 +941,7 @@ pub(crate) mod tests { .with_threshold(&db.threshold_db) .with_initial_dealers_db(&db.initial_dealers_db), ); - let keypair = DkgKeyPair::new(&setup(), OsRng); + let keypair = DkgKeyPair::new(dkg::params(), OsRng); let state = State::new( PathBuf::default(), PersistentState::default(), @@ -906,7 +984,7 @@ pub(crate) mod tests { let private_key_path = temp_dir().join(format!("private{}.pem", random_file)); let public_key_path = temp_dir().join(format!("public{}.pem", random_file)); let keypair_path = KeyPairPath::new(private_key_path.clone(), public_key_path.clone()); - verification_key_submission(dkg_client, state, &keypair_path, true) + verification_key_submission(dkg_client, state, 0, &keypair_path, true) .await .unwrap(); std::fs::remove_file(private_key_path).unwrap(); @@ -967,7 +1045,7 @@ pub(crate) mod tests { .with_threshold(&db.threshold_db) .with_initial_dealers_db(&db.initial_dealers_db), ); - let keypair = DkgKeyPair::new(&setup(), OsRng); + let keypair = DkgKeyPair::new(dkg::params(), OsRng); let state = State::new( PathBuf::default(), PersistentState::default(), @@ -986,7 +1064,7 @@ pub(crate) mod tests { .with_threshold(&db.threshold_db) .with_initial_dealers_db(&db.initial_dealers_db), ); - let keypair = DkgKeyPair::new(&setup(), OsRng); + let keypair = DkgKeyPair::new(dkg::params(), OsRng); let state2 = State::new( PathBuf::default(), PersistentState::default(), @@ -1022,7 +1100,7 @@ pub(crate) mod tests { let private_key_path = temp_dir().join(format!("private{}.pem", random_file)); let public_key_path = temp_dir().join(format!("public{}.pem", random_file)); let keypair_path = KeyPairPath::new(private_key_path.clone(), public_key_path.clone()); - verification_key_submission(dkg_client, state, &keypair_path, false) + verification_key_submission(dkg_client, state, 0, &keypair_path, false) .await .unwrap(); std::fs::remove_file(private_key_path).unwrap(); @@ -1098,7 +1176,7 @@ pub(crate) mod tests { let private_key_path = temp_dir().join(format!("private{}.pem", random_file)); let public_key_path = temp_dir().join(format!("public{}.pem", random_file)); let keypair_path = KeyPairPath::new(private_key_path.clone(), public_key_path.clone()); - verification_key_submission(dkg_client, state, &keypair_path, true) + verification_key_submission(dkg_client, state, 0, &keypair_path, true) .await .unwrap(); std::fs::remove_file(private_key_path).unwrap(); diff --git a/nym-api/src/coconut/error.rs b/nym-api/src/coconut/error.rs index 290f448fb1..05e89b4081 100644 --- a/nym-api/src/coconut/error.rs +++ b/nym-api/src/coconut/error.rs @@ -124,6 +124,11 @@ pub enum CoconutError { // I guess we should make this one a bit more detailed #[error("the provided query arguments were invalid")] InvalidQueryArguments, + + #[error("insufficient number of dealings provided to derive the key")] + InsufficientDealings { + // TODO: details + }, } impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { diff --git a/nym-api/src/coconut/tests/mod.rs b/nym-api/src/coconut/tests/mod.rs index 6c7f70ad41..bc2b2f7a84 100644 --- a/nym-api/src/coconut/tests/mod.rs +++ b/nym-api/src/coconut/tests/mod.rs @@ -8,7 +8,7 @@ use crate::support::storage::NymApiStorage; use async_trait::async_trait; use cosmwasm_std::{coin, to_binary, Addr, CosmosMsg, Decimal, WasmMsg}; use cw3::ProposalResponse; -use cw4::MemberResponse; +use cw4::{Cw4Contract, MemberResponse}; use nym_api_requests::coconut::models::{IssuedCredentialBody, IssuedCredentialResponse}; use nym_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, @@ -22,17 +22,15 @@ use nym_coconut_bandwidth_contract_common::events::{ use nym_coconut_bandwidth_contract_common::spend_credential::{ SpendCredential, SpendCredentialResponse, }; -use nym_coconut_dkg_common::dealer::{ - ContractDealing, DealerDetails, DealerDetailsResponse, DealerType, -}; +use nym_coconut_dkg_common::dealer::{DealerDetails, DealerDetailsResponse, DealerType}; use nym_coconut_dkg_common::event_attributes::{DKG_PROPOSAL_ID, NODE_INDEX}; use nym_coconut_dkg_common::types::{ - EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, TOTAL_DEALINGS, + EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, PartialContractDealing, + State as ContractState, }; use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use nym_coconut_interface::{hash_to_scalar, Credential, VerificationKey}; use nym_config::defaults::VOUCHER_INFO; -use nym_contracts_common::dealings::ContractSafeBytes; use nym_credentials::coconut::bandwidth::BandwidthVoucher; use nym_crypto::asymmetric::{encryption, identity}; use nym_dkg::Threshold; @@ -67,9 +65,10 @@ pub(crate) struct DummyClient { spent_credential_db: Arc>>, epoch: Arc>, + contract_state: Arc>, dealer_details: Arc>>, threshold: Arc>>, - dealings: Arc>>>, + dealings: Arc>>>>, verification_share: Arc>>, group_db: Arc>>, initial_dealers_db: Arc>>, @@ -83,6 +82,12 @@ impl DummyClient { proposal_db: Arc::new(RwLock::new(HashMap::new())), spent_credential_db: Arc::new(RwLock::new(HashMap::new())), epoch: Arc::new(RwLock::new(Epoch::default())), + contract_state: Arc::new(RwLock::new(ContractState { + mix_denom: TEST_COIN_DENOM.to_string(), + multisig_addr: Addr::unchecked("dummy address"), + group_addr: Cw4Contract::new(Addr::unchecked("dummy cw4")), + key_size: 5, + })), dealer_details: Arc::new(RwLock::new(HashMap::new())), threshold: Arc::new(RwLock::new(None)), dealings: Arc::new(RwLock::new(HashMap::new())), @@ -133,7 +138,7 @@ impl DummyClient { pub fn with_dealings( mut self, - dealings: &Arc>>>, + dealings: &Arc>>>>, ) -> Self { self.dealings = Arc::clone(dealings); self @@ -203,6 +208,10 @@ impl super::client::Client for DummyClient { }) } + async fn contract_state(&self) -> Result { + Ok(self.contract_state.read().unwrap().clone()) + } + async fn get_current_epoch(&self) -> Result { Ok(*self.epoch.read().unwrap()) } @@ -259,17 +268,21 @@ impl super::client::Client for DummyClient { .collect()) } - async fn get_dealings(&self, idx: usize) -> Result> { + async fn get_dealings( + &self, + epoch_id: EpochId, + dealer: &str, + ) -> Result> { Ok(self .dealings .read() .unwrap() - .iter() - .map(|(dealer, dealings)| ContractDealing { - dealing: dealings.get(idx).unwrap().clone(), - dealer: Addr::unchecked(dealer), - }) - .collect()) + .get(&epoch_id) + .cloned() + .unwrap_or_default() + .get(dealer) + .cloned() + .unwrap_or_default()) } async fn get_verification_key_shares( @@ -367,19 +380,16 @@ impl super::client::Client for DummyClient { async fn submit_dealing( &self, - dealing_bytes: ContractSafeBytes, + dealing: PartialContractDealing, _resharing: bool, ) -> Result { - self.dealings - .write() - .unwrap() + let current_epoch = self.epoch.read().unwrap().epoch_id; + let mut guard = self.dealings.write().unwrap(); + let epoch_dealings = guard.entry(current_epoch).or_default(); + let existing_dealings = epoch_dealings .entry(self.validator_address.to_string()) - .and_modify(|v| { - if v.len() < TOTAL_DEALINGS { - v.push(dealing_bytes.clone()) - } - }) - .or_insert_with(|| vec![dealing_bytes]); + .or_default(); + existing_dealings.push(dealing); Ok(ExecuteResult { logs: vec![], diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 452f93569f..f2df0b5fa5 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -10,14 +10,13 @@ use cw3::ProposalResponse; use cw4::MemberResponse; use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; use nym_coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; -use nym_coconut_dkg_common::types::InitialReplacementData; +use nym_coconut_dkg_common::types::{InitialReplacementData, PartialContractDealing, State}; use nym_coconut_dkg_common::{ - dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}, + dealer::{DealerDetails, DealerDetailsResponse}, types::{EncodedBTEPublicKeyWithProof, Epoch, EpochId}, verification_key::{ContractVKShare, VerificationKeyShare}, }; use nym_config::defaults::{ChainDetails, NymNetworkDetails}; -use nym_contracts_common::dealings::ContractSafeBytes; use nym_ephemera_common::msg::QueryMsg as EphemeraQueryMsg; use nym_ephemera_common::types::JsonPeerInfo; use nym_mixnet_contract_common::families::FamilyHead; @@ -374,6 +373,10 @@ impl crate::coconut::client::Client for Client { )) } + async fn contract_state(&self) -> crate::coconut::error::Result { + Ok(nyxd_query!(self, get_state().await?)) + } + async fn get_current_epoch(&self) -> crate::coconut::error::Result { Ok(nyxd_query!(self, get_current_epoch().await?)) } @@ -407,9 +410,10 @@ impl crate::coconut::client::Client for Client { async fn get_dealings( &self, - idx: usize, - ) -> crate::coconut::error::Result> { - Ok(nyxd_query!(self, get_all_epoch_dealings(idx as u64).await?)) + epoch_id: EpochId, + dealer: &str, + ) -> crate::coconut::error::Result> { + Ok(nyxd_query!(self, get_all_dealer_dealings(epoch_id, dealer).await?)) } async fn get_verification_key_shares( @@ -456,12 +460,12 @@ impl crate::coconut::client::Client for Client { async fn submit_dealing( &self, - dealing_bytes: ContractSafeBytes, + dealing: PartialContractDealing, resharing: bool, ) -> Result { Ok(nyxd_signing!( self, - submit_dealing_bytes(dealing_bytes, resharing, None).await? + submit_dealing_bytes(dealing, resharing, None).await? )) } diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 6de58c0993..de6e7edcba 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -3814,6 +3814,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-utils", + "cw4", "nym-contracts-common", "nym-multisig-contract-common", ] diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index d35a3c4c6a..62297f25dd 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3220,6 +3220,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-utils", + "cw4", "nym-contracts-common", "nym-multisig-contract-common", ]