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 de9b4e91d9..15d25c9482 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs @@ -64,7 +64,14 @@ pub fn to_cosmos_msg( Ok(msg) } +// DKG SAFETY: +// each legit verification proposal will only contain a single execute msg, +// if they have more than one, we can safely ignore it pub fn owner_from_cosmos_msgs(msgs: &[CosmosMsg]) -> Option { + if msgs.len() != 1 { + return None + } + if let Some(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: _, msg, diff --git a/common/nymcoconut/src/scheme/verification.rs b/common/nymcoconut/src/scheme/verification.rs index 0addda985a..1373c30bc7 100644 --- a/common/nymcoconut/src/scheme/verification.rs +++ b/common/nymcoconut/src/scheme/verification.rs @@ -228,10 +228,11 @@ pub fn check_vk_pairing( // safety: we made an explicit check for if the length of the slice is 0, thus unwrap here is fine #[allow(clippy::unwrap_used)] - if &vk.alpha != *dkg_values.last().as_ref().unwrap() { + if &vk.alpha != *dkg_values.first().as_ref().unwrap() { return false; } - if dkg_values + let dkg_betas = &dkg_values[1..]; + if dkg_betas .iter() .zip(vk.beta_g2.iter()) .any(|(dkg_beta, vk_beta)| dkg_beta != vk_beta) @@ -329,8 +330,9 @@ mod tests { let params = setup(2).unwrap(); let keypair = keygen(¶ms); let vk = keypair.verification_key(); - let mut dkg_values = vk.beta_g2.clone(); - dkg_values.push(vk.alpha); + + let mut dkg_values = vec![vk.alpha]; + dkg_values.append(&mut vk.beta_g2.clone()); assert!(check_vk_pairing(¶ms, &dkg_values, vk)); } diff --git a/nym-api/src/coconut/client.rs b/nym-api/src/coconut/client.rs index 8ac91460d1..f3317c2266 100644 --- a/nym-api/src/coconut/client.rs +++ b/nym-api/src/coconut/client.rs @@ -19,6 +19,8 @@ use nym_validator_client::nyxd::{AccountId, Fee, Hash, TxResponse}; #[async_trait] pub trait Client { async fn address(&self) -> AccountId; + + async fn dkg_contract_address(&self) -> Result; async fn get_tx(&self, tx_hash: Hash) -> Result; async fn get_proposal(&self, proposal_id: u64) -> Result; async fn list_proposals(&self) -> Result>; diff --git a/nym-api/src/coconut/dkg/client.rs b/nym-api/src/coconut/dkg/client.rs index d6bbe4202c..b9b0d4d2a3 100644 --- a/nym-api/src/coconut/dkg/client.rs +++ b/nym-api/src/coconut/dkg/client.rs @@ -35,6 +35,10 @@ impl DkgClient { self.inner.address().await } + pub(crate) async fn dkg_contract_address(&self) -> Result { + self.inner.dkg_contract_address().await + } + pub(crate) async fn get_current_epoch(&self) -> Result { self.inner.get_current_epoch().await } diff --git a/nym-api/src/coconut/dkg/controller/mod.rs b/nym-api/src/coconut/dkg/controller/mod.rs index 7558ff7d1c..36b1c93323 100644 --- a/nym-api/src/coconut/dkg/controller/mod.rs +++ b/nym-api/src/coconut/dkg/controller/mod.rs @@ -3,9 +3,7 @@ use crate::coconut::dkg::client::DkgClient; use crate::coconut::dkg::controller::error::DkgError; -use crate::coconut::dkg::key_derivation::{ - verification_key_finalization, verification_key_validation, -}; +use crate::coconut::dkg::key_finalization::verification_key_finalization; use crate::coconut::dkg::state::{ConsistentState, PersistentState, State}; use crate::coconut::keys::KeyPair as CoconutKeyPair; use crate::nyxd; @@ -158,17 +156,19 @@ impl DkgController { async fn handle_verification_key_validation( &mut self, + epoch_id: EpochId, resharing: bool, ) -> Result<(), DkgError> { debug!("DKG: verification key validation (resharing: {resharing})"); - verification_key_validation(&self.dkg_client, &mut self.state, resharing) + self.verification_key_validation(epoch_id, resharing) .await .map_err(|source| DkgError::VerificationKeyValidationFailure { source }) } async fn handle_verification_key_finalization( &mut self, + epoch_id: EpochId, resharing: bool, ) -> Result<(), DkgError> { debug!("DKG: verification key finalization (resharing: {resharing})"); @@ -218,10 +218,12 @@ impl DkgController { .await? } EpochState::VerificationKeyValidation { resharing } => { - self.handle_verification_key_validation(resharing).await? + self.handle_verification_key_validation(epoch.epoch_id, resharing) + .await? } EpochState::VerificationKeyFinalization { resharing } => { - self.handle_verification_key_finalization(resharing).await? + self.handle_verification_key_finalization(epoch.epoch_id, resharing) + .await? } // Just wait, in case we need to redo dkg at some point EpochState::InProgress => self.handle_in_progress().await?, diff --git a/nym-api/src/coconut/dkg/dealing.rs b/nym-api/src/coconut/dkg/dealing.rs index b58fc06750..2a1ebce273 100644 --- a/nym-api/src/coconut/dkg/dealing.rs +++ b/nym-api/src/coconut/dkg/dealing.rs @@ -60,16 +60,7 @@ impl DkgController { // 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 filtered_receivers = self.state.valid_epoch_receivers_keys(epoch_id)?; 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}"); @@ -283,7 +274,7 @@ impl DkgController { .expected_threshold = Some(threshold); // establish our receiver index - let sorted_dealers = &self.state.dkg_state(epoch_id)?.dealers; + let sorted_dealers = &self.state.dkg_state(epoch_id)?.dealing_exchange.dealers; let Some(receiver_index) = sorted_dealers.keys().position(|idx| idx == &dealer_index) else { // this branch should be impossible as `dealing_exchange` should never be called unless we're actually a dealer @@ -452,7 +443,11 @@ pub(crate) mod tests { let key_size = controller.dkg_client.get_contract_state().await?.key_size; // initial state - assert!(controller.state.dkg_state(epoch)?.dealers.is_empty()); + assert!(controller + .state + .dealing_exchange_state(epoch)? + .dealers + .is_empty()); assert!(controller .state .dealing_exchange_state(epoch)? @@ -473,7 +468,7 @@ pub(crate) mod tests { .collect::>(); let dealers = controller .state - .dkg_state(epoch)? + .dealing_exchange_state(epoch)? .dealers .values() .map(|p| (p.assigned_index, p.unwrap_key())) diff --git a/nym-api/src/coconut/dkg/key_derivation.rs b/nym-api/src/coconut/dkg/key_derivation.rs index 984828de41..274ab43d3a 100644 --- a/nym-api/src/coconut/dkg/key_derivation.rs +++ b/nym-api/src/coconut/dkg/key_derivation.rs @@ -24,192 +24,194 @@ use nym_dkg::bte::{decrypt_share, PublicKey}; use nym_dkg::error::DkgError; use nym_dkg::{bte, combine_shares, try_recover_verification_keys, Dealing, Threshold}; use nym_pemstore::KeyPairPath; +use nym_validator_client::nyxd::bip32::secp256k1::elliptic_curve::group; +use nym_validator_client::nyxd::bip32::secp256k1::elliptic_curve::group::GroupEncoding; use nym_validator_client::nyxd::cosmwasm_client::logs::find_attribute; use rand::{CryptoRng, RngCore}; use std::collections::{BTreeMap, HashMap}; use std::path::PathBuf; -// 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> { - // 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(); - let initial_resharing_dealers = if resharing { - dkg_client - .get_initial_dealers() - .await? - .map(|d| d.initial_dealers) - .unwrap_or_default() - } else { - vec![] - }; - - let params = dkg::params(); - - // 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(|(dealer, dealing)| { - match Dealing::try_from(&dealing) { - Ok(dealing) => { - if dealing - .verify(params, threshold, &initial_receivers, None) - .is_err() - { - state.mark_bad_dealer( - &dealer, - ComplaintReason::DealingVerificationError, - ); - None - } else { - initial_dealers_by_addr - .get(&dealer) - .map(|idx| (*idx, (dealer, dealing))) - } - } - Err(_) => { - state.mark_bad_dealer(&dealer, ComplaintReason::MalformedDealing); - None - } - } - })); - 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) { - for dealings_map in dealings_maps.iter() { - if !dealings_map.iter().any(|(_, (address, _))| address == addr) { - state.mark_bad_dealer(addr, ComplaintReason::MissingDealing); - break; - } - } - } - } - - Ok(dealings_maps) -} - -fn derive_partial_keypair( - state: &mut State, - threshold: Threshold, - dealings_maps: Vec>, -) -> Result { - let filtered_receivers_by_idx = state.current_dealers_by_idx(); - let filtered_dealers_by_addr = state.current_dealers_by_addr(); - let dk = state.dkg_keypair().private_key(); - let node_index_value = state.receiver_index_value()?; - let mut scalars = vec![]; - let mut recovered_vks = vec![]; - for dealings_map in dealings_maps.into_iter() { - let (filtered_dealers, filtered_dealings): (Vec<_>, Vec<_>) = dealings_map - .into_iter() - .filter_map(|(idx, (addr, dealing))| { - if filtered_dealers_by_addr.keys().any(|a| addr == *a) { - Some((idx, dealing)) - } else { - None - } - }) - .unzip(); - debug!( - "Recovering verification keys from dealings of dealers {:?} with receivers {:?}", - filtered_dealers, - filtered_receivers_by_idx.keys().collect::>() - ); - let recovered = try_recover_verification_keys( - &filtered_dealings, - threshold, - &filtered_receivers_by_idx, - )?; - recovered_vks.push(recovered); - - debug!("Decrypting shares"); - let shares = filtered_dealings - .iter() - .map(|dealing| decrypt_share(dk, node_index_value, &dealing.ciphertexts, None)) - .collect::>()?; - debug!("Combining shares into one secret"); - let scalar = combine_shares(shares, &filtered_dealers)?; - scalars.push(scalar); - } - state.set_recovered_vks(recovered_vks); - - let x = scalars.pop().ok_or(CoconutError::DkgError( - DkgError::NotEnoughDealingsAvailable { - available: 0, - required: 1, - }, - ))?; - let sk = SecretKey::create_from_raw(x, scalars); - let vk = sk.verification_key(&BANDWIDTH_CREDENTIAL_PARAMS); - - Ok(CoconutKeyPair::from_keys(sk, vk)) -} +// // 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> { +// // 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(); +// let initial_resharing_dealers = if resharing { +// dkg_client +// .get_initial_dealers() +// .await? +// .map(|d| d.initial_dealers) +// .unwrap_or_default() +// } else { +// vec![] +// }; +// +// let params = dkg::params(); +// +// // 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(|(dealer, dealing)| { +// match Dealing::try_from(&dealing) { +// Ok(dealing) => { +// if dealing +// .verify(params, threshold, &initial_receivers, None) +// .is_err() +// { +// state.mark_bad_dealer( +// &dealer, +// ComplaintReason::DealingVerificationError, +// ); +// None +// } else { +// initial_dealers_by_addr +// .get(&dealer) +// .map(|idx| (*idx, (dealer, dealing))) +// } +// } +// Err(_) => { +// state.mark_bad_dealer(&dealer, ComplaintReason::MalformedDealing); +// None +// } +// } +// })); +// 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) { +// for dealings_map in dealings_maps.iter() { +// if !dealings_map.iter().any(|(_, (address, _))| address == addr) { +// state.mark_bad_dealer(addr, ComplaintReason::MissingDealing); +// break; +// } +// } +// } +// } +// +// Ok(dealings_maps) +// } +// +// fn derive_partial_keypair( +// state: &mut State, +// threshold: Threshold, +// dealings_maps: Vec>, +// ) -> Result { +// let filtered_receivers_by_idx = state.current_dealers_by_idx(); +// let filtered_dealers_by_addr = state.current_dealers_by_addr(); +// let dk = state.dkg_keypair().private_key(); +// let node_index_value = state.receiver_index_value()?; +// let mut scalars = vec![]; +// let mut recovered_vks = vec![]; +// for dealings_map in dealings_maps.into_iter() { +// let (filtered_dealers, filtered_dealings): (Vec<_>, Vec<_>) = dealings_map +// .into_iter() +// .filter_map(|(idx, (addr, dealing))| { +// if filtered_dealers_by_addr.keys().any(|a| addr == *a) { +// Some((idx, dealing)) +// } else { +// None +// } +// }) +// .unzip(); +// debug!( +// "Recovering verification keys from dealings of dealers {:?} with receivers {:?}", +// filtered_dealers, +// filtered_receivers_by_idx.keys().collect::>() +// ); +// let recovered = try_recover_verification_keys( +// &filtered_dealings, +// threshold, +// &filtered_receivers_by_idx, +// )?; +// recovered_vks.push(recovered); +// +// debug!("Decrypting shares"); +// let shares = filtered_dealings +// .iter() +// .map(|dealing| decrypt_share(dk, node_index_value, &dealing.ciphertexts, None)) +// .collect::>()?; +// debug!("Combining shares into one secret"); +// let scalar = combine_shares(shares, &filtered_dealers)?; +// scalars.push(scalar); +// } +// state.set_recovered_vks(recovered_vks); +// +// let x = scalars.pop().ok_or(CoconutError::DkgError( +// DkgError::NotEnoughDealingsAvailable { +// available: 0, +// required: 1, +// }, +// ))?; +// let sk = SecretKey::create_from_raw(x, scalars); +// let vk = sk.verification_key(&BANDWIDTH_CREDENTIAL_PARAMS); +// +// Ok(CoconutKeyPair::from_keys(sk, vk)) +// } impl DkgController { fn verified_dealer_dealings( @@ -380,8 +382,6 @@ impl DkgController { let all_dealers = dealings[&0].keys().copied().collect::>(); - let decryption_key = self.state.dkg_keypair().private_key(); - let mut derived_x = None; let mut derived_secrets = Vec::new(); @@ -397,19 +397,19 @@ impl DkgController { debug!("recovering the partial verification keys"); let recovered = try_recover_verification_keys(&dealings_vec, threshold, &epoch_receivers)?; - // TODO: + + self.state + .key_derivation_state_mut(epoch_id)? + .derived_partials + .insert(dealing_index, recovered); debug!("decrypting received shares"); // for every received share of the key let mut shares = Vec::with_capacity(dealings_vec.len()); for (i, dealing) in dealings_vec.into_iter().enumerate() { // attempt to decrypt our portion - let share = match decrypt_share( - decryption_key, - receiver_index, - &dealing.ciphertexts, - None, - ) { + let dk = self.state.dkg_keypair().private_key(); + let share = match decrypt_share(dk, receiver_index, &dealing.ciphertexts, None) { Ok(share) => share, Err(err) => { let node_index = all_dealers[i]; @@ -582,106 +582,6 @@ impl DkgController { } } -fn validate_proposal(proposal: &ProposalResponse) -> Option<(Addr, u64)> { - if proposal.status == Status::Open { - if let Some(owner) = owner_from_cosmos_msgs(&proposal.msgs) { - return Some((owner, proposal.id)); - } - } - None -} - -pub(crate) async fn verification_key_validation( - dkg_client: &DkgClient, - state: &mut State, - _resharing: bool, -) -> Result<(), CoconutError> { - if state.voted_vks() { - debug!("Already voted on the verification keys, nothing to do"); - return Ok(()); - } - - let epoch_id = dkg_client.get_current_epoch().await?.epoch_id; - let vk_shares = dkg_client.get_verification_key_shares(epoch_id).await?; - let proposal_ids = BTreeMap::from_iter( - dkg_client - .list_proposals() - .await? - .iter() - .filter_map(validate_proposal), - ); - let filtered_receivers_by_idx: Vec<_> = - state.current_dealers_by_idx().keys().copied().collect(); - let recovered_partials: Vec<_> = state - .recovered_vks() - .iter() - .map(|recovered_vk| recovered_vk.recovered_partials.clone()) - .collect(); - let recovered_partials = transpose_matrix(recovered_partials); - let params = &BANDWIDTH_CREDENTIAL_PARAMS; - for contract_share in vk_shares { - if let Some(proposal_id) = proposal_ids.get(&contract_share.owner).copied() { - match VerificationKey::try_from_bs58(contract_share.share) { - Ok(vk) => { - if let Some(idx) = filtered_receivers_by_idx - .iter() - .position(|node_index| contract_share.node_index == *node_index) - { - let ret = if !check_vk_pairing(params, &recovered_partials[idx], &vk) { - debug!( - "Voting NO to proposal {} because of failed VK pairing", - proposal_id - ); - dkg_client - .vote_verification_key_share(proposal_id, false) - .await - } else { - debug!("Voting YES to proposal {}", proposal_id); - dkg_client - .vote_verification_key_share(proposal_id, true) - .await - }; - accepted_vote_err(ret)?; - } - } - Err(_) => { - debug!( - "Voting NO to proposal {} because of failed base 58 deserialization", - proposal_id - ); - let ret = dkg_client - .vote_verification_key_share(proposal_id, false) - .await; - accepted_vote_err(ret)?; - } - } - } - } - state.set_voted_vks(); - info!("DKG: Validated the other verification keys"); - Ok(()) -} - -pub(crate) async fn verification_key_finalization( - dkg_client: &DkgClient, - state: &mut State, - _resharing: bool, -) -> Result<(), CoconutError> { - if state.executed_proposal() { - debug!("Already executed the proposal, nothing to do"); - return Ok(()); - } - - let proposal_id = state.proposal_id_value()?; - dkg_client - .execute_verification_key_share(proposal_id) - .await?; - state.set_executed_proposal(); - info!("DKG: Finalized own verification key on chain"); - - Ok(()) -} - #[cfg(test)] pub(crate) mod tests { use super::*; @@ -1095,11 +995,10 @@ pub(crate) mod tests { submit_public_keys(&mut controllers, false).await; exchange_dealings(&mut controllers, false).await; - let chain = controllers[0].chain_state.clone(); + for controller in controllers.iter_mut() { + let res = controller.verification_key_submission(epoch, false).await; + assert!(res.is_ok()); - derive_keypairs(&mut controllers, false).await; - - for controller in controllers { assert!(controller.state.key_derivation_state(epoch)?.completed); let keys = controller.state.take_coconut_keypair().await; assert!(keys.is_some()); @@ -1107,132 +1006,6 @@ pub(crate) mod tests { } Ok(()) - // let db = MockContractDb::new(); - // let mut clients_and_states = prepare_clients_and_states_with_submission(&db).await; - // - // for controller in clients_and_states.iter_mut() { - // assert!(db - // .proposal_db - // .read() - // .unwrap() - // .contains_key(&controller.state.proposal_id_value().unwrap())); - // assert!(controller.state.coconut_keypair_is_some().await); - // } - } - - #[tokio::test] - #[ignore] // expensive test - async fn validate_verification_key() { - todo!() - // let db = MockContractDb::new(); - // let mut clients_and_states = prepare_clients_and_states_with_validation(&db).await; - // for controller in clients_and_states.iter_mut() { - // let proposal = db - // .proposal_db - // .read() - // .unwrap() - // .get(&controller.state.proposal_id_value().unwrap()) - // .unwrap() - // .clone(); - // assert_eq!(proposal.status, Status::Passed); - // } - } - - #[tokio::test] - #[ignore] // expensive test - async fn validate_verification_key_malformed_share() { - todo!() - // let db = MockContractDb::new(); - // let mut clients_and_states = prepare_clients_and_states_with_submission(&db).await; - // - // db.verification_share_db - // .write() - // .unwrap() - // .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) - // .and_modify(|share| share.share.push('x')); - // - // for controller in clients_and_states.iter_mut() { - // verification_key_validation(&controller.dkg_client, &mut controller.state, false) - // .await - // .unwrap(); - // } - // - // for (idx, controller) in clients_and_states.iter().enumerate() { - // let proposal = db - // .proposal_db - // .read() - // .unwrap() - // .get(&controller.state.proposal_id_value().unwrap()) - // .unwrap() - // .clone(); - // if idx == 0 { - // assert_eq!(proposal.status, Status::Rejected); - // } else { - // assert_eq!(proposal.status, Status::Passed); - // } - // } - } - - #[tokio::test] - #[ignore] // expensive test - async fn validate_verification_key_unpaired_share() { - todo!() - // let db = MockContractDb::new(); - // let mut clients_and_states = prepare_clients_and_states_with_submission(&db).await; - // - // let second_share = db - // .verification_share_db - // .write() - // .unwrap() - // .get(TEST_VALIDATORS_ADDRESS[1]) - // .unwrap() - // .share - // .clone(); - // db.verification_share_db - // .write() - // .unwrap() - // .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) - // .and_modify(|share| share.share = second_share); - // - // for controller in clients_and_states.iter_mut() { - // verification_key_validation(&controller.dkg_client, &mut controller.state, false) - // .await - // .unwrap(); - // } - // - // for (idx, controller) in clients_and_states.iter().enumerate() { - // let proposal = db - // .proposal_db - // .read() - // .unwrap() - // .get(&controller.state.proposal_id_value().unwrap()) - // .unwrap() - // .clone(); - // if idx == 0 { - // assert_eq!(proposal.status, Status::Rejected); - // } else { - // assert_eq!(proposal.status, Status::Passed); - // } - // } - } - - #[tokio::test] - #[ignore] // expensive test - async fn finalize_verification_key() { - todo!() - // let db = MockContractDb::new(); - // let clients_and_states = prepare_clients_and_states_with_finalization(&db).await; - // - // for controller in clients_and_states.iter() { - // let proposal = db - // .proposal_db - // .read() - // .unwrap() - // .get(&controller.state.proposal_id_value().unwrap()) - // .unwrap() - // .clone(); - // assert_eq!(proposal.status, Status::Executed); - // } } #[tokio::test] diff --git a/nym-api/src/coconut/dkg/key_finalization.rs b/nym-api/src/coconut/dkg/key_finalization.rs new file mode 100644 index 0000000000..61de87fbd8 --- /dev/null +++ b/nym-api/src/coconut/dkg/key_finalization.rs @@ -0,0 +1,54 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::coconut::dkg::client::DkgClient; +use crate::coconut::dkg::controller::DkgController; +use crate::coconut::dkg::state::{ConsistentState, State}; +use crate::coconut::error::CoconutError; +use rand::{CryptoRng, RngCore}; + +impl DkgController {} + +pub(crate) async fn verification_key_finalization( + dkg_client: &DkgClient, + state: &mut State, + _resharing: bool, +) -> Result<(), CoconutError> { + if state.executed_proposal() { + log::debug!("Already executed the proposal, nothing to do"); + return Ok(()); + } + + let proposal_id = state.proposal_id_value()?; + dkg_client + .execute_verification_key_share(proposal_id) + .await?; + state.set_executed_proposal(); + info!("DKG: Finalized own verification key on chain"); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + #[ignore] // expensive test + async fn finalize_verification_key() { + todo!() + // let db = MockContractDb::new(); + // let clients_and_states = prepare_clients_and_states_with_finalization(&db).await; + // + // for controller in clients_and_states.iter() { + // let proposal = db + // .proposal_db + // .read() + // .unwrap() + // .get(&controller.state.proposal_id_value().unwrap()) + // .unwrap() + // .clone(); + // assert_eq!(proposal.status, Status::Executed); + // } + } +} diff --git a/nym-api/src/coconut/dkg/key_validation.rs b/nym-api/src/coconut/dkg/key_validation.rs new file mode 100644 index 0000000000..198fb5dba9 --- /dev/null +++ b/nym-api/src/coconut/dkg/key_validation.rs @@ -0,0 +1,393 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::coconut::api_routes::epoch_credentials; +use crate::coconut::dkg::client::DkgClient; +use crate::coconut::dkg::controller::DkgController; +use crate::coconut::dkg::state::State; +use crate::coconut::error::CoconutError; +use crate::coconut::helpers::accepted_vote_err; +use crate::coconut::state::BANDWIDTH_CREDENTIAL_PARAMS; +use cosmwasm_std::Addr; +use cw3::{ProposalResponse, Status}; +use nym_coconut::tests::helpers::transpose_matrix; +use nym_coconut::{check_vk_pairing, Base58, VerificationKey}; +use nym_coconut_dkg_common::types::EpochId; +use nym_coconut_dkg_common::verification_key::{owner_from_cosmos_msgs, ContractVKShare}; +use nym_validator_client::nyxd::AccountId; +use rand::{CryptoRng, RngCore}; +use std::collections::HashMap; + +impl DkgController { + fn filter_proposal( + &self, + dkg_contract: &AccountId, + proposal: &ProposalResponse, + ) -> Option<(Addr, u64)> { + // make sure the proposal we're checking is: + // - still open (not point in voting for anything that has already expired) + // - was proposed by the DKG contract - so that we'd ignore anything from malicious dealers + // - contains valid verification request (checked inside `owner_from_cosmos_msgs`) + if proposal.status == Status::Open && proposal.proposer.as_str() == dkg_contract.as_ref() { + if let Some(owner) = owner_from_cosmos_msgs(&proposal.msgs) { + return Some((owner, proposal.id)); + } + } + None + } + + async fn get_validation_proposals(&self) -> Result, CoconutError> { + let dkg_contract = self.dkg_client.dkg_contract_address().await?; + + // FUTURE OPTIMIZATION: don't query for ALL proposals. say if we're in epoch 50, + // we don't care about expired proposals from epochs 0-49... + // to do it, we'll need to have dkg contract store proposal ids, + // which will require usage of submsgs and replies so that might be a future project + let all_proposals = self.dkg_client.list_proposals().await?; + + let mut deduped_proposals = HashMap::new(); + + // for each proposal, make sure it's a valid validation request; + // if for some reason there exist multiple proposals from the same owner, choose the one + // with the higher id + for proposal in all_proposals { + if let Some((owner, id)) = self.filter_proposal(&dkg_contract, &proposal) { + if let Some(old_id) = deduped_proposals.get(&owner) { + if old_id < &id { + deduped_proposals.insert(owner, id); + } + } else { + deduped_proposals.insert(owner, id); + } + } + } + + // UNHANDLED EDGE CASE: + // since currently proposals are **NOT** tied to epochs, + // we might run into proposals from older epochs we don't have to vote on or might not even have data for + Ok(deduped_proposals) + } + + async fn verify_share( + &self, + epoch_id: EpochId, + share: ContractVKShare, + ) -> Result<(), CoconutError> { + if share.verified { + todo!() + } + + let Some(receiver_index) = self + .state + .valid_epoch_receivers(epoch_id)? + .iter() + .position(|(addr, _)| addr == share.owner) + else { + todo!() + }; + + // EDGE CASE: + // make sure the receiver index of this receiver/dealer is within the size of the derived keys + + // attempt to recover the underlying key from its bs58 representation + let recovered_key = match VerificationKey::try_from_bs58(share.share) { + Ok(key) => key, + Err(err) => { + warn!( + "failed to decode verification share from {}: {err}", + share.owner + ); + todo!() + } + }; + + let Some(self_derived) = self + .state + .key_derivation_state(epoch_id)? + .derived_partials_for(receiver_index) + else { + todo!() + }; + + if !check_vk_pairing(&BANDWIDTH_CREDENTIAL_PARAMS, &self_derived, &recovered_key) { + todo!() + } + + Ok(()) + } + + pub(crate) async fn verification_key_validation( + &mut self, + epoch_id: EpochId, + resharing: bool, + ) -> Result<(), CoconutError> { + let key_validation_state = self.state.key_validation_state(epoch_id)?; + + // check if we have already validated and voted for all keys + if key_validation_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 voted in all validation proposals"); + return Ok(()); + } + + // FAILURE CASE: + // check if we have already verified the keys, but some voting txs either didn't get executed + // or got executed without us knowing about it + if !key_validation_state.votes.is_empty() { + debug!( + "we have already validated all keys for this epoch, but might have failed to vote" + ); + // dkg_client.query_vote to check if our vote is there + todo!() + } + + let proposals = self.get_validation_proposals().await?; + let vk_shares = self + .dkg_client + .get_verification_key_shares(epoch_id) + .await?; + + for contract_share in vk_shares { + // there's no point in checking anything if there doesn't exist an associated multisig proposal + let Some(proposal_id) = proposals.get(&contract_share.owner) else { + warn!( + "there does not seem to exist proposal for vk share from {}", + contract_share.owner + ); + continue; + }; + + let vote = if let Err(err) = self.verify_share(epoch_id, contract_share).await { + todo!(); + false + } else { + true + }; + + self.state + .key_validation_state_mut(epoch_id)? + .votes + .insert(*proposal_id, vote); + } + + // do vote + for (&proposal, &vote) in &self.state.key_validation_state(epoch_id)?.votes { + // FUTURE OPTIMIZATION: we could batch them in a single tx + self.dkg_client + .vote_verification_key_share(proposal, vote) + .await?; + } + + self.state.key_validation_state_mut(epoch_id)?.completed = true; + + // if self.state.voted_vks() { + // log::debug!("Already voted on the verification keys, nothing to do"); + // return Ok(()); + // } + // + // let epoch_id = self.dkg_client.get_current_epoch().await?.epoch_id; + // let vk_shares = self + // .dkg_client + // .get_verification_key_shares(epoch_id) + // .await?; + // let proposal_ids = BTreeMap::from_iter( + // self.dkg_client + // .list_proposals() + // .await? + // .iter() + // .filter_map(validate_proposal), + // ); + // let filtered_receivers_by_idx: Vec<_> = self + // .state + // .current_dealers_by_idx() + // .keys() + // .copied() + // .collect(); + // let recovered_partials: Vec<_> = self + // .state + // .recovered_vks() + // .iter() + // .map(|recovered_vk| recovered_vk.recovered_partials.clone()) + // .collect(); + // let recovered_partials = transpose_matrix(recovered_partials); + // let params = &BANDWIDTH_CREDENTIAL_PARAMS; + // for contract_share in vk_shares { + // if let Some(proposal_id) = proposal_ids.get(&contract_share.owner).copied() { + // match VerificationKey::try_from_bs58(contract_share.share) { + // Ok(vk) => { + // if let Some(idx) = filtered_receivers_by_idx + // .iter() + // .position(|node_index| contract_share.node_index == *node_index) + // { + // let ret = if !check_vk_pairing(params, &recovered_partials[idx], &vk) { + // log::debug!( + // "Voting NO to proposal {} because of failed VK pairing", + // proposal_id + // ); + // self.dkg_client + // .vote_verification_key_share(proposal_id, false) + // .await + // } else { + // log::debug!("Voting YES to proposal {}", proposal_id); + // self.dkg_client + // .vote_verification_key_share(proposal_id, true) + // .await + // }; + // accepted_vote_err(ret)?; + // } + // } + // Err(_) => { + // log::debug!( + // "Voting NO to proposal {} because of failed base 58 deserialization", + // proposal_id + // ); + // let ret = self + // .dkg_client + // .vote_verification_key_share(proposal_id, false) + // .await; + // accepted_vote_err(ret)?; + // } + // } + // } + // } + // self.state.set_voted_vks(); + + info!("DKG: validated all the other verification keys"); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::coconut::tests::helpers::{ + derive_keypairs, exchange_dealings, initialise_controllers, initialise_dkg, + submit_public_keys, + }; + + #[tokio::test] + #[ignore] // expensive test + async fn validate_verification_key() -> anyhow::Result<()> { + let unused = 42; + let validators = 1; + + let mut controllers = initialise_controllers(validators); + let chain = controllers[0].chain_state.clone(); + let epoch = chain.lock().unwrap().dkg_epoch.epoch_id; + + initialise_dkg(&mut controllers, false); + submit_public_keys(&mut controllers, false).await; + exchange_dealings(&mut controllers, false).await; + derive_keypairs(&mut controllers, false).await; + + for controller in controllers.iter_mut() { + let res = controller.verification_key_validation(epoch, false).await; + assert!(res.is_ok()); + + assert!(controller.state.key_validation_state(epoch)?.completed); + } + + let chain = controllers[0].chain_state.clone(); + let guard = chain.lock().unwrap(); + let proposals = &guard.proposals; + assert_eq!(proposals.len(), validators); + + for proposal in proposals.values() { + assert_eq!(Status::Passed, proposal.status) + } + + Ok(()) + + // let db = MockContractDb::new(); + // let mut clients_and_states = prepare_clients_and_states_with_validation(&db).await; + // for controller in clients_and_states.iter_mut() { + // let proposal = db + // .proposal_db + // .read() + // .unwrap() + // .get(&controller.state.proposal_id_value().unwrap()) + // .unwrap() + // .clone(); + // assert_eq!(proposal.status, Status::Passed); + // } + } + + #[tokio::test] + #[ignore] // expensive test + async fn validate_verification_key_malformed_share() { + todo!() + // let db = MockContractDb::new(); + // let mut clients_and_states = prepare_clients_and_states_with_submission(&db).await; + // + // db.verification_share_db + // .write() + // .unwrap() + // .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + // .and_modify(|share| share.share.push('x')); + // + // for controller in clients_and_states.iter_mut() { + // verification_key_validation(&controller.dkg_client, &mut controller.state, false) + // .await + // .unwrap(); + // } + // + // for (idx, controller) in clients_and_states.iter().enumerate() { + // let proposal = db + // .proposal_db + // .read() + // .unwrap() + // .get(&controller.state.proposal_id_value().unwrap()) + // .unwrap() + // .clone(); + // if idx == 0 { + // assert_eq!(proposal.status, Status::Rejected); + // } else { + // assert_eq!(proposal.status, Status::Passed); + // } + // } + } + + #[tokio::test] + #[ignore] // expensive test + async fn validate_verification_key_unpaired_share() { + todo!() + // let db = MockContractDb::new(); + // let mut clients_and_states = prepare_clients_and_states_with_submission(&db).await; + // + // let second_share = db + // .verification_share_db + // .write() + // .unwrap() + // .get(TEST_VALIDATORS_ADDRESS[1]) + // .unwrap() + // .share + // .clone(); + // db.verification_share_db + // .write() + // .unwrap() + // .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + // .and_modify(|share| share.share = second_share); + // + // for controller in clients_and_states.iter_mut() { + // verification_key_validation(&controller.dkg_client, &mut controller.state, false) + // .await + // .unwrap(); + // } + // + // for (idx, controller) in clients_and_states.iter().enumerate() { + // let proposal = db + // .proposal_db + // .read() + // .unwrap() + // .get(&controller.state.proposal_id_value().unwrap()) + // .unwrap() + // .clone(); + // if idx == 0 { + // assert_eq!(proposal.status, Status::Rejected); + // } else { + // assert_eq!(proposal.status, Status::Passed); + // } + // } + } +} diff --git a/nym-api/src/coconut/dkg/mod.rs b/nym-api/src/coconut/dkg/mod.rs index ed0509cbdd..ad4ab16290 100644 --- a/nym-api/src/coconut/dkg/mod.rs +++ b/nym-api/src/coconut/dkg/mod.rs @@ -13,5 +13,7 @@ pub(crate) mod complaints; pub(crate) mod controller; pub(crate) mod dealing; pub(crate) mod key_derivation; +mod key_finalization; +mod key_validation; pub(crate) mod public_key; pub(crate) mod state; diff --git a/nym-api/src/coconut/dkg/state/dealing_exchange.rs b/nym-api/src/coconut/dkg/state/dealing_exchange.rs index 97aa74751e..ef467a0afd 100644 --- a/nym-api/src/coconut/dkg/state/dealing_exchange.rs +++ b/nym-api/src/coconut/dkg/state/dealing_exchange.rs @@ -5,11 +5,13 @@ 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; +use std::collections::{BTreeMap, HashMap}; +use crate::coconut::dkg::state::DkgParticipant; #[derive(Debug, Default, Serialize, Deserialize)] pub struct DealingExchangeState { - // pub(crate) assigned_index: Option, + pub(crate) dealers: BTreeMap, + #[serde(with = "generated_dealings")] pub(crate) generated_dealings: HashMap, diff --git a/nym-api/src/coconut/dkg/state/key_derivation.rs b/nym-api/src/coconut/dkg/state/key_derivation.rs index 05dd3cc774..3af4984668 100644 --- a/nym-api/src/coconut/dkg/state/key_derivation.rs +++ b/nym-api/src/coconut/dkg/state/key_derivation.rs @@ -1,12 +1,20 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_dkg::Threshold; +use super::serde_helpers::recovered_keys; +use nym_coconut_dkg_common::types::DealingIndex; +use nym_dkg::{G2Projective, RecoveredVerificationKeys, Threshold}; use serde_derive::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; + +type ReceiverIndex = usize; #[derive(Debug, Default, Serialize, Deserialize)] pub struct KeyDerivationState { - pub(crate) expected_threshold: Option, // pub(crate) completed: bool, + pub(crate) expected_threshold: Option, + + #[serde(with = "recovered_keys")] + pub(crate) derived_partials: BTreeMap, pub(crate) completed: bool, // because we couldn't decrypt shares or there were no shares, etc @@ -14,6 +22,19 @@ pub struct KeyDerivationState { } impl KeyDerivationState { + pub fn derived_partials_for(&self, receiver_index: ReceiverIndex) -> Option> { + let mut recovered = Vec::new(); + for keys in self.derived_partials.values() { + // SAFETY: + // make sure the receiver index of this receiver/dealer is within the size of the derived keys + if keys.recovered_partials.len() <= receiver_index { + return None; + }; + recovered.push(keys.recovered_partials[receiver_index]) + } + Some(recovered) + } + pub fn completed(&self) -> bool { self.completed } diff --git a/nym-api/src/coconut/dkg/state/key_finalization.rs b/nym-api/src/coconut/dkg/state/key_finalization.rs new file mode 100644 index 0000000000..c5f7c939e3 --- /dev/null +++ b/nym-api/src/coconut/dkg/state/key_finalization.rs @@ -0,0 +1,14 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use serde_derive::{Deserialize, Serialize}; + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct FinalizationState {} + +impl FinalizationState { + /// Specifies whether this dealer has already registered in the particular DKG epoch + pub fn completed(&self) -> bool { + todo!() + } +} diff --git a/nym-api/src/coconut/dkg/state/key_validation.rs b/nym-api/src/coconut/dkg/state/key_validation.rs new file mode 100644 index 0000000000..68877c3f0e --- /dev/null +++ b/nym-api/src/coconut/dkg/state/key_validation.rs @@ -0,0 +1,21 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use serde_derive::{Deserialize, Serialize}; +use std::collections::HashMap; + +type ProposalId = u64; + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct ValidationState { + pub votes: HashMap, + + pub completed: bool, +} + +impl ValidationState { + /// Specifies whether this dealer has already registered in the particular 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 3161f542ee..240b27cb08 100644 --- a/nym-api/src/coconut/dkg/state/mod.rs +++ b/nym-api/src/coconut/dkg/state/mod.rs @@ -26,9 +26,13 @@ use std::path::{Path, PathBuf}; use time::OffsetDateTime; use tokio::sync::RwLockReadGuard; use url::Url; +use crate::coconut::dkg::state::key_finalization::FinalizationState; +use crate::coconut::dkg::state::key_validation::ValidationState; mod dealing_exchange; mod key_derivation; +mod key_finalization; +mod key_validation; mod registration; mod serde_helpers; @@ -242,18 +246,19 @@ 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, pub(crate) key_generation: KeyDerivationState, + + pub(crate) key_validation: ValidationState, + + pub(crate) key_finalization: FinalizationState, } impl DkgState { pub(crate) fn set_dealers(&mut self, raw_dealers: Vec) { - assert!(self.dealers.is_empty()); + assert!(self.dealing_exchange.dealers.is_empty()); for raw_dealer in raw_dealers { let dkg_participant = DkgParticipant::from(raw_dealer); if let ParticipantState::Invalid(complaint) = &dkg_participant.state { @@ -262,7 +267,7 @@ impl DkgState { dkg_participant.address ) } - self.dealers + self.dealing_exchange.dealers .insert(dkg_participant.assigned_index, dkg_participant); } } @@ -365,7 +370,7 @@ impl State { epoch_id: EpochId, ) -> Result, CoconutError> { Ok(self - .dkg_state(epoch_id)? + .dealing_exchange_state(epoch_id)? .dealers .values() .filter_map(|d| { @@ -383,7 +388,7 @@ impl State { epoch_id: EpochId, ) -> Result, CoconutError> { Ok(self - .dkg_state(epoch_id)? + .dealing_exchange_state(epoch_id)? .dealers .values() .filter_map(|d| d.state.public_key().map(|k| (d.assigned_index, k))) @@ -462,6 +467,46 @@ impl State { .ok_or(CoconutError::MissingDkgState { epoch_id }) } + pub fn key_validation_state( + &self, + epoch_id: EpochId, + ) -> Result<&ValidationState, CoconutError> { + self.dkg_instances + .get(&epoch_id) + .map(|state| &state.key_validation) + .ok_or(CoconutError::MissingDkgState { epoch_id }) + } + + pub fn key_validation_state_mut( + &mut self, + epoch_id: EpochId, + ) -> Result<&mut ValidationState, CoconutError> { + self.dkg_instances + .get_mut(&epoch_id) + .map(|state| &mut state.key_validation) + .ok_or(CoconutError::MissingDkgState { epoch_id }) + } + + pub fn key_finalization_state( + &self, + epoch_id: EpochId, + ) -> Result<&FinalizationState, CoconutError> { + self.dkg_instances + .get(&epoch_id) + .map(|state| &state.key_finalization) + .ok_or(CoconutError::MissingDkgState { epoch_id }) + } + + pub fn key_finalization_state_mut( + &mut self, + epoch_id: EpochId, + ) -> Result<&mut FinalizationState, CoconutError> { + self.dkg_instances + .get_mut(&epoch_id) + .map(|state| &mut state.key_finalization) + .ok_or(CoconutError::MissingDkgState { epoch_id }) + } + pub fn threshold(&self, epoch_id: EpochId) -> Result { self.key_derivation_state(epoch_id)? .expected_threshold diff --git a/nym-api/src/coconut/dkg/state/serde_helpers.rs b/nym-api/src/coconut/dkg/state/serde_helpers.rs index 8db9a3a172..8980b47668 100644 --- a/nym-api/src/coconut/dkg/state/serde_helpers.rs +++ b/nym-api/src/coconut/dkg/state/serde_helpers.rs @@ -50,6 +50,44 @@ pub(super) mod vks_serde { } } +pub(super) mod recovered_keys { + use nym_coconut_dkg_common::types::DealingIndex; + use nym_dkg::RecoveredVerificationKeys; + use serde::de::Error; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + use std::collections::{BTreeMap, HashMap}; + + type Helper = BTreeMap>; + + pub fn serialize( + val: &BTreeMap, + serializer: S, + ) -> Result { + let helper: Helper = val + .iter() + .map(|(idx, rec)| (*idx, rec.to_bytes())) + .collect(); + helper.serialize(serializer) + } + + pub fn deserialize<'de, D>( + deserializer: D, + ) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let helper = Helper::deserialize(deserializer)?; + helper + .into_iter() + .map(|(idx, rec)| { + RecoveredVerificationKeys::try_from_bytes(&rec) + .map_err(|err| D::Error::custom(format_args!("{:?}", err))) + .map(|vk| (idx, vk)) + }) + .collect() + } +} + pub(super) mod generated_dealings { use nym_coconut_dkg_common::types::DealingIndex; use nym_dkg::Dealing; diff --git a/nym-api/src/coconut/tests/helpers.rs b/nym-api/src/coconut/tests/helpers.rs index df0eae8f90..6dfb902724 100644 --- a/nym-api/src/coconut/tests/helpers.rs +++ b/nym-api/src/coconut/tests/helpers.rs @@ -2,9 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::coconut::dkg::controller::DkgController; -use crate::coconut::dkg::key_derivation::{ - verification_key_finalization, verification_key_validation, -}; use crate::coconut::tests::fixtures::{TestingDkgController, TestingDkgControllerBuilder}; use crate::coconut::tests::FakeChainState; use nym_coconut_dkg_common::types::EpochState; @@ -110,23 +107,40 @@ pub(crate) async fn derive_keypairs(controllers: &mut [TestingDkgController], re } pub(crate) async fn validate_keys(controllers: &mut [TestingDkgController], resharing: bool) { - todo!() - // let mut clients_and_states = derive_keypairs(db).await; - // for controller in clients_and_states.iter_mut() { - // verification_key_validation(&controller.dkg_client, &mut controller.state, false) - // .await - // .unwrap(); - // } - // clients_and_states + let epoch = controllers[0] + .chain_state + .lock() + .unwrap() + .dkg_epoch + .epoch_id; + + for controller in controllers.iter_mut() { + controller + .verification_key_validation(epoch, resharing) + .await + .unwrap(); + } + + let mut guard = controllers[0].chain_state.lock().unwrap(); + guard.dkg_epoch.state = EpochState::VerificationKeyFinalization { resharing } } pub(crate) async fn finalize(controllers: &mut [TestingDkgController], resharing: bool) { - todo!() - // let mut clients_and_states = validate_keys(db).await; - // for controller in clients_and_states.iter_mut() { - // verification_key_finalization(&controller.dkg_client, &mut controller.state, false) - // .await - // .unwrap(); - // } - // clients_and_states + let epoch = controllers[0] + .chain_state + .lock() + .unwrap() + .dkg_epoch + .epoch_id; + + for controller in controllers.iter_mut() { + todo!() + // controller + // .verification_key_finalization(epoch, resharing) + // .await + // .unwrap(); + } + + let mut guard = controllers[0].chain_state.lock().unwrap(); + guard.dkg_epoch.state = EpochState::InProgress {} } diff --git a/nym-api/src/coconut/tests/mod.rs b/nym-api/src/coconut/tests/mod.rs index cf5b8a5284..a9a38f7b35 100644 --- a/nym-api/src/coconut/tests/mod.rs +++ b/nym-api/src/coconut/tests/mod.rs @@ -64,6 +64,8 @@ const TEST_REWARDING_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8g #[derive(Debug)] pub(crate) struct FakeChainState { + pub(crate) dkg_contract: AccountId, + pub(crate) dealers: HashMap, pub(crate) past_dealers: HashMap, @@ -90,6 +92,7 @@ pub(crate) struct FakeChainState { impl Default for FakeChainState { fn default() -> Self { FakeChainState { + dkg_contract: AccountId::new("n", &[69u8; 32]).unwrap(), dealers: HashMap::new(), past_dealers: Default::default(), @@ -248,6 +251,10 @@ impl super::client::Client for DummyClient { self.validator_address.clone() } + async fn dkg_contract_address(&self) -> Result { + Ok(self.state.lock().unwrap().dkg_contract.clone()) + } + async fn get_tx(&self, tx_hash: Hash) -> Result { todo!() // Ok(self @@ -274,15 +281,14 @@ impl super::client::Client for DummyClient { } async fn list_proposals(&self) -> Result> { - todo!() - // Ok(self - // .state - // .lock() - // .unwrap() - // .proposals - // .values() - // .cloned() - // .collect()) + Ok(self + .state + .lock() + .unwrap() + .proposals + .values() + .cloned() + .collect()) } async fn get_spent_credential( @@ -427,16 +433,15 @@ impl super::client::Client for DummyClient { vote_yes: bool, _fee: Option, ) -> Result<()> { - todo!() - // if let Some(proposal) = self.state.lock().unwrap().proposals.get_mut(&proposal_id) { - // // for now, just suppose that every vote is honest - // if !vote_yes { - // proposal.status = cw3::Status::Rejected; - // } else if vote_yes && proposal.status == cw3::Status::Open { - // proposal.status = cw3::Status::Passed; - // } - // } - // Ok(()) + if let Some(proposal) = self.state.lock().unwrap().proposals.get_mut(&proposal_id) { + // for now, just suppose that every vote is honest + if !vote_yes { + proposal.status = cw3::Status::Rejected; + } else if vote_yes && proposal.status == cw3::Status::Open { + proposal.status = cw3::Status::Passed; + } + } + Ok(()) } async fn execute_proposal(&self, proposal_id: u64) -> Result<()> { @@ -541,6 +546,7 @@ impl super::client::Client for DummyClient { resharing: bool, ) -> Result { let address = self.validator_address.to_string(); + let dkg_contract = self.state.lock().unwrap().dkg_contract.clone(); let Some(dealer_details) = self.get_dealer_by_address(&address).await else { // Just throw some error, not really the correct one @@ -588,7 +594,7 @@ impl super::client::Client for DummyClient { percentage: Decimal::from_ratio(2u32, 3u32), total_weight: 100, }, - proposer: Addr::unchecked(self.validator_address.as_ref()), + proposer: Addr::unchecked(dkg_contract.as_ref()), deposit: None, }; guard.proposals.insert(proposal_id, proposal); diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index b4779c6d2b..d1a43d87ce 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -346,6 +346,15 @@ impl crate::coconut::client::Client for Client { self.client_address().await } + async fn dkg_contract_address(&self) -> Result { + nyxd_query!( + self, + dkg_contract_address() + .cloned() + .ok_or_else(|| NyxdError::unavailable_contract_address("dkg contract").into()) + ) + } + async fn get_tx(&self, tx_hash: Hash) -> crate::coconut::error::Result { nyxd_query!(self, get_tx(tx_hash).await).map_err(|source| { CoconutError::TxRetrievalFailure {