From d89081d8a12c35db4f0481f2279a13fa85e3d274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Tue, 7 Feb 2023 12:56:08 +0200 Subject: [PATCH] Feature/dkg reshare (#2936) * Add resharing parameter * Fix equality of dealers and members * Contract resharing handling * Dealer verification unit test * Dealing commit unit test * Epoch state unit tests * Fix clippy * Fmt * Query initial dealer data * Resharing nym-api changes * Implement the mockups for nym-api dkg tests * Dealing test * Vk unit test * Fix skipping vk submission * Fix clippy * Missing dealing for noninitial resharing dealer * Check master vk holds after resharing on nym-apis * Update changelog * Fix clippy --- CHANGELOG.md | 2 + .../src/nyxd/traits/dkg_query_client.rs | 11 +- .../src/nyxd/traits/dkg_signing_client.rs | 14 +- .../coconut-dkg/src/msg.rs | 5 + .../coconut-dkg/src/types.rs | 66 ++- .../coconut-dkg/src/verification_key.rs | 6 +- common/nymcoconut/src/scheme/keygen.rs | 4 + contracts/coconut-dkg/src/contract.rs | 26 +- .../coconut-dkg/src/dealers/transactions.rs | 77 ++- .../coconut-dkg/src/dealings/transactions.rs | 62 +- .../coconut-dkg/src/epoch_state/queries.rs | 15 +- .../coconut-dkg/src/epoch_state/storage.rs | 4 +- .../src/epoch_state/transactions.rs | 554 +++++++++++++----- contracts/coconut-dkg/src/error.rs | 3 + .../coconut-dkg/src/support/tests/helpers.rs | 18 +- .../verification_key_shares/transactions.rs | 69 ++- .../src/submit_vk_creates_proposal.rs | 2 + nym-api/src/coconut/client.rs | 13 +- nym-api/src/coconut/dkg/client.rs | 21 +- nym-api/src/coconut/dkg/controller.rs | 27 +- nym-api/src/coconut/dkg/dealing.rs | 148 ++++- nym-api/src/coconut/dkg/public_key.rs | 11 +- nym-api/src/coconut/dkg/state.rs | 32 +- nym-api/src/coconut/dkg/verification_key.rs | 287 +++++++-- nym-api/src/coconut/error.rs | 3 + nym-api/src/coconut/tests.rs | 74 ++- nym-api/src/support/nyxd/mod.rs | 16 +- 27 files changed, 1236 insertions(+), 334 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eaa9db428..b108c9437e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,11 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - remove coconut feature and unify builds ([#2890]) - native-client: is now capable of listening for requests on sockets different than `127.0.0.1` ([#2939]). This can be specified via `--host` flag during `init` or `run`. Alternatively a custom `host` can be set in `config.toml` file under `socket` section. +- dkg resharing mode ([#2936]) [#2890]: https://github.com/nymtech/nym/pull/2890 [#2939]: https://github.com/nymtech/nym/pull/2939 +[#2936]: https://github.com/nymtech/nym/pull/2936 # [v1.1.8] (2023-01-31) diff --git a/common/client-libs/validator-client/src/nyxd/traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nyxd/traits/dkg_query_client.rs index 3b405caf9e..296f8913eb 100644 --- a/common/client-libs/validator-client/src/nyxd/traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/traits/dkg_query_client.rs @@ -8,7 +8,7 @@ use coconut_dkg_common::dealer::{ DealerDetailsResponse, PagedDealerResponse, PagedDealingsResponse, }; use coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; -use coconut_dkg_common::types::{Epoch, EpochId}; +use coconut_dkg_common::types::{Epoch, EpochId, InitialReplacementData}; use coconut_dkg_common::verification_key::PagedVKSharesResponse; use cosmrs::AccountId; @@ -16,6 +16,7 @@ use cosmrs::AccountId; pub trait DkgQueryClient { async fn get_current_epoch(&self) -> Result; async fn get_current_epoch_threshold(&self) -> Result, NyxdError>; + async fn get_initial_dealers(&self) -> Result, NyxdError>; async fn get_dealer_details( &self, address: &AccountId, @@ -62,6 +63,14 @@ where .query_contract_smart(self.coconut_dkg_contract_address(), &request) .await } + + async fn get_initial_dealers(&self) -> Result, NyxdError> { + let request = DkgQueryMsg::GetInitialDealers {}; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } + async fn get_dealer_details( &self, address: &AccountId, diff --git a/common/client-libs/validator-client/src/nyxd/traits/dkg_signing_client.rs b/common/client-libs/validator-client/src/nyxd/traits/dkg_signing_client.rs index a025a84db0..90df5b5db7 100644 --- a/common/client-libs/validator-client/src/nyxd/traits/dkg_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/traits/dkg_signing_client.rs @@ -17,18 +17,21 @@ pub trait DkgSigningClient { &self, bte_key: EncodedBTEPublicKeyWithProof, announce_address: String, + resharing: bool, fee: Option, ) -> Result; async fn submit_dealing_bytes( &self, commitment: ContractSafeBytes, + resharing: bool, fee: Option, ) -> Result; async fn submit_verification_key_share( &self, share: VerificationKeyShare, + resharing: bool, fee: Option, ) -> Result; } @@ -57,11 +60,13 @@ where &self, bte_key: EncodedBTEPublicKeyWithProof, announce_address: String, + resharing: bool, fee: Option, ) -> Result { let req = DkgExecuteMsg::RegisterDealer { bte_key_with_proof: bte_key, announce_address, + resharing, }; self.client @@ -79,9 +84,13 @@ where async fn submit_dealing_bytes( &self, dealing_bytes: ContractSafeBytes, + resharing: bool, fee: Option, ) -> Result { - let req = DkgExecuteMsg::CommitDealing { dealing_bytes }; + let req = DkgExecuteMsg::CommitDealing { + dealing_bytes, + resharing, + }; self.client .execute( @@ -98,9 +107,10 @@ where async fn submit_verification_key_share( &self, share: VerificationKeyShare, + resharing: bool, fee: Option, ) -> Result { - let req = DkgExecuteMsg::CommitVerificationKeyShare { share }; + let req = DkgExecuteMsg::CommitVerificationKeyShare { share, resharing }; self.client .execute( diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs index d3b59c9f1b..62891e8fe7 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -21,18 +21,22 @@ pub enum ExecuteMsg { RegisterDealer { bte_key_with_proof: EncodedBTEPublicKeyWithProof, announce_address: String, + resharing: bool, }, CommitDealing { dealing_bytes: ContractSafeBytes, + resharing: bool, }, CommitVerificationKeyShare { share: VerificationKeyShare, + resharing: bool, }, VerifyVerificationKeyShare { owner: Addr, + resharing: bool, }, SurpassedThreshold {}, @@ -45,6 +49,7 @@ pub enum ExecuteMsg { pub enum QueryMsg { GetCurrentEpochState {}, GetCurrentEpochThreshold {}, + GetInitialDealers {}, GetDealerDetails { dealer_address: String, }, diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs index 170a8d83f7..7db0f64ffe 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs @@ -18,6 +18,12 @@ pub type EpochId = u64; // 2 public attributes, 2 private attributes, 1 fixed for coconut credential pub const TOTAL_DEALINGS: usize = 2 + 2 + 1; +#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq, Ord, PartialOrd)] +pub struct InitialReplacementData { + pub initial_dealers: Vec, + pub initial_height: Option, +} + #[derive( Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, JsonSchema, )] @@ -86,15 +92,17 @@ impl Epoch { current_timestamp: Timestamp, ) -> Self { let duration = match state { - EpochState::PublicKeySubmission => time_configuration.public_key_submission_time_secs, - EpochState::DealingExchange => time_configuration.dealing_exchange_time_secs, - EpochState::VerificationKeySubmission => { + EpochState::PublicKeySubmission { .. } => { + time_configuration.public_key_submission_time_secs + } + EpochState::DealingExchange { .. } => time_configuration.dealing_exchange_time_secs, + EpochState::VerificationKeySubmission { .. } => { time_configuration.verification_key_submission_time_secs } - EpochState::VerificationKeyValidation => { + EpochState::VerificationKeyValidation { .. } => { time_configuration.verification_key_validation_time_secs } - EpochState::VerificationKeyFinalization => { + EpochState::VerificationKeyFinalization { .. } => { time_configuration.verification_key_finalization_time_secs } EpochState::InProgress => time_configuration.in_progress_time_secs, @@ -123,28 +131,36 @@ impl Epoch { #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)] #[serde(rename_all = "snake_case")] pub enum EpochState { - PublicKeySubmission, - DealingExchange, - VerificationKeySubmission, - VerificationKeyValidation, - VerificationKeyFinalization, + PublicKeySubmission { resharing: bool }, + DealingExchange { resharing: bool }, + VerificationKeySubmission { resharing: bool }, + VerificationKeyValidation { resharing: bool }, + VerificationKeyFinalization { resharing: bool }, InProgress, } impl Default for EpochState { fn default() -> Self { - Self::PublicKeySubmission + Self::PublicKeySubmission { resharing: false } } } impl Display for EpochState { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - EpochState::PublicKeySubmission => write!(f, "PublicKeySubmission"), - EpochState::DealingExchange => write!(f, "DealingExchange"), - EpochState::VerificationKeySubmission => write!(f, "VerificationKeySubmission"), - EpochState::VerificationKeyValidation => write!(f, "VerificationKeyValidation"), - EpochState::VerificationKeyFinalization => write!(f, "VerificationKeyFinalization"), + EpochState::PublicKeySubmission { resharing } => { + write!(f, "PublicKeySubmission with resharing {resharing}") + } + EpochState::DealingExchange { resharing } => write!(f, "DealingExchange {resharing}"), + EpochState::VerificationKeySubmission { resharing } => { + write!(f, "VerificationKeySubmission with resharing {resharing}") + } + EpochState::VerificationKeyValidation { resharing } => { + write!(f, "VerificationKeyValidation with resharing {resharing}") + } + EpochState::VerificationKeyFinalization { resharing } => { + write!(f, "VerificationKeyFinalization with resharing {resharing}") + } EpochState::InProgress => write!(f, "InProgress"), } } @@ -153,11 +169,19 @@ impl Display for EpochState { impl EpochState { pub fn next(self) -> Option { match self { - EpochState::PublicKeySubmission => Some(EpochState::DealingExchange), - EpochState::DealingExchange => Some(EpochState::VerificationKeySubmission), - EpochState::VerificationKeySubmission => Some(EpochState::VerificationKeyValidation), - EpochState::VerificationKeyValidation => Some(EpochState::VerificationKeyFinalization), - EpochState::VerificationKeyFinalization => Some(EpochState::InProgress), + EpochState::PublicKeySubmission { resharing } => { + Some(EpochState::DealingExchange { resharing }) + } + EpochState::DealingExchange { resharing } => { + Some(EpochState::VerificationKeySubmission { resharing }) + } + EpochState::VerificationKeySubmission { resharing } => { + Some(EpochState::VerificationKeyValidation { resharing }) + } + EpochState::VerificationKeyValidation { resharing } => { + Some(EpochState::VerificationKeyFinalization { resharing }) + } + EpochState::VerificationKeyFinalization { .. } => Some(EpochState::InProgress), EpochState::InProgress => None, } } 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 2dcd222b91..90b66acde7 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs @@ -30,11 +30,12 @@ pub struct PagedVKSharesResponse { pub fn to_cosmos_msg( owner: Addr, + resharing: bool, coconut_dkg_addr: String, multisig_addr: String, expiration_time: Timestamp, ) -> StdResult { - let verify_vk_share_req = ExecuteMsg::VerifyVerificationKeyShare { owner }; + let verify_vk_share_req = ExecuteMsg::VerifyVerificationKeyShare { owner, resharing }; let verify_vk_share_msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: coconut_dkg_addr, msg: to_binary(&verify_vk_share_req)?, @@ -62,7 +63,8 @@ pub fn owner_from_cosmos_msgs(msgs: &[CosmosMsg]) -> Option { funds: _, })) = msgs.get(0) { - if let Ok(ExecuteMsg::VerifyVerificationKeyShare { owner }) = from_binary::(msg) + if let Ok(ExecuteMsg::VerifyVerificationKeyShare { owner, .. }) = + from_binary::(msg) { return Some(owner); } diff --git a/common/nymcoconut/src/scheme/keygen.rs b/common/nymcoconut/src/scheme/keygen.rs index d80779381a..ee694b6c48 100644 --- a/common/nymcoconut/src/scheme/keygen.rs +++ b/common/nymcoconut/src/scheme/keygen.rs @@ -93,6 +93,10 @@ impl SecretKey { Self { x, ys } } + pub fn into_raw(&self) -> (Scalar, Vec) { + (self.x, self.ys.clone()) + } + /// Derive verification key using this secret key. pub fn verification_key(&self, params: &Parameters) -> VerificationKey { let g1 = params.gen1(); diff --git a/contracts/coconut-dkg/src/contract.rs b/contracts/coconut-dkg/src/contract.rs index c2d16d51c6..fb0138b47f 100644 --- a/contracts/coconut-dkg/src/contract.rs +++ b/contracts/coconut-dkg/src/contract.rs @@ -7,7 +7,9 @@ use crate::dealers::queries::{ use crate::dealers::transactions::try_add_dealer; use crate::dealings::queries::query_dealings_paged; use crate::dealings::transactions::try_commit_dealings; -use crate::epoch_state::queries::{query_current_epoch, query_current_epoch_threshold}; +use crate::epoch_state::queries::{ + query_current_epoch, query_current_epoch_threshold, query_initial_dealers, +}; use crate::epoch_state::storage::CURRENT_EPOCH; use crate::epoch_state::transactions::{advance_epoch_state, try_surpassed_threshold}; use crate::error::ContractError; @@ -75,15 +77,17 @@ pub fn execute( ExecuteMsg::RegisterDealer { bte_key_with_proof, announce_address, - } => try_add_dealer(deps, info, bte_key_with_proof, announce_address), - ExecuteMsg::CommitDealing { dealing_bytes } => { - try_commit_dealings(deps, info, dealing_bytes) + resharing, + } => try_add_dealer(deps, info, bte_key_with_proof, announce_address, resharing), + ExecuteMsg::CommitDealing { + dealing_bytes, + resharing, + } => try_commit_dealings(deps, info, dealing_bytes, resharing), + ExecuteMsg::CommitVerificationKeyShare { share, resharing } => { + try_commit_verification_key_share(deps, env, info, share, resharing) } - ExecuteMsg::CommitVerificationKeyShare { share } => { - try_commit_verification_key_share(deps, env, info, share) - } - ExecuteMsg::VerifyVerificationKeyShare { owner } => { - try_verify_verification_key_share(deps, info, owner) + ExecuteMsg::VerifyVerificationKeyShare { owner, resharing } => { + try_verify_verification_key_share(deps, info, owner, resharing) } ExecuteMsg::SurpassedThreshold {} => try_surpassed_threshold(deps, env), ExecuteMsg::AdvanceEpochState {} => advance_epoch_state(deps, env), @@ -97,6 +101,7 @@ pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result { to_binary(&query_current_epoch_threshold(deps.storage)?)? } + QueryMsg::GetInitialDealers {} => to_binary(&query_initial_dealers(deps.storage)?)?, QueryMsg::GetDealerDetails { dealer_address } => { to_binary(&query_dealer_details(deps, dealer_address)?)? } @@ -238,6 +243,7 @@ mod tests { &RegisterDealer { bte_key_with_proof: "bte_key_with_proof".to_string(), announce_address: "127.0.0.1:8000".to_string(), + resharing: false, }, &vec![], ) @@ -251,6 +257,7 @@ mod tests { &RegisterDealer { bte_key_with_proof: "bte_key_with_proof".to_string(), announce_address: "127.0.0.1:8000".to_string(), + resharing: false, }, &vec![], ) @@ -266,6 +273,7 @@ mod tests { &RegisterDealer { bte_key_with_proof: "bte_key_with_proof".to_string(), announce_address: "127.0.0.1:8000".to_string(), + resharing: false, }, &vec![], ) diff --git a/contracts/coconut-dkg/src/dealers/transactions.rs b/contracts/coconut-dkg/src/dealers/transactions.rs index 44cc19d9b5..0c2d0b4241 100644 --- a/contracts/coconut-dkg/src/dealers/transactions.rs +++ b/contracts/coconut-dkg/src/dealers/transactions.rs @@ -2,26 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 use crate::dealers::storage as dealers_storage; +use crate::epoch_state::storage::INITIAL_REPLACEMENT_DATA; use crate::epoch_state::utils::check_epoch_state; use crate::error::ContractError; -use crate::state::{State, STATE}; +use crate::state::STATE; use coconut_dkg_common::types::{DealerDetails, EncodedBTEPublicKeyWithProof, EpochState}; use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response}; // currently we only require that // a) it's part of the signer group // b) it isn't already a dealer -fn verify_dealer(deps: DepsMut<'_>, state: &State, dealer: &Addr) -> Result<(), ContractError> { +fn verify_dealer(deps: DepsMut<'_>, dealer: &Addr, resharing: bool) -> Result<(), ContractError> { if dealers_storage::current_dealers() .may_load(deps.storage, dealer)? .is_some() { return Err(ContractError::AlreadyADealer); } + let state = STATE.load(deps.storage)?; + let height = if resharing { + INITIAL_REPLACEMENT_DATA.load(deps.storage)?.initial_height + } else { + None + }; state .group_addr - .is_voting_member(&deps.querier, dealer, None)? + .is_voting_member(&deps.querier, dealer, height)? .ok_or(ContractError::Unauthorized {})?; Ok(()) @@ -32,11 +39,11 @@ pub fn try_add_dealer( info: MessageInfo, bte_key_with_proof: EncodedBTEPublicKeyWithProof, announce_address: String, + resharing: bool, ) -> Result { - check_epoch_state(deps.storage, EpochState::PublicKeySubmission)?; - let state = STATE.load(deps.storage)?; + check_epoch_state(deps.storage, EpochState::PublicKeySubmission { resharing })?; - verify_dealer(deps.branch(), &state, &info.sender)?; + verify_dealer(deps.branch(), &info.sender, resharing)?; // if it was already a dealer in the past, assign the same node index let node_index = if let Some(prior_details) = @@ -69,10 +76,63 @@ pub fn try_add_dealer( #[cfg(test)] pub(crate) mod tests { use super::*; + use crate::dealers::storage::current_dealers; use crate::epoch_state::transactions::advance_epoch_state; + use crate::support::tests::fixtures::dealer_details_fixture; use crate::support::tests::helpers; - use coconut_dkg_common::types::TimeConfiguration; + use crate::support::tests::helpers::GROUP_MEMBERS; + use coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration}; use cosmwasm_std::testing::{mock_env, mock_info}; + use cw4::Member; + use rusty_fork::rusty_fork_test; + + rusty_fork_test! { + #[test] + fn verification() { + let mut deps = helpers::init_contract(); + let new_dealer = Addr::unchecked("new_dealer"); + let details1 = dealer_details_fixture(1); + let details2 = dealer_details_fixture(2); + let details3 = dealer_details_fixture(3); + current_dealers() + .save(deps.as_mut().storage, &details1.address, &details1) + .unwrap(); + let err = verify_dealer(deps.as_mut(), &details1.address, false).unwrap_err(); + assert_eq!(err, ContractError::AlreadyADealer); + + INITIAL_REPLACEMENT_DATA + .save( + deps.as_mut().storage, + &InitialReplacementData { + initial_dealers: vec![details1.address, details2.address, details3.address], + initial_height: Some(1), + }, + ) + .unwrap(); + let err = verify_dealer(deps.as_mut(), &new_dealer, false).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized); + + GROUP_MEMBERS.lock().unwrap().push(( + Member { + addr: new_dealer.to_string(), + weight: 10, + }, + 2, + )); + verify_dealer(deps.as_mut(), &new_dealer, false).unwrap(); + + let err = verify_dealer(deps.as_mut(), &new_dealer, true).unwrap_err(); + assert_eq!(err, ContractError::Unauthorized); + + INITIAL_REPLACEMENT_DATA + .update::<_, ContractError>(deps.as_mut().storage, |mut data| { + data.initial_height = Some(2); + Ok(data) + }) + .unwrap(); + verify_dealer(deps.as_mut(), &new_dealer, true).unwrap(); + } + } #[test] fn invalid_state() { @@ -94,12 +154,13 @@ pub(crate) mod tests { info.clone(), bte_key_with_proof.clone(), announce_address.clone(), + false, ) .unwrap_err(); assert_eq!( ret, ContractError::IncorrectEpochState { - current_state: EpochState::DealingExchange.to_string(), + current_state: EpochState::DealingExchange { resharing: false }.to_string(), expected_state: EpochState::default().to_string(), } ); diff --git a/contracts/coconut-dkg/src/dealings/transactions.rs b/contracts/coconut-dkg/src/dealings/transactions.rs index 9cca53cb02..3f6a797775 100644 --- a/contracts/coconut-dkg/src/dealings/transactions.rs +++ b/contracts/coconut-dkg/src/dealings/transactions.rs @@ -3,6 +3,7 @@ use crate::dealers::storage as dealers_storage; use crate::dealings::storage::DEALINGS_BYTES; +use crate::epoch_state::storage::INITIAL_REPLACEMENT_DATA; use crate::epoch_state::utils::check_epoch_state; use crate::error::ContractError; use coconut_dkg_common::types::{ContractSafeBytes, EpochState}; @@ -12,8 +13,9 @@ pub fn try_commit_dealings( deps: DepsMut<'_>, info: MessageInfo, dealing_bytes: ContractSafeBytes, + resharing: bool, ) -> Result { - check_epoch_state(deps.storage, EpochState::DealingExchange)?; + check_epoch_state(deps.storage, EpochState::DealingExchange { resharing })?; // ensure the sender is a dealer if dealers_storage::current_dealers() .may_load(deps.storage, &info.sender)? @@ -21,6 +23,14 @@ pub fn try_commit_dealings( { return Err(ContractError::NotADealer); } + if resharing + && !INITIAL_REPLACEMENT_DATA + .load(deps.storage)? + .initial_dealers + .contains(&info.sender) + { + return Err(ContractError::NotAnInitialDealer); + } // check if this dealer has already committed to all dealings // (we don't want to allow overwriting anything) @@ -39,29 +49,30 @@ pub fn try_commit_dealings( #[cfg(test)] pub(crate) mod tests { use super::*; + use crate::epoch_state::storage::CURRENT_EPOCH; use crate::epoch_state::transactions::advance_epoch_state; - use crate::support::tests::fixtures::dealing_bytes_fixture; + use crate::support::tests::fixtures::{dealer_details_fixture, dealing_bytes_fixture}; use crate::support::tests::helpers; use coconut_dkg_common::dealer::DealerDetails; - use coconut_dkg_common::types::TimeConfiguration; + use coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration}; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::Addr; #[test] fn invalid_commit_dealing() { let mut deps = helpers::init_contract(); - let owner = Addr::unchecked("owner"); + let owner = Addr::unchecked("owner1"); let mut env = mock_env(); let info = mock_info(owner.as_str(), &[]); let dealing_bytes = dealing_bytes_fixture(); - let ret = - try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone()).unwrap_err(); + let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), false) + .unwrap_err(); assert_eq!( ret, ContractError::IncorrectEpochState { current_state: EpochState::default().to_string(), - expected_state: EpochState::DealingExchange.to_string() + expected_state: EpochState::DealingExchange { resharing: false }.to_string() } ); @@ -71,8 +82,8 @@ pub(crate) mod tests { .plus_seconds(TimeConfiguration::default().public_key_submission_time_secs); advance_epoch_state(deps.as_mut(), env).unwrap(); - let ret = - try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone()).unwrap_err(); + let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), false) + .unwrap_err(); assert_eq!(ret, ContractError::NotADealer); let dealer_details = DealerDetails { @@ -85,14 +96,41 @@ pub(crate) mod tests { .save(deps.as_mut().storage, &owner, &dealer_details) .unwrap(); + // assume we're in resharing mode + CURRENT_EPOCH + .update::<_, ContractError>(deps.as_mut().storage, |mut epoch| { + epoch.state = EpochState::DealingExchange { resharing: true }; + Ok(epoch) + }) + .unwrap(); + INITIAL_REPLACEMENT_DATA + .save( + deps.as_mut().storage, + &InitialReplacementData { + initial_dealers: vec![], + initial_height: None, + }, + ) + .unwrap(); + let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), true) + .unwrap_err(); + assert_eq!(ret, ContractError::NotAnInitialDealer); + + INITIAL_REPLACEMENT_DATA + .update::<_, ContractError>(deps.as_mut().storage, |mut data| { + data.initial_dealers = vec![dealer_details_fixture(1).address]; + Ok(data) + }) + .unwrap(); + for dealings in DEALINGS_BYTES { assert!(!dealings.has(deps.as_mut().storage, &owner)); - let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone()); + let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), true); assert!(ret.is_ok()); assert!(dealings.has(deps.as_mut().storage, &owner)); } - let ret = - try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone()).unwrap_err(); + let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), true) + .unwrap_err(); assert_eq!( ret, ContractError::AlreadyCommitted { diff --git a/contracts/coconut-dkg/src/epoch_state/queries.rs b/contracts/coconut-dkg/src/epoch_state/queries.rs index 386e9000c9..c6aedb17e8 100644 --- a/contracts/coconut-dkg/src/epoch_state/queries.rs +++ b/contracts/coconut-dkg/src/epoch_state/queries.rs @@ -1,9 +1,9 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::epoch_state::storage::{CURRENT_EPOCH, THRESHOLD}; +use crate::epoch_state::storage::{CURRENT_EPOCH, INITIAL_REPLACEMENT_DATA, THRESHOLD}; use crate::error::ContractError; -use coconut_dkg_common::types::Epoch; +use coconut_dkg_common::types::{Epoch, InitialReplacementData}; use cosmwasm_std::Storage; pub(crate) fn query_current_epoch(storage: &dyn Storage) -> Result { @@ -18,6 +18,12 @@ pub(crate) fn query_current_epoch_threshold( Ok(THRESHOLD.may_load(storage)?) } +pub(crate) fn query_initial_dealers( + storage: &dyn Storage, +) -> Result, ContractError> { + Ok(INITIAL_REPLACEMENT_DATA.may_load(storage)?) +} + #[cfg(test)] pub(crate) mod test { use super::*; @@ -29,7 +35,10 @@ pub(crate) mod test { fn query_state() { let mut deps = init_contract(); let epoch = query_current_epoch(deps.as_mut().storage).unwrap(); - assert_eq!(epoch.state, EpochState::PublicKeySubmission); + assert_eq!( + epoch.state, + EpochState::PublicKeySubmission { resharing: false } + ); assert_eq!( epoch.finish_timestamp, mock_env() diff --git a/contracts/coconut-dkg/src/epoch_state/storage.rs b/contracts/coconut-dkg/src/epoch_state/storage.rs index d5aac5b041..95536fd7c9 100644 --- a/contracts/coconut-dkg/src/epoch_state/storage.rs +++ b/contracts/coconut-dkg/src/epoch_state/storage.rs @@ -1,8 +1,10 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use coconut_dkg_common::types::Epoch; +use coconut_dkg_common::types::{Epoch, InitialReplacementData}; use cw_storage_plus::Item; pub(crate) const CURRENT_EPOCH: Item<'_, Epoch> = Item::new("current_epoch"); pub const THRESHOLD: Item = Item::new("threshold"); +pub const INITIAL_REPLACEMENT_DATA: Item = + Item::new("initial_replacement_data"); diff --git a/contracts/coconut-dkg/src/epoch_state/transactions.rs b/contracts/coconut-dkg/src/epoch_state/transactions.rs index 31e3916021..414ba2344c 100644 --- a/contracts/coconut-dkg/src/epoch_state/transactions.rs +++ b/contracts/coconut-dkg/src/epoch_state/transactions.rs @@ -3,12 +3,12 @@ use crate::dealers::storage::{current_dealers, past_dealers}; use crate::dealings::storage::DEALINGS_BYTES; -use crate::epoch_state::storage::{CURRENT_EPOCH, THRESHOLD}; +use crate::epoch_state::storage::{CURRENT_EPOCH, INITIAL_REPLACEMENT_DATA, THRESHOLD}; use crate::epoch_state::utils::check_epoch_state; use crate::error::ContractError; use crate::state::STATE; -use coconut_dkg_common::types::{Epoch, EpochState}; -use cosmwasm_std::{DepsMut, Env, Order, Response, Storage}; +use coconut_dkg_common::types::{Epoch, EpochState, InitialReplacementData}; +use cosmwasm_std::{Addr, Deps, DepsMut, Env, Order, Response, Storage}; fn reset_epoch_state(storage: &mut dyn Storage) -> Result<(), ContractError> { THRESHOLD.remove(storage); @@ -27,13 +27,13 @@ fn reset_epoch_state(storage: &mut dyn Storage) -> Result<(), ContractError> { Ok(()) } -fn dealers_still_active(deps: &DepsMut<'_>) -> Result { +fn dealers_still_active( + deps: &Deps<'_>, + dealers: impl Iterator, +) -> Result { let state = STATE.load(deps.storage)?; let mut still_active = 0; - for dealer_addr in current_dealers() - .keys(deps.storage, None, None, Order::Ascending) - .flatten() - { + for dealer_addr in dealers { if state .group_addr .is_voting_member(&deps.querier, &dealer_addr, None)? @@ -45,6 +45,36 @@ fn dealers_still_active(deps: &DepsMut<'_>) -> Result { Ok(still_active) } +fn dealers_eq_members(deps: &DepsMut<'_>) -> Result { + let dealers_still_active = dealers_still_active( + &deps.as_ref(), + current_dealers() + .keys(deps.storage, None, None, Order::Ascending) + .flatten(), + )?; + let all_dealers = current_dealers() + .keys(deps.storage, None, None, Order::Ascending) + .count(); + let group_members = STATE + .load(deps.storage)? + .group_addr + .list_members(&deps.querier, None, None)? + .len(); + + Ok(dealers_still_active == all_dealers && all_dealers == group_members) +} + +fn replacement_threshold_surpassed(deps: &DepsMut<'_>) -> Result { + let threshold = THRESHOLD.load(deps.storage)? as usize; + let initial_dealers = INITIAL_REPLACEMENT_DATA.load(deps.storage)?.initial_dealers; + let initial_dealer_count = initial_dealers.len(); + let replacement_threshold = threshold - (initial_dealers.len() + 2 - 1) / 2 + 1; + let removed_dealer_count = + initial_dealer_count - dealers_still_active(&deps.as_ref(), initial_dealers.into_iter())?; + + Ok(removed_dealer_count >= replacement_threshold) +} + pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result { let epoch = CURRENT_EPOCH.load(deps.storage)?; if epoch.finish_timestamp > env.block.time { @@ -59,13 +89,20 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result, _>>()?; // note: ceiling in integer division can be achieved via q = (x + y - 1) / y; - let threshold = (2 * current_dealer_count as u64 + 3 - 1) / 3; + let threshold = (2 * current_dealers.len() as u64 + 3 - 1) / 3; THRESHOLD.save(deps.storage, &threshold)?; + if !resharing { + let replacement_data = InitialReplacementData { + initial_dealers: current_dealers, + initial_height: None, + }; + INITIAL_REPLACEMENT_DATA.save(deps.storage, &replacement_data)?; + } } Epoch::new( state, @@ -73,14 +110,9 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result, env: Env) -> Result(deps.storage, |mut data| { + data.initial_height = Some(env.block.height); + Ok(data) + })?; + EpochState::PublicKeySubmission { resharing: true } + }; reset_epoch_state(deps.storage)?; Epoch::new( - EpochState::default(), + state, current_epoch.epoch_id + 1, current_epoch.time_configuration, env.block.time, @@ -109,7 +152,10 @@ pub(crate) fn try_surpassed_threshold( check_epoch_state(deps.storage, EpochState::InProgress)?; let threshold = THRESHOLD.load(deps.storage)?; - if dealers_still_active(&deps)? < threshold as usize { + let dealers = current_dealers() + .keys(deps.storage, None, None, Order::Ascending) + .flatten(); + if dealers_still_active(&deps.as_ref(), dealers)? < threshold as usize { reset_epoch_state(deps.storage)?; CURRENT_EPOCH.update::<_, ContractError>(deps.storage, |epoch| { Ok(Epoch::new( @@ -139,27 +185,137 @@ pub(crate) mod tests { use rusty_fork::rusty_fork_test; // Because of the global variable handling group, we need individual process for each test + rusty_fork_test! { + // Using values from the DKG document + #[test] + fn threshold_surpassed() { + let mut deps = init_contract(); + let two_thirds = |n: u64| (2 * n + 3 - 1) / 3; + let three_fourths = |n: u64| (3 * n + 4 - 1) / 4; + let ninty_pc = |n: u64| (9 * n + 10 - 2) / 10; + let mut limits = [3, 4, 5, 5, 7, 11, 10, 14, 21, 18, 26, 41].iter(); + + for n in [10, 25, 50, 100] { + let dealers: Vec<_> = (0..n).map(dealer_details_fixture).collect(); + let initial_dealers = dealers.iter().map(|d| d.address.clone()).collect(); + let data = InitialReplacementData { + initial_dealers, + initial_height: None, + }; + for f in [two_thirds, three_fourths, ninty_pc] { + let threshold = f(n); + THRESHOLD.save(deps.as_mut().storage, &threshold).unwrap(); + INITIAL_REPLACEMENT_DATA + .save(deps.as_mut().storage, &data) + .unwrap(); + + let limit = *limits.next().unwrap(); + { + let mut group_members = GROUP_MEMBERS.lock().unwrap(); + for i in 0..n as usize { + group_members.push(( + Member { + addr: dealers[i].address.to_string(), + weight: 10, + }, + 1, + )); + } + for _ in 1..limit { + group_members.pop(); + } + } + assert!(!replacement_threshold_surpassed(&deps.as_mut()).unwrap()); + GROUP_MEMBERS.lock().unwrap().pop(); + assert!(replacement_threshold_surpassed(&deps.as_mut()).unwrap()); + + *GROUP_MEMBERS.lock().unwrap() = vec![]; + } + } + } + + #[test] + fn dealers_and_members() { + let mut deps = init_contract(); + + assert!(dealers_eq_members(&deps.as_mut()).unwrap()); + + let details = dealer_details_fixture(1); + let different_details = dealer_details_fixture(2); + current_dealers() + .save(deps.as_mut().storage, &details.address, &details) + .unwrap(); + assert!(!dealers_eq_members(&deps.as_mut()).unwrap()); + + current_dealers() + .remove(deps.as_mut().storage, &details.address) + .unwrap(); + GROUP_MEMBERS.lock().unwrap().push(( + Member { + addr: "owner1".to_string(), + weight: 10, + }, + 1, + )); + assert!(!dealers_eq_members(&deps.as_mut()).unwrap()); + + current_dealers() + .save( + deps.as_mut().storage, + &different_details.address, + &different_details, + ) + .unwrap(); + assert!(!dealers_eq_members(&deps.as_mut()).unwrap()); + + current_dealers() + .remove(deps.as_mut().storage, &different_details.address) + .unwrap(); + current_dealers() + .save(deps.as_mut().storage, &details.address, &details) + .unwrap(); + assert!(dealers_eq_members(&deps.as_mut()).unwrap()); + } + #[test] fn still_active() { let mut deps = init_contract(); { let mut group = GROUP_MEMBERS.lock().unwrap(); - group.push(Member { - addr: "owner1".to_string(), - weight: 10, - }); - group.push(Member { - addr: "owner2".to_string(), - weight: 10, - }); - group.push(Member { - addr: "owner3".to_string(), - weight: 10, - }); + group.push(( + Member { + addr: "owner1".to_string(), + weight: 10, + }, + 1, + )); + group.push(( + Member { + addr: "owner2".to_string(), + weight: 10, + }, + 1, + )); + group.push(( + Member { + addr: "owner3".to_string(), + weight: 10, + }, + 1, + )); } - assert_eq!(0, dealers_still_active(&deps.as_mut()).unwrap()); + assert_eq!( + 0, + dealers_still_active( + &deps.as_ref(), + current_dealers() + .keys(&deps.storage, None, None, Order::Ascending) + .flatten() + ) + .unwrap() + ); for i in 0..3 as u64 { let details = dealer_details_fixture(i + 1); current_dealers() @@ -167,7 +323,13 @@ pub(crate) mod tests { .unwrap(); assert_eq!( i as usize + 1, - dealers_still_active(&deps.as_mut()).unwrap() + dealers_still_active( + &deps.as_ref(), + current_dealers() + .keys(&deps.storage, None, None, Order::Ascending) + .flatten() + ) + .unwrap() ); } } @@ -179,22 +341,41 @@ pub(crate) mod tests { { let mut group = GROUP_MEMBERS.lock().unwrap(); - group.push(Member { - addr: "owner1".to_string(), - weight: 10, - }); - group.push(Member { - addr: "owner2".to_string(), - weight: 10, - }); - group.push(Member { - addr: "owner3".to_string(), - weight: 10, - }); + group.push(( + Member { + addr: "owner1".to_string(), + weight: 10, + }, + 1, + )); + group.push(( + Member { + addr: "owner2".to_string(), + weight: 10, + }, + 1, + )); + group.push(( + Member { + addr: "owner3".to_string(), + weight: 10, + }, + 1, + )); + group.push(( + Member { + addr: "owner4".to_string(), + weight: 10, + }, + 1, + )); } let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); - assert_eq!(epoch.state, EpochState::PublicKeySubmission); + assert_eq!( + epoch.state, + EpochState::PublicKeySubmission { resharing: false } + ); assert_eq!( epoch.finish_timestamp, env.block @@ -211,16 +392,37 @@ pub(crate) mod tests { EarlyEpochStateAdvancement(1) ); + // setup dealer details + let all_details: [_; 4] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 1)); + for details in all_details.iter() { + current_dealers() + .save(deps.as_mut().storage, &details.address, details) + .unwrap(); + } + + assert!(INITIAL_REPLACEMENT_DATA + .may_load(&deps.storage) + .unwrap() + .is_none()); env.block.time = env.block.time.plus_seconds(1); advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); - assert_eq!(epoch.state, EpochState::DealingExchange); + assert_eq!( + epoch.state, + EpochState::DealingExchange { resharing: false } + ); assert_eq!( epoch.finish_timestamp, env.block .time .plus_seconds(epoch.time_configuration.dealing_exchange_time_secs) ); + let replacement_data = INITIAL_REPLACEMENT_DATA.load(&deps.storage).unwrap(); + let expected_replacement_data = InitialReplacementData { + initial_dealers: all_details.iter().map(|d| d.address.clone()).collect(), + initial_height: None, + }; + assert_eq!(replacement_data, expected_replacement_data); env.block.time = env .block @@ -234,7 +436,10 @@ pub(crate) mod tests { env.block.time = env.block.time.plus_seconds(3); advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); - assert_eq!(epoch.state, EpochState::VerificationKeySubmission); + assert_eq!( + epoch.state, + EpochState::VerificationKeySubmission { resharing: false } + ); assert_eq!( epoch.finish_timestamp, env.block.time.plus_seconds( @@ -258,7 +463,10 @@ pub(crate) mod tests { env.block.time = env.block.time.plus_seconds(3); advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); - assert_eq!(epoch.state, EpochState::VerificationKeyValidation); + assert_eq!( + epoch.state, + EpochState::VerificationKeyValidation { resharing: false } + ); assert_eq!( epoch.finish_timestamp, env.block.time.plus_seconds( @@ -282,7 +490,10 @@ pub(crate) mod tests { env.block.time = env.block.time.plus_seconds(3); advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); - assert_eq!(epoch.state, EpochState::VerificationKeyFinalization); + assert_eq!( + epoch.state, + EpochState::VerificationKeyFinalization { resharing: false } + ); assert_eq!( epoch.finish_timestamp, env.block.time.plus_seconds( @@ -327,14 +538,6 @@ pub(crate) mod tests { EarlyEpochStateAdvancement(50) ); - // setup dealer details - let all_details: [_; 3] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 1)); - for details in all_details.iter() { - current_dealers() - .save(deps.as_mut().storage, &details.address, details) - .unwrap(); - } - // Group hasn't changed, so we remain in the same epoch, with updated finish timestamp env.block.time = env.block.time.plus_seconds(100); let prev_epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); @@ -348,11 +551,14 @@ pub(crate) mod tests { ); assert_eq!(curr_epoch, expected_epoch); - // Group changed, slightly, so reset dkg state - *GROUP_MEMBERS.lock().unwrap().first_mut().unwrap() = Member { - addr: "owner4".to_string(), - weight: 10, - }; + // Group changed slightly, so re-run dkg in reshare mode + *GROUP_MEMBERS.lock().unwrap().first_mut().unwrap() = ( + Member { + addr: "owner5".to_string(), + weight: 10, + }, + 1, + ); env.block.time = env .block .time @@ -361,7 +567,49 @@ pub(crate) mod tests { advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); let curr_epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); let expected_epoch = Epoch::new( - EpochState::default(), + EpochState::PublicKeySubmission { resharing: true }, + prev_epoch.epoch_id + 1, + prev_epoch.time_configuration, + env.block.time, + ); + assert_eq!(curr_epoch, expected_epoch); + assert!(THRESHOLD.may_load(&deps.storage).unwrap().is_none()); + + let all_details: [_; 2] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 2)); + for details in all_details.iter() { + past_dealers().remove(deps.as_mut().storage, &details.address).unwrap(); + current_dealers() + .save(deps.as_mut().storage, &details.address, details) + .unwrap(); + } + for times in [ + epoch.time_configuration.public_key_submission_time_secs, + epoch.time_configuration.dealing_exchange_time_secs, + epoch.time_configuration.verification_key_submission_time_secs, + epoch.time_configuration.verification_key_validation_time_secs, + epoch.time_configuration.verification_key_finalization_time_secs, + ] { + env.block.time = env.block.time.plus_seconds(times); + advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); + } + + // Group changed even more, surpassing threshold, so re-run dkg in reset mode + *GROUP_MEMBERS.lock().unwrap().last_mut().unwrap() = ( + Member { + addr: "owner6".to_string(), + weight: 10, + }, + 1, + ); + env.block.time = env + .block + .time + .plus_seconds(epoch.time_configuration.in_progress_time_secs); + let prev_epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); + advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); + let curr_epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap(); + let expected_epoch = Epoch::new( + EpochState::PublicKeySubmission { resharing: false }, prev_epoch.epoch_id + 1, prev_epoch.time_configuration, env.block.time, @@ -370,89 +618,103 @@ pub(crate) mod tests { assert!(THRESHOLD.may_load(&deps.storage).unwrap().is_none()); } + #[test] + fn surpass_threshold() { + let mut deps = init_contract(); + let mut env = mock_env(); + let time_configuration = TimeConfiguration::default(); + { + let mut group = GROUP_MEMBERS.lock().unwrap(); - #[test] - fn surpass_threshold() { - let mut deps = init_contract(); - let mut env = mock_env(); - let time_configuration = TimeConfiguration::default(); - { - let mut group = GROUP_MEMBERS.lock().unwrap(); - - group.push(Member { - addr: "owner1".to_string(), - weight: 10, - }); - group.push(Member { - addr: "owner2".to_string(), - weight: 10, - }); - group.push(Member { - addr: "owner3".to_string(), - weight: 10, - }); - } - - let ret = try_surpassed_threshold(deps.as_mut(), env.clone()).unwrap_err(); - assert_eq!( - ret, - ContractError::IncorrectEpochState { - current_state: EpochState::default().to_string(), - expected_state: EpochState::InProgress.to_string() + group.push(( + Member { + addr: "owner1".to_string(), + weight: 10, + }, + 1, + )); + group.push(( + Member { + addr: "owner2".to_string(), + weight: 10, + }, + 1, + )); + group.push(( + Member { + addr: "owner3".to_string(), + weight: 10, + }, + 1, + )); } - ); - let all_details: [_; 3] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 1)); - for details in all_details.iter() { - current_dealers() - .save(deps.as_mut().storage, &details.address, details) - .unwrap(); + let ret = try_surpassed_threshold(deps.as_mut(), env.clone()).unwrap_err(); + assert_eq!( + ret, + ContractError::IncorrectEpochState { + current_state: EpochState::default().to_string(), + expected_state: EpochState::InProgress.to_string() + } + ); + + let all_details: [_; 3] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 1)); + for details in all_details.iter() { + current_dealers() + .save(deps.as_mut().storage, &details.address, details) + .unwrap(); + } + + for times in [ + time_configuration.public_key_submission_time_secs, + time_configuration.dealing_exchange_time_secs, + time_configuration.verification_key_submission_time_secs, + time_configuration.verification_key_validation_time_secs, + time_configuration.verification_key_finalization_time_secs, + ] { + env.block.time = env.block.time.plus_seconds(times); + advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); + } + let curr_epoch = CURRENT_EPOCH.load(&deps.storage).unwrap(); + assert_eq!(THRESHOLD.load(&deps.storage).unwrap(), 2); + + // epoch hasn't advanced as we are still in the threshold range + try_surpassed_threshold(deps.as_mut(), env.clone()).unwrap(); + assert_eq!(THRESHOLD.load(&deps.storage).unwrap(), 2); + assert_eq!(CURRENT_EPOCH.load(&deps.storage).unwrap(), curr_epoch); + + *GROUP_MEMBERS.lock().unwrap().first_mut().unwrap() = ( + Member { + addr: "owner4".to_string(), + weight: 10, + }, + 1, + ); + // epoch hasn't advanced as we are still in the threshold range + try_surpassed_threshold(deps.as_mut(), env.clone()).unwrap(); + assert_eq!(THRESHOLD.load(&deps.storage).unwrap(), 2); + assert_eq!(CURRENT_EPOCH.load(&deps.storage).unwrap(), curr_epoch); + + *GROUP_MEMBERS.lock().unwrap().last_mut().unwrap() = ( + Member { + addr: "owner5".to_string(), + weight: 10, + }, + 1, + ); + try_surpassed_threshold(deps.as_mut(), env.clone()).unwrap(); + assert!(THRESHOLD.may_load(&deps.storage).unwrap().is_none()); + let next_epoch = CURRENT_EPOCH.load(&deps.storage).unwrap(); + assert_eq!( + next_epoch, + Epoch::new( + EpochState::default(), + curr_epoch.epoch_id + 1, + curr_epoch.time_configuration, + env.block.time, + ) + ); } - - for times in [ - time_configuration.public_key_submission_time_secs, - time_configuration.dealing_exchange_time_secs, - time_configuration.verification_key_submission_time_secs, - time_configuration.verification_key_validation_time_secs, - time_configuration.verification_key_finalization_time_secs, - ] { - env.block.time = env.block.time.plus_seconds(times); - advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); - } - let curr_epoch = CURRENT_EPOCH.load(&deps.storage).unwrap(); - assert_eq!(THRESHOLD.load(&deps.storage).unwrap(), 2); - - // epoch hasn't advanced as we are still in the threshold range - try_surpassed_threshold(deps.as_mut(), env.clone()).unwrap(); - assert_eq!(THRESHOLD.load(&deps.storage).unwrap(), 2); - assert_eq!(CURRENT_EPOCH.load(&deps.storage).unwrap(), curr_epoch); - - *GROUP_MEMBERS.lock().unwrap().first_mut().unwrap() = Member { - addr: "owner4".to_string(), - weight: 10, - }; - // epoch hasn't advanced as we are still in the threshold range - try_surpassed_threshold(deps.as_mut(), env.clone()).unwrap(); - assert_eq!(THRESHOLD.load(&deps.storage).unwrap(), 2); - assert_eq!(CURRENT_EPOCH.load(&deps.storage).unwrap(), curr_epoch); - - *GROUP_MEMBERS.lock().unwrap().last_mut().unwrap() = Member { - addr: "owner5".to_string(), - weight: 10, - }; - try_surpassed_threshold(deps.as_mut(), env.clone()).unwrap(); - assert!(THRESHOLD.may_load(&deps.storage).unwrap().is_none()); - let next_epoch = CURRENT_EPOCH.load(&deps.storage).unwrap(); - assert_eq!( - next_epoch, - Epoch::new( - EpochState::default(), - curr_epoch.epoch_id + 1, - curr_epoch.time_configuration, - env.block.time, - ) - ); - } } #[test] diff --git a/contracts/coconut-dkg/src/error.rs b/contracts/coconut-dkg/src/error.rs index 0baeae5edd..f8458d0c47 100644 --- a/contracts/coconut-dkg/src/error.rs +++ b/contracts/coconut-dkg/src/error.rs @@ -40,6 +40,9 @@ pub enum ContractError { #[error("This sender is not a dealer for the current epoch")] NotADealer, + #[error("This sender is not a dealer for the current resharing epoch")] + NotAnInitialDealer, + #[error("This dealer has already committed {commitment}")] AlreadyCommitted { commitment: String }, diff --git a/contracts/coconut-dkg/src/support/tests/helpers.rs b/contracts/coconut-dkg/src/support/tests/helpers.rs index d5451e1ee1..2b8dd56115 100644 --- a/contracts/coconut-dkg/src/support/tests/helpers.rs +++ b/contracts/coconut-dkg/src/support/tests/helpers.rs @@ -19,7 +19,7 @@ pub const GROUP_CONTRACT: &str = "group contract address"; pub const MULTISIG_CONTRACT: &str = "multisig contract address"; lazy_static! { - pub static ref GROUP_MEMBERS: Mutex> = Mutex::new(vec![]); + pub static ref GROUP_MEMBERS: Mutex> = Mutex::new(vec![]); } fn querier_handler(query: &WasmQuery) -> QuerierResult { @@ -29,9 +29,14 @@ fn querier_handler(query: &WasmQuery) -> QuerierResult { panic!("Not supported"); } match from_binary(msg) { - Ok(Cw4QueryMsg::Member { addr, .. }) => { - let weight = GROUP_MEMBERS.lock().unwrap().iter().find_map(|m| { + Ok(Cw4QueryMsg::Member { addr, at_height }) => { + let weight = GROUP_MEMBERS.lock().unwrap().iter().find_map(|(m, h)| { if m.addr == addr { + if let Some(height) = at_height { + if height != *h { + return None; + } + } Some(m.weight) } else { None @@ -40,7 +45,12 @@ fn querier_handler(query: &WasmQuery) -> QuerierResult { to_binary(&MemberResponse { weight }).unwrap() } Ok(Cw4QueryMsg::ListMembers { .. }) => { - let members = GROUP_MEMBERS.lock().unwrap().to_vec(); + let members = GROUP_MEMBERS + .lock() + .unwrap() + .iter() + .map(|m| m.0.clone()) + .collect(); to_binary(&MemberListResponse { members }).unwrap() } _ => panic!("Not supported"), diff --git a/contracts/coconut-dkg/src/verification_key_shares/transactions.rs b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs index 45bbe529d2..f7b2e80844 100644 --- a/contracts/coconut-dkg/src/verification_key_shares/transactions.rs +++ b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs @@ -17,8 +17,12 @@ pub fn try_commit_verification_key_share( env: Env, info: MessageInfo, share: VerificationKeyShare, + resharing: bool, ) -> Result { - check_epoch_state(deps.storage, EpochState::VerificationKeySubmission)?; + check_epoch_state( + deps.storage, + EpochState::VerificationKeySubmission { resharing }, + )?; // ensure the sender is a dealer let details = dealers_storage::current_dealers() .load(deps.storage, &info.sender) @@ -45,6 +49,7 @@ pub fn try_commit_verification_key_share( let msg = to_cosmos_msg( info.sender, + resharing, env.contract.address.to_string(), STATE.load(deps.storage)?.multisig_addr.to_string(), env.block @@ -59,8 +64,12 @@ pub fn try_verify_verification_key_share( deps: DepsMut<'_>, info: MessageInfo, owner: Addr, + resharing: bool, ) -> Result { - check_epoch_state(deps.storage, EpochState::VerificationKeyFinalization)?; + check_epoch_state( + deps.storage, + EpochState::VerificationKeyFinalization { resharing }, + )?; let epoch_id = CURRENT_EPOCH.load(deps.storage)?.epoch_id; MULTISIG.assert_admin(deps.as_ref(), &info.sender)?; vk_shares().update(deps.storage, (&owner, epoch_id), |vk_share| { @@ -117,8 +126,14 @@ mod tests { .save(deps.as_mut().storage, &dealer, &dealer_details) .unwrap(); - try_commit_verification_key_share(deps.as_mut(), env.clone(), info.clone(), share.clone()) - .unwrap(); + try_commit_verification_key_share( + deps.as_mut(), + env.clone(), + info.clone(), + share.clone(), + false, + ) + .unwrap(); let vk_share = vk_shares().load(&deps.storage, (&info.sender, 0)).unwrap(); assert_eq!( vk_share, @@ -145,13 +160,15 @@ mod tests { env.clone(), info.clone(), share.clone(), + false, ) .unwrap_err(); assert_eq!( ret, ContractError::IncorrectEpochState { current_state: EpochState::default().to_string(), - expected_state: EpochState::VerificationKeySubmission.to_string() + expected_state: EpochState::VerificationKeySubmission { resharing: false } + .to_string() } ); env.block.time = env @@ -169,6 +186,7 @@ mod tests { env.clone(), info.clone(), share.clone(), + false, ) .unwrap_err(); assert_eq!(ret, ContractError::NotADealer); @@ -184,14 +202,21 @@ mod tests { .save(deps.as_mut().storage, &dealer, &dealer_details) .unwrap(); - try_commit_verification_key_share(deps.as_mut(), env.clone(), info.clone(), share.clone()) - .unwrap(); + try_commit_verification_key_share( + deps.as_mut(), + env.clone(), + info.clone(), + share.clone(), + false, + ) + .unwrap(); let ret = try_commit_verification_key_share( deps.as_mut(), env.clone(), info.clone(), share.clone(), + false, ) .unwrap_err(); assert_eq!( @@ -210,13 +235,15 @@ mod tests { let owner = Addr::unchecked("owner"); let multisig_info = mock_info(MULTISIG_CONTRACT, &[]); - let ret = try_verify_verification_key_share(deps.as_mut(), info.clone(), owner.clone()) - .unwrap_err(); + let ret = + try_verify_verification_key_share(deps.as_mut(), info.clone(), owner.clone(), false) + .unwrap_err(); assert_eq!( ret, ContractError::IncorrectEpochState { current_state: EpochState::default().to_string(), - expected_state: EpochState::VerificationKeyFinalization.to_string() + expected_state: EpochState::VerificationKeyFinalization { resharing: false } + .to_string() } ); @@ -241,12 +268,13 @@ mod tests { .plus_seconds(TimeConfiguration::default().verification_key_validation_time_secs); advance_epoch_state(deps.as_mut(), env).unwrap(); - let ret = - try_verify_verification_key_share(deps.as_mut(), info, owner.clone()).unwrap_err(); + let ret = try_verify_verification_key_share(deps.as_mut(), info, owner.clone(), false) + .unwrap_err(); assert_eq!(ret, ContractError::Admin(AdminError::NotAdmin {})); - let ret = try_verify_verification_key_share(deps.as_mut(), multisig_info, owner.clone()) - .unwrap_err(); + let ret = + try_verify_verification_key_share(deps.as_mut(), multisig_info, owner.clone(), false) + .unwrap_err(); assert_eq!( ret, ContractError::NoCommitForOwner { @@ -284,8 +312,14 @@ mod tests { dealers_storage::current_dealers() .save(deps.as_mut().storage, &owner, &dealer_details) .unwrap(); - try_commit_verification_key_share(deps.as_mut(), env.clone(), info.clone(), share.clone()) - .unwrap(); + try_commit_verification_key_share( + deps.as_mut(), + env.clone(), + info.clone(), + share.clone(), + false, + ) + .unwrap(); env.block.time = env .block @@ -298,6 +332,7 @@ mod tests { .plus_seconds(TimeConfiguration::default().verification_key_validation_time_secs); advance_epoch_state(deps.as_mut(), env).unwrap(); - try_verify_verification_key_share(deps.as_mut(), multisig_info, owner.clone()).unwrap(); + try_verify_verification_key_share(deps.as_mut(), multisig_info, owner.clone(), false) + .unwrap(); } } diff --git a/contracts/coconut-test/src/submit_vk_creates_proposal.rs b/contracts/coconut-test/src/submit_vk_creates_proposal.rs index c2f09d4bb2..884637f18c 100644 --- a/contracts/coconut-test/src/submit_vk_creates_proposal.rs +++ b/contracts/coconut-test/src/submit_vk_creates_proposal.rs @@ -103,6 +103,7 @@ fn dkg_proposal() { &RegisterDealer { bte_key_with_proof: "bte_key_with_proof".to_string(), announce_address: "127.0.0.1:8000".to_string(), + resharing: false, }, &vec![], ) @@ -124,6 +125,7 @@ fn dkg_proposal() { let msg = CommitVerificationKeyShare { share: "share".to_string(), + resharing: false, }; let res = app .execute_contract( diff --git a/nym-api/src/coconut/client.rs b/nym-api/src/coconut/client.rs index a80c6716ba..28b51fd252 100644 --- a/nym-api/src/coconut/client.rs +++ b/nym-api/src/coconut/client.rs @@ -4,7 +4,9 @@ use crate::coconut::error::Result; use coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; -use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, Epoch, EpochId}; +use coconut_dkg_common::types::{ + EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, +}; use coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use contracts_common::dealings::ContractSafeBytes; use cw3::ProposalResponse; @@ -26,6 +28,7 @@ pub trait Client { 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>; @@ -38,10 +41,16 @@ pub trait Client { &self, bte_key: EncodedBTEPublicKeyWithProof, announce_address: String, + resharing: bool, + ) -> Result; + async fn submit_dealing( + &self, + dealing_bytes: ContractSafeBytes, + resharing: bool, ) -> Result; - async fn submit_dealing(&self, dealing_bytes: ContractSafeBytes) -> Result; async fn submit_verification_key_share( &self, share: VerificationKeyShare, + resharing: bool, ) -> Result; } diff --git a/nym-api/src/coconut/dkg/client.rs b/nym-api/src/coconut/dkg/client.rs index 53f462c0de..6768622b87 100644 --- a/nym-api/src/coconut/dkg/client.rs +++ b/nym-api/src/coconut/dkg/client.rs @@ -4,7 +4,9 @@ use crate::coconut::client::Client; use crate::coconut::error::CoconutError; use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; -use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, Epoch, EpochId, NodeIndex}; +use coconut_dkg_common::types::{ + EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, NodeIndex, +}; use coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use contracts_common::dealings::ContractSafeBytes; use cw3::ProposalResponse; @@ -59,6 +61,12 @@ impl DkgClient { self.inner.get_current_epoch_threshold().await } + pub(crate) async fn get_initial_dealers( + &self, + ) -> Result, CoconutError> { + self.inner.get_initial_dealers().await + } + pub(crate) async fn get_self_registered_dealer_details( &self, ) -> Result { @@ -102,10 +110,11 @@ impl DkgClient { &self, bte_key: EncodedBTEPublicKeyWithProof, announce_address: String, + resharing: bool, ) -> Result { let res = self .inner - .register_dealer(bte_key, announce_address) + .register_dealer(bte_key, announce_address, resharing) .await?; let node_index = find_attribute(&res.logs, "wasm", NODE_INDEX) .ok_or(CoconutError::NodeIndexRecoveryError { @@ -123,18 +132,20 @@ impl DkgClient { pub(crate) async fn submit_dealing( &self, dealing_bytes: ContractSafeBytes, + resharing: bool, ) -> Result<(), CoconutError> { - self.inner.submit_dealing(dealing_bytes).await?; + self.inner.submit_dealing(dealing_bytes, resharing).await?; Ok(()) } pub(crate) async fn submit_verification_key_share( &self, share: VerificationKeyShare, + resharing: bool, ) -> Result { let mut ret = self .inner - .submit_verification_key_share(share.clone()) + .submit_verification_key_share(share.clone(), resharing) .await; for _ in 0..Self::RETRIES { if let Ok(res) = ret { @@ -142,7 +153,7 @@ impl DkgClient { } ret = self .inner - .submit_verification_key_share(share.clone()) + .submit_verification_key_share(share.clone(), resharing) .await; } ret diff --git a/nym-api/src/coconut/dkg/controller.rs b/nym-api/src/coconut/dkg/controller.rs index 7329a2b62c..9c926a7d27 100644 --- a/nym-api/src/coconut/dkg/controller.rs +++ b/nym-api/src/coconut/dkg/controller.rs @@ -101,13 +101,19 @@ impl DkgController { return; } let ret = match epoch.state { - EpochState::PublicKeySubmission => { - public_key_submission(&self.dkg_client, &mut self.state).await + EpochState::PublicKeySubmission { resharing } => { + public_key_submission(&self.dkg_client, &mut self.state, resharing).await } - EpochState::DealingExchange => { - dealing_exchange(&self.dkg_client, &mut self.state, self.rng.clone()).await + EpochState::DealingExchange { resharing } => { + dealing_exchange( + &self.dkg_client, + &mut self.state, + self.rng.clone(), + resharing, + ) + .await } - EpochState::VerificationKeySubmission => { + EpochState::VerificationKeySubmission { resharing } => { let keypair_path = pemstore::KeyPairPath::new( self.secret_key_path.clone(), self.verification_key_path.clone(), @@ -116,14 +122,17 @@ impl DkgController { &self.dkg_client, &mut self.state, &keypair_path, + resharing, ) .await } - EpochState::VerificationKeyValidation => { - verification_key_validation(&self.dkg_client, &mut self.state).await + EpochState::VerificationKeyValidation { resharing } => { + verification_key_validation(&self.dkg_client, &mut self.state, resharing) + .await } - EpochState::VerificationKeyFinalization => { - verification_key_finalization(&self.dkg_client, &mut self.state).await + EpochState::VerificationKeyFinalization { resharing } => { + verification_key_finalization(&self.dkg_client, &mut self.state, resharing) + .await } // Just wait, in case we need to redo dkg at some point EpochState::InProgress => { diff --git a/nym-api/src/coconut/dkg/dealing.rs b/nym-api/src/coconut/dkg/dealing.rs index 9e7fe3c74a..85ed717ac4 100644 --- a/nym-api/src/coconut/dkg/dealing.rs +++ b/nym-api/src/coconut/dkg/dealing.rs @@ -9,11 +9,13 @@ use contracts_common::dealings::ContractSafeBytes; use dkg::bte::setup; use dkg::Dealing; use rand::RngCore; +use std::collections::VecDeque; pub(crate) async fn dealing_exchange( dkg_client: &DkgClient, state: &mut State, rng: impl RngCore + Clone, + resharing: bool, ) -> Result<(), CoconutError> { if state.receiver_index().is_some() { return Ok(()); @@ -21,26 +23,54 @@ pub(crate) async fn dealing_exchange( let dealers = dkg_client.get_current_dealers().await?; let threshold = dkg_client.get_current_epoch_threshold().await?; + let initial_dealers = dkg_client + .get_initial_dealers() + .await? + .map(|d| d.initial_dealers) + .unwrap_or_default(); + let own_address = dkg_client.get_address().await.as_ref().to_string(); state.set_dealers(dealers); state.set_threshold(threshold); let receivers = state.current_dealers_by_idx(); - let params = setup(); let dealer_index = state.node_index_value()?; let receiver_index = receivers .keys() .position(|node_index| *node_index == dealer_index); - for _ in 0..TOTAL_DEALINGS { - let (dealing, _) = Dealing::create( - rng.clone(), - ¶ms, - dealer_index, - state.threshold()?, - &receivers, - None, - ); - dkg_client - .submit_dealing(ContractSafeBytes::from(&dealing)) - .await?; + + let prior_resharing_secrets = if let Some(sk) = state.coconut_secret_key().await { + // Double check that we are in resharing mode + if resharing { + let (x, mut scalars) = sk.into_raw(); + if scalars.len() + 1 != TOTAL_DEALINGS { + return Err(CoconutError::CorruptedCoconutKeyPair); + } + // We can now erase the keypair from memory + state.set_coconut_keypair(None).await; + scalars.push(x); + scalars + } else { + log::warn!("Coconut key hasn't been reset in memory. The state might be corrupt"); + vec![] + } + } else { + vec![] + }; + let mut prior_resharing_secrets = VecDeque::from(prior_resharing_secrets); + if !resharing || initial_dealers.iter().any(|d| *d == own_address) { + let params = setup(); + for _ in 0..TOTAL_DEALINGS { + let (dealing, _) = Dealing::create( + rng.clone(), + ¶ms, + dealer_index, + state.threshold()?, + &receivers, + prior_resharing_secrets.pop_front(), + ); + dkg_client + .submit_dealing(ContractSafeBytes::from(&dealing), resharing) + .await?; + } } info!("DKG: Finished submitting dealing"); @@ -57,9 +87,11 @@ pub(crate) mod tests { use crate::coconut::tests::DummyClient; use crate::coconut::KeyPair; use coconut_dkg_common::dealer::DealerDetails; + use coconut_dkg_common::types::InitialReplacementData; use cosmwasm_std::Addr; use dkg::bte::keys::KeyPair as DkgKeyPair; use dkg::bte::{Params, PublicKeyWithProof}; + use nymcoconut::{ttp_keygen, Parameters}; use rand::rngs::OsRng; use std::collections::HashMap; use std::path::PathBuf; @@ -68,10 +100,11 @@ pub(crate) mod tests { use url::Url; use validator_client::nyxd::AccountId; - const TEST_VALIDATORS_ADDRESS: [&str; 3] = [ + const TEST_VALIDATORS_ADDRESS: [&str; 4] = [ "n1aq9kakfgwqcufr23lsv644apavcntrsqsk4yus", "n1s9l3xr4g0rglvk4yctktmck3h4eq0gp6z2e20v", "n19kl4py32vsk297dm93ezem992cdyzdy4zuc2x6", + "n1jfrs6cmw9t7dv0x8cgny6geunzjh56n2s89fkv", ]; fn insert_dealers( @@ -121,7 +154,7 @@ pub(crate) mod tests { state.set_node_index(Some(self_index)); let keypairs = insert_dealers(¶ms, &dealer_details_db); - dealing_exchange(&dkg_client, &mut state, OsRng) + dealing_exchange(&dkg_client, &mut state, OsRng, false) .await .unwrap(); @@ -142,7 +175,7 @@ pub(crate) mod tests { .clone(); assert_eq!(dealings.len(), TOTAL_DEALINGS); - dealing_exchange(&dkg_client, &mut state, OsRng) + dealing_exchange(&dkg_client, &mut state, OsRng, false) .await .unwrap(); let new_dealings = dealings_db @@ -201,7 +234,7 @@ pub(crate) mod tests { details.bte_public_key_with_proof = bs58::encode(&bytes).into_string(); }); - dealing_exchange(&dkg_client, &mut state, OsRng) + dealing_exchange(&dkg_client, &mut state, OsRng, false) .await .unwrap(); assert_eq!( @@ -214,4 +247,85 @@ pub(crate) mod tests { ComplaintReason::InvalidBTEPublicKey ); } + + #[tokio::test] + #[ignore] // expensive test + async fn resharing_exchange_dealing() { + let self_index = 2; + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let threshold_db = Arc::new(RwLock::new(Some(3))); + let initial_dealers_db = Arc::new(RwLock::new(Some(InitialReplacementData { + initial_dealers: vec![Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])], + initial_height: Some(100), + }))); + let dkg_client = DkgClient::new( + DummyClient::new( + AccountId::from_str("n1vxkywf9g4cg0k2dehanzwzz64jw782qm0kuynf").unwrap(), + ) + .with_dealer_details(&dealer_details_db) + .with_dealings(&dealings_db) + .with_threshold(&threshold_db) + .with_initial_dealers_db(&initial_dealers_db), + ); + let params = setup(); + 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; + + let mut state = State::new( + PathBuf::default(), + PersistentState::default(), + Url::parse("localhost:8000").unwrap(), + DkgKeyPair::new(¶ms, OsRng), + coconut_keypair.clone(), + ); + state.set_node_index(Some(self_index)); + let keypairs = insert_dealers(¶ms, &dealer_details_db); + + dealing_exchange(&dkg_client, &mut state, OsRng, true) + .await + .unwrap(); + + assert_eq!( + state.current_dealers_by_idx().values().collect::>(), + keypairs + .iter() + .map(|k| k.public_key().public_key()) + .collect::>() + ); + 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 mut state = State::new( + PathBuf::default(), + PersistentState::default(), + Url::parse("localhost:8000").unwrap(), + DkgKeyPair::new(¶ms, OsRng), + coconut_keypair, + ); + state.set_node_index(Some(self_index)); + // Use a client that is in the initial dealers set + let dkg_client = DkgClient::new( + DummyClient::new(AccountId::from_str(TEST_VALIDATORS_ADDRESS[0]).unwrap()) + .with_dealer_details(&dealer_details_db) + .with_dealings(&dealings_db) + .with_threshold(&threshold_db) + .with_initial_dealers_db(&initial_dealers_db), + ); + + dealing_exchange(&dkg_client, &mut state, OsRng, true) + .await + .unwrap(); + + let dealings = dealings_db + .read() + .unwrap() + .get(TEST_VALIDATORS_ADDRESS[0]) + .unwrap() + .clone(); + assert_eq!(dealings.len(), TOTAL_DEALINGS); + } } diff --git a/nym-api/src/coconut/dkg/public_key.rs b/nym-api/src/coconut/dkg/public_key.rs index 67fd0ccce2..ed27542baa 100644 --- a/nym-api/src/coconut/dkg/public_key.rs +++ b/nym-api/src/coconut/dkg/public_key.rs @@ -9,9 +9,10 @@ use coconut_dkg_common::dealer::DealerType; pub(crate) async fn public_key_submission( dkg_client: &DkgClient, state: &mut State, + resharing: bool, ) -> Result<(), CoconutError> { if state.was_in_progress() { - state.reset_persistent().await; + state.reset_persistent(resharing).await; } if state.node_index().is_some() { return Ok(()); @@ -23,14 +24,14 @@ pub(crate) async fn public_key_submission( if dealer_details.dealer_type == DealerType::Past { // If it was a dealer in a previous epoch, re-register it for this epoch dkg_client - .register_dealer(bte_key, state.announce_address().to_string()) + .register_dealer(bte_key, state.announce_address().to_string(), resharing) .await?; } details.assigned_index } else { // First time registration dkg_client - .register_dealer(bte_key, state.announce_address().to_string()) + .register_dealer(bte_key, state.announce_address().to_string(), resharing) .await? }; state.set_node_index(Some(index)); @@ -74,7 +75,7 @@ pub(crate) mod tests { .unwrap() .details .is_none()); - public_key_submission(&dkg_client, &mut state) + public_key_submission(&dkg_client, &mut state, false) .await .unwrap(); let client_idx = dkg_client @@ -88,7 +89,7 @@ pub(crate) mod tests { // keeps the same index from chain, not calling register_dealer again state.set_node_index(None); - public_key_submission(&dkg_client, &mut state) + public_key_submission(&dkg_client, &mut state, false) .await .unwrap(); assert_eq!(state.node_index().unwrap(), client_idx); diff --git a/nym-api/src/coconut/dkg/state.rs b/nym-api/src/coconut/dkg/state.rs index 7a2c46c526..6277eb6ccf 100644 --- a/nym-api/src/coconut/dkg/state.rs +++ b/nym-api/src/coconut/dkg/state.rs @@ -9,6 +9,7 @@ use coconut_dkg_common::types::EpochState; use cosmwasm_std::Addr; use dkg::bte::{keys::KeyPair as DkgKeyPair, PublicKey, PublicKeyWithProof}; use dkg::{NodeIndex, RecoveredVerificationKeys, Threshold}; +use nymcoconut::SecretKey; use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::collections::BTreeMap; @@ -71,18 +72,18 @@ pub(crate) trait ConsistentState { fn proposal_id_value(&self) -> Result; async fn is_consistent(&self, epoch_state: EpochState) -> Result<(), CoconutError> { match epoch_state { - EpochState::PublicKeySubmission => {} - EpochState::DealingExchange => { + EpochState::PublicKeySubmission { .. } => {} + EpochState::DealingExchange { .. } => { self.node_index_value()?; } - EpochState::VerificationKeySubmission => { + EpochState::VerificationKeySubmission { .. } => { self.receiver_index_value()?; self.threshold()?; } - EpochState::VerificationKeyValidation => { + EpochState::VerificationKeyValidation { .. } => { self.coconut_keypair_is_some().await?; } - EpochState::VerificationKeyFinalization => { + EpochState::VerificationKeyFinalization { .. } => { self.proposal_id_value()?; } EpochState::InProgress => {} @@ -241,8 +242,10 @@ impl State { } } - pub async fn reset_persistent(&mut self) { - self.coconut_keypair.set(None).await; + pub async fn reset_persistent(&mut self, resharing: bool) { + if !resharing { + self.coconut_keypair.set(None).await; + } self.node_index = Default::default(); self.dealers = Default::default(); self.receiver_index = Default::default(); @@ -270,6 +273,14 @@ impl State { self.coconut_keypair.get().await.is_some() } + pub async fn coconut_secret_key(&self) -> Option { + self.coconut_keypair + .get() + .await + .as_ref() + .map(|kp| kp.secret_key()) + } + pub fn node_index(&self) -> Option { self.node_index } @@ -324,8 +335,11 @@ impl State { self.recovered_vks = recovered_vks; } - pub async fn set_coconut_keypair(&mut self, coconut_keypair: coconut_interface::KeyPair) { - self.coconut_keypair.set(Some(coconut_keypair)).await + pub async fn set_coconut_keypair( + &mut self, + coconut_keypair: Option, + ) { + self.coconut_keypair.set(coconut_keypair).await } pub fn set_node_index(&mut self, node_index: Option) { diff --git a/nym-api/src/coconut/dkg/verification_key.rs b/nym-api/src/coconut/dkg/verification_key.rs index 9aed655cfa..892162dced 100644 --- a/nym-api/src/coconut/dkg/verification_key.rs +++ b/nym-api/src/coconut/dkg/verification_key.rs @@ -14,6 +14,7 @@ use cosmwasm_std::Addr; use credentials::coconut::bandwidth::{PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES}; use cw3::{ProposalResponse, Status}; use dkg::bte::{decrypt_share, setup}; +use dkg::error::DkgError; use dkg::{combine_shares, try_recover_verification_keys, Dealing, Threshold}; use nymcoconut::tests::helpers::transpose_matrix; use nymcoconut::{check_vk_pairing, Base58, KeyPair, Parameters, SecretKey, VerificationKey}; @@ -26,10 +27,21 @@ async fn deterministic_filter_dealers( dkg_client: &DkgClient, state: &mut State, threshold: Threshold, + resharing: bool, ) -> Result>, CoconutError> { let mut dealings_maps = vec![]; 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 = setup(); for idx in 0..TOTAL_DEALINGS { @@ -66,11 +78,15 @@ async fn deterministic_filter_dealers( })); dealings_maps.push(dealings_map); } + for (addr, _) in initial_dealers_by_addr.iter() { - for dealings_map in dealings_maps.iter() { - if !dealings_map.iter().any(|(_, (address, _))| address == addr) { - state.mark_bad_dealer(addr, ComplaintReason::MissingDealing); - break; + // 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; + } } } } @@ -90,16 +106,16 @@ fn derive_partial_keypair( let mut scalars = vec![]; let mut recovered_vks = vec![]; for dealings_map in dealings_maps.into_iter() { - let filtered_dealings: Vec<_> = dealings_map + let (filtered_dealers, filtered_dealings): (Vec<_>, Vec<_>) = dealings_map .into_iter() - .filter_map(|(_, (addr, dealing))| { + .filter_map(|(idx, (addr, dealing))| { if filtered_dealers_by_addr.keys().any(|a| addr == *a) { - Some(dealing) + Some((idx, dealing)) } else { None } }) - .collect(); + .unzip(); let recovered = try_recover_verification_keys( &filtered_dealings, threshold, @@ -111,19 +127,18 @@ fn derive_partial_keypair( .iter() .map(|dealing| decrypt_share(dk, node_index_value, &dealing.ciphertexts, None)) .collect::>()?; - let scalar = combine_shares( - shares, - &filtered_receivers_by_idx - .keys() - .copied() - .collect::>(), - )?; + let scalar = combine_shares(shares, &filtered_dealers)?; scalars.push(scalar); } state.set_recovered_vks(recovered_vks); let params = Parameters::new(PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES)?; - let x = scalars.pop().unwrap(); + 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(¶ms); @@ -134,17 +149,21 @@ pub(crate) async fn verification_key_submission( dkg_client: &DkgClient, state: &mut State, keypair_path: &KeyPairPath, + resharing: bool, ) -> Result<(), CoconutError> { if state.coconut_keypair_is_some().await { return Ok(()); } let threshold = state.threshold()?; - let dealings_maps = deterministic_filter_dealers(dkg_client, state, threshold).await?; + let dealings_maps = + deterministic_filter_dealers(dkg_client, state, threshold, resharing).await?; let coconut_keypair = derive_partial_keypair(state, threshold, dealings_maps)?; let vk_share = coconut_keypair.verification_key().to_bs58(); pemstore::store_keypair(&coconut_keypair, keypair_path)?; - let res = dkg_client.submit_verification_key_share(vk_share).await?; + let res = dkg_client + .submit_verification_key_share(vk_share, resharing) + .await?; let proposal_id = find_attribute(&res.logs, "wasm", DKG_PROPOSAL_ID) .ok_or(CoconutError::ProposalIdError { reason: String::from("proposal id not found"), @@ -155,7 +174,7 @@ pub(crate) async fn verification_key_submission( reason: String::from("proposal id could not be parsed to u64"), })?; state.set_proposal_id(proposal_id); - state.set_coconut_keypair(coconut_keypair).await; + state.set_coconut_keypair(Some(coconut_keypair)).await; info!("DKG: Submitted own verification key"); Ok(()) @@ -173,6 +192,7 @@ fn validate_proposal(proposal: &ProposalResponse) -> Option<(Addr, u64)> { pub(crate) async fn verification_key_validation( dkg_client: &DkgClient, state: &mut State, + _resharing: bool, ) -> Result<(), CoconutError> { if state.voted_vks() { return Ok(()); @@ -233,6 +253,7 @@ pub(crate) async fn verification_key_validation( pub(crate) async fn verification_key_finalization( dkg_client: &DkgClient, state: &mut State, + _resharing: bool, ) -> Result<(), CoconutError> { if state.executed_proposal() { return Ok(()); @@ -257,9 +278,11 @@ pub(crate) mod tests { use crate::coconut::tests::DummyClient; use crate::coconut::KeyPair; use coconut_dkg_common::dealer::DealerDetails; + use coconut_dkg_common::types::InitialReplacementData; use coconut_dkg_common::verification_key::ContractVKShare; use contracts_common::dealings::ContractSafeBytes; use dkg::bte::keys::KeyPair as DkgKeyPair; + use nymcoconut::aggregate_verification_keys; use rand::rngs::OsRng; use rand::Rng; use std::collections::HashMap; @@ -276,6 +299,7 @@ pub(crate) mod tests { proposal_db: Arc>>, verification_share_db: Arc>>, threshold_db: Arc>>, + initial_dealers_db: Arc>>, } impl MockContractDb { @@ -286,6 +310,7 @@ pub(crate) mod tests { proposal_db: Arc::new(Default::default()), verification_share_db: Arc::new(Default::default()), threshold_db: Arc::new(RwLock::new(Some(2))), + initial_dealers_db: Arc::new(RwLock::new(Default::default())), } } } @@ -307,7 +332,8 @@ pub(crate) mod tests { .with_dealings(&db.dealings_db) .with_proposal_db(&db.proposal_db) .with_verification_share(&db.verification_share_db) - .with_threshold(&db.threshold_db), + .with_threshold(&db.threshold_db) + .with_initial_dealers_db(&db.initial_dealers_db), ); let keypair = DkgKeyPair::new(¶ms, OsRng); let state = State::new( @@ -320,10 +346,21 @@ pub(crate) mod tests { clients_and_states.push((dkg_client, state)); } for (dkg_client, state) in clients_and_states.iter_mut() { - public_key_submission(dkg_client, state).await.unwrap(); + public_key_submission(dkg_client, state, false) + .await + .unwrap(); } + clients_and_states + } + + async fn prepare_clients_and_states_with_dealing( + db: &MockContractDb, + ) -> Vec<(DkgClient, State)> { + let mut clients_and_states = prepare_clients_and_states(db).await; for (dkg_client, state) in clients_and_states.iter_mut() { - dealing_exchange(dkg_client, state, OsRng).await.unwrap(); + dealing_exchange(dkg_client, state, OsRng, false) + .await + .unwrap(); } clients_and_states } @@ -331,13 +368,13 @@ pub(crate) mod tests { async fn prepare_clients_and_states_with_submission( db: &MockContractDb, ) -> Vec<(DkgClient, State)> { - let mut clients_and_states = prepare_clients_and_states(db).await; + let mut clients_and_states = prepare_clients_and_states_with_dealing(db).await; for (dkg_client, state) in clients_and_states.iter_mut() { let random_file: usize = OsRng.gen(); 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) + verification_key_submission(dkg_client, state, &keypair_path, false) .await .unwrap(); std::fs::remove_file(private_key_path).unwrap(); @@ -351,7 +388,7 @@ pub(crate) mod tests { ) -> Vec<(DkgClient, State)> { let mut clients_and_states = prepare_clients_and_states_with_submission(db).await; for (dkg_client, state) in clients_and_states.iter_mut() { - verification_key_validation(dkg_client, state) + verification_key_validation(dkg_client, state, false) .await .unwrap(); } @@ -363,7 +400,7 @@ pub(crate) mod tests { ) -> Vec<(DkgClient, State)> { let mut clients_and_states = prepare_clients_and_states_with_validation(db).await; for (dkg_client, state) in clients_and_states.iter_mut() { - verification_key_finalization(dkg_client, state) + verification_key_finalization(dkg_client, state, false) .await .unwrap(); } @@ -374,9 +411,9 @@ pub(crate) mod tests { #[ignore] // expensive test async fn check_dealers_filter_all_good() { let db = MockContractDb::new(); - let mut clients_and_states = prepare_clients_and_states(&db).await; + 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) + let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) .await .unwrap(); assert_eq!(filtered.len(), TOTAL_DEALINGS); @@ -390,7 +427,7 @@ pub(crate) mod tests { #[ignore] // expensive test async fn check_dealers_filter_one_bad_dealing() { let db = MockContractDb::new(); - let mut clients_and_states = prepare_clients_and_states(&db).await; + let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await; // corrupt just one dealing db.dealings_db @@ -404,7 +441,7 @@ pub(crate) mod tests { }); for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { - let filtered = deterministic_filter_dealers(dkg_client, state, 2) + let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) .await .unwrap(); assert_eq!(filtered.len(), TOTAL_DEALINGS); @@ -420,10 +457,74 @@ pub(crate) mod tests { #[tokio::test] #[ignore] // expensive test - async fn check_dealers_filter_all_bad_dealings() { + async fn check_dealers_resharing_filter_one_missing_dealing() { let db = MockContractDb::new(); let mut clients_and_states = prepare_clients_and_states(&db).await; + // add all but the first dealing + for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { + dealing_exchange(dkg_client, state, OsRng, true) + .await + .unwrap(); + } + + for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { + *db.initial_dealers_db.write().unwrap() = Some(InitialReplacementData { + initial_dealers: vec![Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])], + initial_height: None, + }); + let filtered = deterministic_filter_dealers(dkg_client, state, 2, true) + .await + .unwrap(); + assert_eq!(filtered.len(), TOTAL_DEALINGS); + let corrupted_status = state + .all_dealers() + .get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])) + .unwrap() + .as_ref() + .unwrap_err(); + + assert_eq!(*corrupted_status, ComplaintReason::MissingDealing); + } + } + + #[tokio::test] + #[ignore] // expensive test + 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; + + // add all but the first dealing + for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { + dealing_exchange(dkg_client, state, OsRng, true) + .await + .unwrap(); + } + + for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { + *db.initial_dealers_db.write().unwrap() = Some(InitialReplacementData { + initial_dealers: vec![], + initial_height: None, + }); + let filtered = deterministic_filter_dealers(dkg_client, state, 2, true) + .await + .unwrap(); + assert_eq!(filtered.len(), TOTAL_DEALINGS); + assert!(state + .all_dealers() + .get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])) + .unwrap() + .as_ref() + .is_ok(),); + } + } + + #[tokio::test] + #[ignore] // expensive test + 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; + // corrupt all dealings of one address db.dealings_db .write() @@ -436,7 +537,7 @@ pub(crate) mod tests { }); for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { - let filtered = deterministic_filter_dealers(dkg_client, state, 2) + let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) .await .unwrap(); assert_eq!(filtered.len(), TOTAL_DEALINGS); @@ -457,7 +558,7 @@ pub(crate) mod tests { #[ignore] // expensive test async fn check_dealers_filter_malformed_dealing() { let db = MockContractDb::new(); - let mut clients_and_states = prepare_clients_and_states(&db).await; + let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await; // corrupt just one dealing db.dealings_db @@ -471,12 +572,12 @@ pub(crate) mod tests { }); for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { - deterministic_filter_dealers(dkg_client, state, 2) + deterministic_filter_dealers(dkg_client, state, 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) + let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) .await .unwrap(); assert_eq!(filtered.len(), TOTAL_DEALINGS); @@ -494,7 +595,7 @@ pub(crate) mod tests { #[ignore] // expensive test async fn check_dealers_filter_dealing_verification_error() { let db = MockContractDb::new(); - let mut clients_and_states = prepare_clients_and_states(&db).await; + let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await; // corrupt just one dealing db.dealings_db @@ -513,12 +614,12 @@ pub(crate) mod tests { }); for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { - deterministic_filter_dealers(dkg_client, state, 2) + deterministic_filter_dealers(dkg_client, state, 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) + let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) .await .unwrap(); assert_eq!(filtered.len(), TOTAL_DEALINGS); @@ -536,9 +637,9 @@ pub(crate) mod tests { #[ignore] // expensive test async fn partial_keypair_derivation() { let db = MockContractDb::new(); - let mut clients_and_states = prepare_clients_and_states(&db).await; + 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) + let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) .await .unwrap(); assert!(derive_partial_keypair(state, 2, filtered).is_ok()); @@ -549,7 +650,7 @@ pub(crate) mod tests { #[ignore] // expensive test async fn partial_keypair_derivation_with_threshold() { let db = MockContractDb::new(); - let mut clients_and_states = prepare_clients_and_states(&db).await; + let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await; // corrupt just one dealing db.dealings_db @@ -563,7 +664,7 @@ pub(crate) mod tests { }); for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { - let filtered = deterministic_filter_dealers(dkg_client, state, 2) + let filtered = deterministic_filter_dealers(dkg_client, state, 2, false) .await .unwrap(); assert!(derive_partial_keypair(state, 2, filtered).is_ok()); @@ -616,7 +717,7 @@ pub(crate) mod tests { .and_modify(|share| share.share.push('x')); for (dkg_client, state) in clients_and_states.iter_mut() { - verification_key_validation(dkg_client, state) + verification_key_validation(dkg_client, state, false) .await .unwrap(); } @@ -658,7 +759,7 @@ pub(crate) mod tests { .and_modify(|share| share.share = second_share); for (dkg_client, state) in clients_and_states.iter_mut() { - verification_key_validation(dkg_client, state) + verification_key_validation(dkg_client, state, false) .await .unwrap(); } @@ -696,4 +797,102 @@ pub(crate) mod tests { assert_eq!(proposal.status, Status::Executed); } } + + #[tokio::test] + #[ignore] // expensive test + async fn reshare_preserves_keys() { + let db = MockContractDb::new(); + let mut clients_and_states = prepare_clients_and_states_with_finalization(&db).await; + let params = Parameters::new(4).unwrap(); + + let mut vks = vec![]; + let mut indices = vec![]; + for (_, state) in clients_and_states.iter() { + let vk = state + .coconut_secret_key() + .await + .unwrap() + .verification_key(¶ms); + let index = state.node_index().unwrap(); + vks.push(vk); + indices.push(index); + } + let initial_master_vk = aggregate_verification_keys(&vks, Some(&indices)).unwrap(); + + let new_dkg_client = DkgClient::new( + DummyClient::new( + AccountId::from_str("n1sqkxzh7nl6kgndr4ew9795t2nkwmd8tpql67q7").unwrap(), + ) + .with_dealer_details(&db.dealer_details_db) + .with_dealings(&db.dealings_db) + .with_proposal_db(&db.proposal_db) + .with_verification_share(&db.verification_share_db) + .with_threshold(&db.threshold_db) + .with_initial_dealers_db(&db.initial_dealers_db), + ); + let keypair = DkgKeyPair::new(&setup(), OsRng); + let state = State::new( + PathBuf::default(), + PersistentState::default(), + Url::parse("localhost:8000").unwrap(), + keypair, + KeyPair::new(), + ); + + let removed_dealer = clients_and_states.first().unwrap().0.get_address().await; + db.dealer_details_db + .write() + .unwrap() + .remove(removed_dealer.as_ref()); + *db.dealings_db.write().unwrap() = Default::default(); + *db.verification_share_db.write().unwrap() = Default::default(); + *db.initial_dealers_db.write().unwrap() = Some(InitialReplacementData { + initial_dealers: vec![ + Addr::unchecked(clients_and_states[1].0.get_address().await.as_ref()), + Addr::unchecked(clients_and_states[2].0.get_address().await.as_ref()), + ], + initial_height: None, + }); + *clients_and_states.first_mut().unwrap() = (new_dkg_client, state); + clients_and_states[1].1.set_was_in_progress(); + clients_and_states[2].1.set_was_in_progress(); + + for (dkg_client, state) in clients_and_states.iter_mut() { + public_key_submission(dkg_client, state, true) + .await + .unwrap(); + } + + for (dkg_client, state) in clients_and_states.iter_mut() { + dealing_exchange(dkg_client, state, OsRng, true) + .await + .unwrap(); + } + + for (dkg_client, state) in clients_and_states.iter_mut() { + let random_file: usize = OsRng.gen(); + 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) + .await + .unwrap(); + std::fs::remove_file(private_key_path).unwrap(); + std::fs::remove_file(public_key_path).unwrap(); + } + let mut vks = vec![]; + let mut indices = vec![]; + for (_, state) in clients_and_states.iter() { + let vk = state + .coconut_secret_key() + .await + .unwrap() + .verification_key(¶ms); + let index = state.node_index().unwrap(); + vks.push(vk); + indices.push(index); + } + let reshared_master_vk = aggregate_verification_keys(&vks, Some(&indices)).unwrap(); + assert_eq!(initial_master_vk, reshared_master_vk); + } } diff --git a/nym-api/src/coconut/error.rs b/nym-api/src/coconut/error.rs index dc484e691e..b283532bd2 100644 --- a/nym-api/src/coconut/error.rs +++ b/nym-api/src/coconut/error.rs @@ -100,6 +100,9 @@ pub enum CoconutError { #[error("DKG has not finished yet in order to derive the coconut key")] KeyPairNotDerivedYet, + #[error("The coconut keypair is corrupted")] + CorruptedCoconutKeyPair, + #[error("There was a problem with the proposal id: {reason}")] ProposalIdError { reason: String }, } diff --git a/nym-api/src/coconut/tests.rs b/nym-api/src/coconut/tests.rs index 09ea5789ea..9fb52b290f 100644 --- a/nym-api/src/coconut/tests.rs +++ b/nym-api/src/coconut/tests.rs @@ -40,7 +40,9 @@ use coconut_dkg_common::dealer::{ ContractDealing, DealerDetails, DealerDetailsResponse, DealerType, }; use coconut_dkg_common::event_attributes::{DKG_PROPOSAL_ID, NODE_INDEX}; -use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, Epoch, EpochId, TOTAL_DEALINGS}; +use coconut_dkg_common::types::{ + EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, TOTAL_DEALINGS, +}; use coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use contracts_common::dealings::ContractSafeBytes; use crypto::asymmetric::{encryption, identity}; @@ -72,6 +74,8 @@ pub(crate) struct DummyClient { threshold: Arc>>, dealings: Arc>>>, verification_share: Arc>>, + group_db: Arc>>, + initial_dealers_db: Arc>>, } impl DummyClient { @@ -86,6 +90,8 @@ impl DummyClient { threshold: Arc::new(RwLock::new(None)), dealings: Arc::new(RwLock::new(HashMap::new())), verification_share: Arc::new(RwLock::new(HashMap::new())), + group_db: Arc::new(RwLock::new(HashMap::new())), + initial_dealers_db: Arc::new(RwLock::new(None)), } } @@ -143,6 +149,22 @@ impl DummyClient { self.verification_share = Arc::clone(verification_share); self } + + pub fn _with_group_db( + mut self, + group_db: &Arc>>, + ) -> Self { + self.group_db = Arc::clone(group_db); + self + } + + pub fn with_initial_dealers_db( + mut self, + initial_dealers: &Arc>>, + ) -> Self { + self.initial_dealers_db = Arc::clone(initial_dealers); + self + } } #[async_trait] @@ -193,14 +215,24 @@ impl super::client::Client for DummyClient { Ok(*self.epoch.read().unwrap()) } - async fn group_member(&self, _addr: String) -> Result { - todo!() + async fn group_member(&self, addr: String) -> Result { + Ok(self + .group_db + .read() + .unwrap() + .get(&addr) + .cloned() + .unwrap_or(MemberResponse { weight: None })) } async fn get_current_epoch_threshold(&self) -> Result> { Ok(*self.threshold.read().unwrap()) } + async fn get_initial_dealers(&self) -> Result> { + Ok(self.initial_dealers_db.read().unwrap().clone()) + } + async fn get_self_registered_dealer_details(&self) -> Result { Ok(DealerDetailsResponse { details: self @@ -289,17 +321,25 @@ impl super::client::Client for DummyClient { &self, bte_public_key_with_proof: EncodedBTEPublicKeyWithProof, announce_address: String, + _resharing: bool, ) -> Result { - let assigned_index = OsRng.gen(); - self.dealer_details.write().unwrap().insert( - self.validator_address.to_string(), - DealerDetails { - address: Addr::unchecked(self.validator_address.to_string()), - bte_public_key_with_proof, - announce_address, - assigned_index, - }, - ); + let mut dealer_details = self.dealer_details.write().unwrap(); + let assigned_index = + if let Some(details) = dealer_details.get(self.validator_address.as_ref()) { + details.assigned_index + } else { + let assigned_index = OsRng.gen(); + dealer_details.insert( + self.validator_address.to_string(), + DealerDetails { + address: Addr::unchecked(self.validator_address.to_string()), + bte_public_key_with_proof, + announce_address, + assigned_index, + }, + ); + assigned_index + }; Ok(ExecuteResult { logs: vec![Log { msg_index: 0, @@ -312,7 +352,11 @@ impl super::client::Client for DummyClient { }) } - async fn submit_dealing(&self, dealing_bytes: ContractSafeBytes) -> Result { + async fn submit_dealing( + &self, + dealing_bytes: ContractSafeBytes, + _resharing: bool, + ) -> Result { self.dealings .write() .unwrap() @@ -335,6 +379,7 @@ impl super::client::Client for DummyClient { async fn submit_verification_key_share( &self, share: VerificationKeyShare, + resharing: bool, ) -> Result { let dealer_details = self .dealer_details @@ -357,6 +402,7 @@ impl super::client::Client for DummyClient { let proposal_id = OsRng.gen(); let verify_vk_share_req = coconut_dkg_common::msg::ExecuteMsg::VerifyVerificationKeyShare { owner: Addr::unchecked(self.validator_address.as_ref()), + resharing, }; let verify_vk_share_msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: String::new(), diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 438d0ac369..ad5b880f4d 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -7,6 +7,7 @@ use crate::support::config::Config; use anyhow::Result; use async_trait::async_trait; use coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; +use coconut_dkg_common::types::InitialReplacementData; use coconut_dkg_common::{ dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}, types::{EncodedBTEPublicKeyWithProof, Epoch, EpochId}, @@ -338,6 +339,12 @@ impl crate::coconut::client::Client for Client { .await?) } + async fn get_initial_dealers( + &self, + ) -> crate::coconut::error::Result> { + Ok(self.0.read().await.nyxd.get_initial_dealers().await?) + } + async fn get_self_registered_dealer_details( &self, ) -> crate::coconut::error::Result { @@ -413,39 +420,42 @@ impl crate::coconut::client::Client for Client { &self, bte_key: EncodedBTEPublicKeyWithProof, announce_address: String, + resharing: bool, ) -> Result { Ok(self .0 .write() .await .nyxd - .register_dealer(bte_key, announce_address, None) + .register_dealer(bte_key, announce_address, resharing, None) .await?) } async fn submit_dealing( &self, dealing_bytes: ContractSafeBytes, + resharing: bool, ) -> Result { Ok(self .0 .write() .await .nyxd - .submit_dealing_bytes(dealing_bytes, None) + .submit_dealing_bytes(dealing_bytes, resharing, None) .await?) } async fn submit_verification_key_share( &self, share: VerificationKeyShare, + resharing: bool, ) -> crate::coconut::error::Result { Ok(self .0 .write() .await .nyxd - .submit_verification_key_share(share, None) + .submit_verification_key_share(share, resharing, None) .await?) } }