From 5c841de6ae121ef48a4a1ebf9603182139d02bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Tue, 29 Nov 2022 18:32:37 +0200 Subject: [PATCH] Feature/dkg integration tests (#1815) * DKG contract e2e test * Refactor to the same format as other contracts * Vk share tests * State tests * Dealings tests * Dealer tests * Api dkg tests * Fix path to contract after refactor * Fix test target clippy --- .../src/nymd/cosmwasm_client/logs.rs | 4 +- .../contracts-common/src/dealings.rs | 2 +- contracts/Cargo.lock | 3 + contracts/coconut-bandwidth/src/storage.rs | 2 +- contracts/coconut-dkg/Cargo.toml | 6 +- contracts/coconut-dkg/src/contract.rs | 264 +++++++++ contracts/coconut-dkg/src/dealers/queries.rs | 167 +++++- .../coconut-dkg/src/dealers/transactions.rs | 40 +- contracts/coconut-dkg/src/dealings/queries.rs | 156 +++++ .../coconut-dkg/src/dealings/transactions.rs | 64 +- contracts/coconut-dkg/src/epoch_state/mod.rs | 1 + .../coconut-dkg/src/epoch_state/queries.rs | 21 +- .../coconut-dkg/src/epoch_state/storage.rs | 22 - .../src/epoch_state/transactions.rs | 78 +++ .../coconut-dkg/src/epoch_state/utils.rs | 28 +- contracts/coconut-dkg/src/error.rs | 25 +- contracts/coconut-dkg/src/lib.rs | 140 +---- contracts/coconut-dkg/src/support/mod.rs | 5 + .../coconut-dkg/src/support/tests/fixtures.rs | 32 + .../coconut-dkg/src/support/tests/helpers.rs | 27 + .../coconut-dkg/src/support/tests/mod.rs | 5 + .../src/verification_key_shares/queries.rs | 144 +++++ .../verification_key_shares/transactions.rs | 141 +++++ contracts/coconut-test/Cargo.toml | 1 + contracts/coconut-test/src/helpers.rs | 6 +- .../src/submit_vk_creates_proposal.rs | 72 ++- validator-api/src/coconut/dkg/complaints.rs | 7 +- validator-api/src/coconut/dkg/controller.rs | 2 +- validator-api/src/coconut/dkg/dealing.rs | 98 ++++ validator-api/src/coconut/dkg/public_key.rs | 53 +- validator-api/src/coconut/dkg/state.rs | 9 +- .../src/coconut/dkg/verification_key.rs | 554 +++++++++++++++++- validator-api/src/coconut/mod.rs | 2 +- validator-api/src/coconut/tests.rs | 296 ++++++++-- 34 files changed, 2194 insertions(+), 283 deletions(-) create mode 100644 contracts/coconut-dkg/src/contract.rs create mode 100644 contracts/coconut-dkg/src/epoch_state/transactions.rs create mode 100644 contracts/coconut-dkg/src/support/mod.rs create mode 100644 contracts/coconut-dkg/src/support/tests/fixtures.rs create mode 100644 contracts/coconut-dkg/src/support/tests/helpers.rs create mode 100644 contracts/coconut-dkg/src/support/tests/mod.rs diff --git a/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs index 6c68c306f6..f08903b99c 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs @@ -15,12 +15,12 @@ pub use coconut_dkg_common::event_attributes::*; pub struct Log { #[serde(default)] // weird thing is that the first msg_index seems to always be undefined on the raw logs - msg_index: usize, + pub msg_index: usize, // unless I'm missing something obvious, the "log" type in cosmjs is always an empty string // and launchpad cosmos validator was setting it to what essentially is just the raw version of what // we received (and we don't care about launchpad, we, as the time of writing this, work on the stargate) // log: String, - events: Vec, + pub events: Vec, } /// Searches in logs for the first event of the given event type and in that event diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs b/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs index 8beedd4a42..98e587737e 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs @@ -15,7 +15,7 @@ pub const MAX_DISPLAY_SIZE: usize = 128; // TODO: if we are to use this for different types, it might make sense to introduce something like // CommitmentTypeId field on the below for distinguishing different ones. it would somehow become part of the trait #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, JsonSchema)] -pub struct ContractSafeBytes(Vec); +pub struct ContractSafeBytes(pub Vec); impl Deref for ContractSafeBytes { type Target = Vec; diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index d2fc273d22..9da3d72397 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -197,8 +197,10 @@ dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-controllers", + "cw-multi-test", "cw-storage-plus", "cw4", + "cw4-group", "schemars", "serde", "thiserror", @@ -231,6 +233,7 @@ dependencies = [ "cw-multi-test", "cw-storage-plus", "cw-utils", + "cw3", "cw3-flex-multisig", "cw4", "cw4-group", diff --git a/contracts/coconut-bandwidth/src/storage.rs b/contracts/coconut-bandwidth/src/storage.rs index 40e33b9f8d..549e64caaf 100644 --- a/contracts/coconut-bandwidth/src/storage.rs +++ b/contracts/coconut-bandwidth/src/storage.rs @@ -25,7 +25,7 @@ impl<'a> IndexList for SpendCredentialIndex<'a> { } } -// gateways() is the storage access function. +// spent_credentials() is the storage access function. pub(crate) fn spent_credentials<'a>( ) -> IndexedMap<'a, &'a str, SpendCredential, SpendCredentialIndex<'a>> { let indexes = SpendCredentialIndex { diff --git a/contracts/coconut-dkg/Cargo.toml b/contracts/coconut-dkg/Cargo.toml index 479187c370..fe4440cf2f 100644 --- a/contracts/coconut-dkg/Cargo.toml +++ b/contracts/coconut-dkg/Cargo.toml @@ -19,4 +19,8 @@ cw4 = { version = "0.13.4" } schemars = "0.8" serde = { version = "1.0.103", default-features = false, features = ["derive"] } -thiserror = "1.0.23" \ No newline at end of file +thiserror = "1.0.23" + +[dev-dependencies] +cw-multi-test = { version = "0.13.4" } +cw4-group = { path = "../multisig/cw4-group" } \ No newline at end of file diff --git a/contracts/coconut-dkg/src/contract.rs b/contracts/coconut-dkg/src/contract.rs new file mode 100644 index 0000000000..283a49ad1b --- /dev/null +++ b/contracts/coconut-dkg/src/contract.rs @@ -0,0 +1,264 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::dealers::queries::{ + query_current_dealers_paged, query_dealer_details, query_past_dealers_paged, +}; +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_state; +use crate::epoch_state::storage::CURRENT_EPOCH_STATE; +use crate::epoch_state::transactions::advance_epoch_state; +use crate::error::ContractError; +use crate::state::{State, ADMIN, MULTISIG, STATE}; +use crate::verification_key_shares::queries::query_vk_shares_paged; +use crate::verification_key_shares::transactions::try_commit_verification_key_share; +use crate::verification_key_shares::transactions::try_verify_verification_key_share; +use coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use coconut_dkg_common::types::EpochState; +use cosmwasm_std::{ + entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, +}; +use cw4::Cw4Contract; + +/// Instantiate the contract. +/// +/// `deps` contains Storage, API and Querier +/// `env` contains block, message and contract info +/// `msg` is the contract initialization message, sort of like a constructor call. +#[entry_point] +pub fn instantiate( + mut deps: DepsMut<'_>, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let admin_addr = deps.api.addr_validate(&msg.admin)?; + let multisig_addr = deps.api.addr_validate(&msg.multisig_addr)?; + ADMIN.set(deps.branch(), Some(admin_addr))?; + MULTISIG.set(deps.branch(), Some(multisig_addr.clone()))?; + + let group_addr = Cw4Contract(deps.api.addr_validate(&msg.group_addr).map_err(|_| { + ContractError::InvalidGroup { + addr: msg.group_addr.clone(), + } + })?); + + let state = State { + group_addr, + multisig_addr, + mix_denom: msg.mix_denom, + }; + STATE.save(deps.storage, &state)?; + + CURRENT_EPOCH_STATE.save(deps.storage, &EpochState::default())?; + + Ok(Response::default()) +} + +/// Handle an incoming message +#[entry_point] +pub fn execute( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + 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) + } + ExecuteMsg::CommitVerificationKeyShare { share } => { + try_commit_verification_key_share(deps, env, info, share) + } + ExecuteMsg::VerifyVerificationKeyShare { owner } => { + try_verify_verification_key_share(deps, info, owner) + } + ExecuteMsg::AdvanceEpochState {} => advance_epoch_state(deps, info), + } +} + +#[entry_point] +pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result { + let response = match msg { + QueryMsg::GetCurrentEpochState {} => to_binary(&query_current_epoch_state(deps.storage)?)?, + QueryMsg::GetDealerDetails { dealer_address } => { + to_binary(&query_dealer_details(deps, dealer_address)?)? + } + QueryMsg::GetCurrentDealers { limit, start_after } => { + to_binary(&query_current_dealers_paged(deps, start_after, limit)?)? + } + QueryMsg::GetPastDealers { limit, start_after } => { + to_binary(&query_past_dealers_paged(deps, start_after, limit)?)? + } + QueryMsg::GetDealing { + idx, + limit, + start_after, + } => to_binary(&query_dealings_paged(deps, idx, start_after, limit)?)?, + QueryMsg::GetVerificationKeys { limit, start_after } => { + to_binary(&query_vk_shares_paged(deps, start_after, limit)?)? + } + }; + + Ok(response) +} + +#[entry_point] +pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { + Ok(Default::default()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::fixtures::{dealer_details_fixture, TEST_MIX_DENOM}; + use crate::support::tests::helpers::{ADMIN_ADDRESS, MULTISIG_CONTRACT}; + use coconut_dkg_common::dealer::DealerDetails; + use coconut_dkg_common::msg::ExecuteMsg::RegisterDealer; + use coconut_dkg_common::types::NodeIndex; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::{coins, Addr}; + use cw4::Member; + use cw4_group::msg::InstantiateMsg as GroupInstantiateMsg; + use cw_multi_test::{App, AppBuilder, AppResponse, ContractWrapper, Executor}; + + fn instantiate_with_group(app: &mut App, members: &[Addr]) -> Addr { + let group_code_id = app.store_code(Box::new(ContractWrapper::new( + cw4_group::contract::execute, + cw4_group::contract::instantiate, + cw4_group::contract::query, + ))); + let msg = GroupInstantiateMsg { + admin: Some(ADMIN_ADDRESS.to_string()), + members: members + .iter() + .map(|member| Member { + addr: member.to_string(), + weight: 10, + }) + .collect(), + }; + let group_contract_addr = app + .instantiate_contract( + group_code_id, + Addr::unchecked(ADMIN_ADDRESS), + &msg, + &[], + "group", + None, + ) + .unwrap(); + + let coconut_dkg_code_id = + app.store_code(Box::new(ContractWrapper::new(execute, instantiate, query))); + let msg = InstantiateMsg { + group_addr: group_contract_addr.to_string(), + multisig_addr: MULTISIG_CONTRACT.to_string(), + admin: Addr::unchecked(ADMIN_ADDRESS).to_string(), + mix_denom: TEST_MIX_DENOM.to_string(), + }; + app.instantiate_contract( + coconut_dkg_code_id, + Addr::unchecked(ADMIN_ADDRESS), + &msg, + &[], + "coconut dkg", + None, + ) + .unwrap() + } + + fn parse_node_index(res: AppResponse) -> NodeIndex { + res.events + .into_iter() + .find(|e| &e.ty == "wasm") + .unwrap() + .attributes + .into_iter() + .find(|attr| &attr.key == "node_index") + .unwrap() + .value + .parse::() + .unwrap() + } + + #[test] + fn initialize_contract() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = InstantiateMsg { + group_addr: "group_addr".to_string(), + multisig_addr: "multisig_addr".to_string(), + admin: "admin".to_string(), + mix_denom: "nym".to_string(), + }; + let info = mock_info("creator", &[]); + + let res = instantiate(deps.as_mut(), env.clone(), info, msg); + assert!(res.is_ok()) + } + + #[test] + fn execute_add_dealer() { + let init_funds = coins(100, TEST_MIX_DENOM); + const MEMBER_SIZE: usize = 100; + let members: [Addr; MEMBER_SIZE] = + std::array::from_fn(|idx| Addr::unchecked(format!("member{}", idx))); + + let mut app = AppBuilder::new().build(|router, _, storage| { + router + .bank + .init_balance(storage, &Addr::unchecked(ADMIN_ADDRESS), init_funds) + .unwrap(); + }); + let coconut_dkg_contract_addr = instantiate_with_group(&mut app, &members); + + for (idx, member) in members.iter().enumerate() { + let res = app + .execute_contract( + member.clone(), + coconut_dkg_contract_addr.clone(), + &RegisterDealer { + bte_key_with_proof: "bte_key_with_proof".to_string(), + announce_address: "127.0.0.1:8000".to_string(), + }, + &vec![], + ) + .unwrap(); + assert_eq!(parse_node_index(res), (idx + 1) as u64); + + let err = app + .execute_contract( + member.clone(), + coconut_dkg_contract_addr.clone(), + &RegisterDealer { + bte_key_with_proof: "bte_key_with_proof".to_string(), + announce_address: "127.0.0.1:8000".to_string(), + }, + &vec![], + ) + .unwrap_err(); + assert_eq!(ContractError::AlreadyADealer, err.downcast().unwrap()); + } + + let unauthorized_member = Addr::unchecked("not_a_member"); + let err = app + .execute_contract( + unauthorized_member, + coconut_dkg_contract_addr.clone(), + &RegisterDealer { + bte_key_with_proof: "bte_key_with_proof".to_string(), + announce_address: "127.0.0.1:8000".to_string(), + }, + &vec![], + ) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized, err.downcast().unwrap()); + } +} diff --git a/contracts/coconut-dkg/src/dealers/queries.rs b/contracts/coconut-dkg/src/dealers/queries.rs index 5221994dc0..d4ed2ca04e 100644 --- a/contracts/coconut-dkg/src/dealers/queries.rs +++ b/contracts/coconut-dkg/src/dealers/queries.rs @@ -10,7 +10,7 @@ fn query_dealers( deps: Deps<'_>, start_after: Option, limit: Option, - underlying_map: IndexedDealersMap<'_>, + underlying_map: &IndexedDealersMap<'_>, ) -> StdResult { let limit = limit .unwrap_or(storage::DEALERS_PAGE_DEFAULT_LIMIT) @@ -55,7 +55,7 @@ pub fn query_current_dealers_paged( start_after: Option, limit: Option, ) -> StdResult { - query_dealers(deps, start_after, limit, storage::current_dealers()) + query_dealers(deps, start_after, limit, &storage::current_dealers()) } pub fn query_past_dealers_paged( @@ -63,5 +63,166 @@ pub fn query_past_dealers_paged( start_after: Option, limit: Option, ) -> StdResult { - query_dealers(deps, start_after, limit, storage::past_dealers()) + query_dealers(deps, start_after, limit, &storage::past_dealers()) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::dealers::storage::{DEALERS_PAGE_DEFAULT_LIMIT, DEALERS_PAGE_MAX_LIMIT}; + use crate::support::tests::fixtures::dealer_details_fixture; + use crate::support::tests::helpers::init_contract; + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::{Addr, DepsMut}; + + fn fill_dealers(deps: DepsMut<'_>, mapping: &IndexedDealersMap<'_>, size: usize) { + for n in 0..size { + let dealer_details = dealer_details_fixture(n as u64); + mapping + .save(deps.storage, &dealer_details.address, &dealer_details) + .unwrap(); + } + } + + fn remove_dealers(deps: DepsMut<'_>, mapping: &IndexedDealersMap<'_>, size: usize) { + for n in 0..size { + let dealer_details = dealer_details_fixture(n as u64); + mapping + .remove(deps.storage, &dealer_details.address) + .unwrap(); + } + } + + #[test] + fn dealers_empty_on_init() { + let mut deps = init_contract(); + let env = mock_env(); + + for mapping in [storage::current_dealers(), storage::past_dealers()] { + let page1 = query_dealers(deps.as_ref(), None, None, &mapping).unwrap(); + assert_eq!(0, page1.dealers.len() as u32); + } + } + + #[test] + fn dealers_paged_retrieval_obeys_limits() { + let mut deps = init_contract(); + let env = mock_env(); + let owner = Addr::unchecked("owner"); + let limit = 2; + + for mapping in [storage::current_dealers(), storage::past_dealers()] { + fill_dealers(deps.as_mut(), &mapping, 1000); + + let page1 = query_dealers(deps.as_ref(), None, Option::from(limit), &mapping).unwrap(); + assert_eq!(limit, page1.dealers.len() as u32); + + remove_dealers(deps.as_mut(), &mapping, 1000); + } + } + + #[test] + fn dealers_paged_retrieval_has_default_limit() { + let mut deps = init_contract(); + let env = mock_env(); + + for mapping in [storage::current_dealers(), storage::past_dealers()] { + fill_dealers(deps.as_mut(), &mapping, 1000); + + // query without explicitly setting a limit + let page1 = query_dealers(deps.as_ref(), None, None, &mapping).unwrap(); + + assert_eq!(DEALERS_PAGE_DEFAULT_LIMIT, page1.dealers.len() as u32); + + remove_dealers(deps.as_mut(), &mapping, 1000); + } + } + + #[test] + fn dealers_paged_retrieval_has_max_limit() { + let mut deps = init_contract(); + let env = mock_env(); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000 * DEALERS_PAGE_MAX_LIMIT; + + for mapping in [storage::current_dealers(), storage::past_dealers()] { + fill_dealers(deps.as_mut(), &mapping, 1000); + + let page1 = + query_dealers(deps.as_ref(), None, Option::from(crazy_limit), &mapping).unwrap(); + + // we default to a decent sized upper bound instead + let expected_limit = DEALERS_PAGE_MAX_LIMIT; + assert_eq!(expected_limit, page1.dealers.len() as u32); + + remove_dealers(deps.as_mut(), &mapping, 1000); + } + } + + #[test] + fn dealers_pagination_works() { + let mut deps = init_contract(); + let env = mock_env(); + + let per_page = 2; + + for mapping in [storage::current_dealers(), storage::past_dealers()] { + fill_dealers(deps.as_mut(), &mapping, 1); + let page1 = + query_dealers(deps.as_ref(), None, Option::from(per_page), &mapping).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.dealers.len()); + remove_dealers(deps.as_mut(), &mapping, 1); + } + + for mapping in [storage::current_dealers(), storage::past_dealers()] { + fill_dealers(deps.as_mut(), &mapping, 2); + // page1 should have 2 results on it + let page1 = + query_dealers(deps.as_ref(), None, Option::from(per_page), &mapping).unwrap(); + assert_eq!(2, page1.dealers.len()); + remove_dealers(deps.as_mut(), &mapping, 2); + } + + for mapping in [storage::current_dealers(), storage::past_dealers()] { + fill_dealers(deps.as_mut(), &mapping, 3); + // page1 still has 2 results + let page1 = + query_dealers(deps.as_ref(), None, Option::from(per_page), &mapping).unwrap(); + assert_eq!(2, page1.dealers.len()); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_dealers( + deps.as_ref(), + Option::from(start_after.to_string()), + Option::from(per_page), + &mapping, + ) + .unwrap(); + + assert_eq!(1, page2.dealers.len()); + remove_dealers(deps.as_mut(), &mapping, 3); + } + + for mapping in [storage::current_dealers(), storage::past_dealers()] { + fill_dealers(deps.as_mut(), &mapping, 4); + let page1 = + query_dealers(deps.as_ref(), None, Option::from(per_page), &mapping).unwrap(); + let start_after = page1.start_next_after.unwrap(); + let page2 = query_dealers( + deps.as_ref(), + Option::from(start_after.to_string()), + Option::from(per_page), + &mapping, + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.dealers.len()); + remove_dealers(deps.as_mut(), &mapping, 4); + } + } } diff --git a/contracts/coconut-dkg/src/dealers/transactions.rs b/contracts/coconut-dkg/src/dealers/transactions.rs index 74e7d61aee..6c5ccbb8db 100644 --- a/contracts/coconut-dkg/src/dealers/transactions.rs +++ b/contracts/coconut-dkg/src/dealers/transactions.rs @@ -3,7 +3,8 @@ use crate::dealers::storage as dealers_storage; use crate::epoch_state::utils::check_epoch_state; -use crate::{ContractError, State, STATE}; +use crate::error::ContractError; +use crate::state::{State, STATE}; use coconut_dkg_common::types::{DealerDetails, EncodedBTEPublicKeyWithProof, EpochState}; use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response}; @@ -64,3 +65,40 @@ pub fn try_add_dealer( Ok(Response::new().add_attribute("node_index", node_index.to_string())) } + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::epoch_state::transactions::advance_epoch_state; + use crate::support::tests::fixtures::dealer_details_fixture; + use crate::support::tests::helpers; + use crate::support::tests::helpers::ADMIN_ADDRESS; + use cosmwasm_std::testing::mock_info; + + #[test] + fn invalid_state() { + let mut deps = helpers::init_contract(); + let owner = Addr::unchecked("owner"); + let info = mock_info(owner.as_str(), &[]); + let dealer_details = dealer_details_fixture(1); + let bte_key_with_proof = String::from("bte_key_with_proof"); + let announce_address = String::from("localhost:8000"); + + advance_epoch_state(deps.as_mut(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + + let ret = try_add_dealer( + deps.as_mut(), + info.clone(), + bte_key_with_proof.clone(), + announce_address.clone(), + ) + .unwrap_err(); + assert_eq!( + ret, + ContractError::IncorrectEpochState { + current_state: EpochState::DealingExchange.to_string(), + expected_state: EpochState::default().to_string(), + } + ); + } +} diff --git a/contracts/coconut-dkg/src/dealings/queries.rs b/contracts/coconut-dkg/src/dealings/queries.rs index cf2321837c..7fe476a458 100644 --- a/contracts/coconut-dkg/src/dealings/queries.rs +++ b/contracts/coconut-dkg/src/dealings/queries.rs @@ -43,3 +43,159 @@ pub fn query_dealings_paged( start_next_after, )) } + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::dealings::storage::{DEALINGS_PAGE_DEFAULT_LIMIT, DEALINGS_PAGE_MAX_LIMIT}; + use crate::support::tests::fixtures::dealing_bytes_fixture; + use crate::support::tests::helpers::init_contract; + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::{Addr, DepsMut}; + + fn fill_dealings(deps: DepsMut<'_>, size: usize) { + for n in 0..size { + let dealing_share = dealing_bytes_fixture(); + let sender = Addr::unchecked(format!("owner{}", n)); + for idx in 0..TOTAL_DEALINGS { + DEALINGS_BYTES[idx] + .save(deps.storage, &sender, &dealing_share) + .unwrap(); + } + } + } + + #[test] + fn empty_on_bad_idx() { + let mut deps = init_contract(); + let env = mock_env(); + fill_dealings(deps.as_mut(), 1000); + + for idx in TOTAL_DEALINGS as u64..100 * TOTAL_DEALINGS as u64 { + let page1 = query_dealings_paged(deps.as_ref(), idx, None, None).unwrap(); + assert_eq!(0, page1.dealings.len() as u32); + } + } + + #[test] + fn dealings_empty_on_init() { + let deps = init_contract(); + for idx in 0..TOTAL_DEALINGS as u64 { + let response = query_dealings_paged(deps.as_ref(), idx, None, Option::from(2)).unwrap(); + assert_eq!(0, response.dealings.len()); + } + } + + #[test] + fn dealings_paged_retrieval_obeys_limits() { + let mut deps = init_contract(); + let env = mock_env(); + let limit = 2; + fill_dealings(deps.as_mut(), 1000); + + for idx in 0..TOTAL_DEALINGS as u64 { + let page1 = + query_dealings_paged(deps.as_ref(), idx, None, Option::from(limit)).unwrap(); + assert_eq!(limit, page1.dealings.len() as u32); + } + } + + #[test] + fn dealings_paged_retrieval_has_default_limit() { + let mut deps = init_contract(); + let env = mock_env(); + fill_dealings(deps.as_mut(), 1000); + + for idx in 0..TOTAL_DEALINGS as u64 { + // query without explicitly setting a limit + let page1 = query_dealings_paged(deps.as_ref(), idx, None, None).unwrap(); + + assert_eq!(DEALINGS_PAGE_DEFAULT_LIMIT, page1.dealings.len() as u32); + } + } + + #[test] + fn dealings_paged_retrieval_has_max_limit() { + let mut deps = init_contract(); + let env = mock_env(); + fill_dealings(deps.as_mut(), 1000); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000 * DEALINGS_PAGE_MAX_LIMIT; + for idx in 0..TOTAL_DEALINGS as u64 { + let page1 = + query_dealings_paged(deps.as_ref(), idx, None, Option::from(crazy_limit)).unwrap(); + + // we default to a decent sized upper bound instead + let expected_limit = DEALINGS_PAGE_MAX_LIMIT; + assert_eq!(expected_limit, page1.dealings.len() as u32); + } + } + + #[test] + fn dealings_pagination_works() { + let mut deps = init_contract(); + let env = mock_env(); + + fill_dealings(deps.as_mut(), 1); + + let per_page = 2; + + for idx in 0..TOTAL_DEALINGS as u64 { + let page1 = + query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.dealings.len()); + } + + // save another + fill_dealings(deps.as_mut(), 2); + + for idx in 0..TOTAL_DEALINGS as u64 { + // page1 should have 2 results on it + let page1 = + query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.dealings.len()); + } + + fill_dealings(deps.as_mut(), 3); + + for idx in 0..TOTAL_DEALINGS as u64 { + // page1 still has 2 results + let page1 = + query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.dealings.len()); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_dealings_paged( + deps.as_ref(), + idx, + Option::from(start_after.to_string()), + Option::from(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.dealings.len()); + } + + fill_dealings(deps.as_mut(), 4); + + for idx in 0..TOTAL_DEALINGS as u64 { + let page1 = + query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap(); + let start_after = page1.start_next_after.unwrap(); + let page2 = query_dealings_paged( + deps.as_ref(), + idx, + Option::from(start_after.to_string()), + Option::from(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.dealings.len()); + } + } +} diff --git a/contracts/coconut-dkg/src/dealings/transactions.rs b/contracts/coconut-dkg/src/dealings/transactions.rs index 73e067e0d6..7320768979 100644 --- a/contracts/coconut-dkg/src/dealings/transactions.rs +++ b/contracts/coconut-dkg/src/dealings/transactions.rs @@ -4,7 +4,7 @@ use crate::dealers::storage as dealers_storage; use crate::dealings::storage::DEALINGS_BYTES; use crate::epoch_state::utils::check_epoch_state; -use crate::ContractError; +use crate::error::ContractError; use coconut_dkg_common::types::{ContractSafeBytes, EpochState}; use cosmwasm_std::{DepsMut, MessageInfo, Response}; @@ -35,3 +35,65 @@ pub fn try_commit_dealings( commitment: String::from("dealing"), }) } + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::epoch_state::transactions::advance_epoch_state; + use crate::support::tests::fixtures::dealing_bytes_fixture; + use crate::support::tests::helpers; + use crate::support::tests::helpers::ADMIN_ADDRESS; + use coconut_dkg_common::dealer::DealerDetails; + use coconut_dkg_common::types::TOTAL_DEALINGS; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Addr; + + #[test] + fn invalid_commit_dealing() { + let mut deps = helpers::init_contract(); + let owner = Addr::unchecked("owner"); + 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(); + assert_eq!( + ret, + ContractError::IncorrectEpochState { + current_state: EpochState::default().to_string(), + expected_state: EpochState::DealingExchange.to_string() + } + ); + + advance_epoch_state(deps.as_mut(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + + let ret = + try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone()).unwrap_err(); + assert_eq!(ret, ContractError::NotADealer); + + let dealer_details = DealerDetails { + address: owner.clone(), + bte_public_key_with_proof: String::new(), + announce_address: String::new(), + assigned_index: 1, + }; + dealers_storage::current_dealers() + .save(deps.as_mut().storage, &owner, &dealer_details) + .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()); + 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(); + assert_eq!( + ret, + ContractError::AlreadyCommitted { + commitment: String::from("dealing"), + } + ); + } +} diff --git a/contracts/coconut-dkg/src/epoch_state/mod.rs b/contracts/coconut-dkg/src/epoch_state/mod.rs index 195ff89361..02c5bc4402 100644 --- a/contracts/coconut-dkg/src/epoch_state/mod.rs +++ b/contracts/coconut-dkg/src/epoch_state/mod.rs @@ -3,4 +3,5 @@ pub mod queries; pub mod storage; +pub mod transactions; pub mod utils; diff --git a/contracts/coconut-dkg/src/epoch_state/queries.rs b/contracts/coconut-dkg/src/epoch_state/queries.rs index 142ee4826a..4c63f40c7e 100644 --- a/contracts/coconut-dkg/src/epoch_state/queries.rs +++ b/contracts/coconut-dkg/src/epoch_state/queries.rs @@ -1,13 +1,28 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::storage; -use crate::ContractError; +use crate::epoch_state::storage::CURRENT_EPOCH_STATE; +use crate::error::ContractError; use coconut_dkg_common::types::EpochState; use cosmwasm_std::Storage; pub(crate) fn query_current_epoch_state( storage: &dyn Storage, ) -> Result { - storage::current_epoch_state(storage) + CURRENT_EPOCH_STATE + .load(storage) + .map_err(|_| ContractError::EpochNotInitialised) +} + +#[cfg(test)] +pub(crate) mod test { + use super::*; + use crate::support::tests::helpers::init_contract; + + #[test] + fn query_state() { + let mut deps = init_contract(); + let state = query_current_epoch_state(deps.as_mut().storage).unwrap(); + assert_eq!(state, EpochState::PublicKeySubmission); + } } diff --git a/contracts/coconut-dkg/src/epoch_state/storage.rs b/contracts/coconut-dkg/src/epoch_state/storage.rs index b0abe5fa5b..87d1769183 100644 --- a/contracts/coconut-dkg/src/epoch_state/storage.rs +++ b/contracts/coconut-dkg/src/epoch_state/storage.rs @@ -1,29 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{ContractError, ADMIN}; use coconut_dkg_common::types::EpochState; -use cosmwasm_std::{DepsMut, MessageInfo, Response, Storage}; use cw_storage_plus::Item; pub(crate) const CURRENT_EPOCH_STATE: Item<'_, EpochState> = Item::new("current_epoch_state"); - -pub(crate) fn current_epoch_state(storage: &dyn Storage) -> Result { - CURRENT_EPOCH_STATE - .load(storage) - .map_err(|_| ContractError::EpochNotInitialised) -} - -pub(crate) fn advance_epoch_state( - deps: DepsMut<'_>, - info: MessageInfo, -) -> Result { - ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - CURRENT_EPOCH_STATE.update::<_, ContractError>(deps.storage, |mut epoch_state| { - // TODO: When defaulting to the first state, some action will probably need to be taken on the - // rest of the contract, as we're starting with a new set of signers - epoch_state = epoch_state.next().unwrap_or_default(); - Ok(epoch_state) - })?; - Ok(Response::default()) -} diff --git a/contracts/coconut-dkg/src/epoch_state/transactions.rs b/contracts/coconut-dkg/src/epoch_state/transactions.rs new file mode 100644 index 0000000000..ba70371fd8 --- /dev/null +++ b/contracts/coconut-dkg/src/epoch_state/transactions.rs @@ -0,0 +1,78 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::epoch_state::storage::CURRENT_EPOCH_STATE; +use crate::error::ContractError; +use crate::state::ADMIN; +use cosmwasm_std::{DepsMut, MessageInfo, Response}; + +pub(crate) fn advance_epoch_state( + deps: DepsMut<'_>, + info: MessageInfo, +) -> Result { + ADMIN.assert_admin(deps.as_ref(), &info.sender)?; + CURRENT_EPOCH_STATE.update::<_, ContractError>(deps.storage, |mut epoch_state| { + // TODO: When defaulting to the first state, some action will probably need to be taken on the + // rest of the contract, as we're starting with a new set of signers + epoch_state = epoch_state.next().unwrap_or_default(); + Ok(epoch_state) + })?; + Ok(Response::default()) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::support::tests::helpers::{init_contract, ADMIN_ADDRESS}; + use coconut_dkg_common::types::EpochState; + use cosmwasm_std::testing::mock_info; + use cw_controllers::AdminError; + + #[test] + fn advance_state() { + let mut deps = init_contract(); + let info = mock_info("requester", &[]); + let admin_info = mock_info(ADMIN_ADDRESS, &[]); + + assert_eq!( + advance_epoch_state(deps.as_mut(), info).unwrap_err(), + ContractError::Admin(AdminError::NotAdmin {}) + ); + + advance_epoch_state(deps.as_mut(), admin_info.clone()).unwrap(); + assert_eq!( + CURRENT_EPOCH_STATE.load(deps.as_mut().storage).unwrap(), + EpochState::DealingExchange + ); + + advance_epoch_state(deps.as_mut(), admin_info.clone()).unwrap(); + assert_eq!( + CURRENT_EPOCH_STATE.load(deps.as_mut().storage).unwrap(), + EpochState::VerificationKeySubmission + ); + + advance_epoch_state(deps.as_mut(), admin_info.clone()).unwrap(); + assert_eq!( + CURRENT_EPOCH_STATE.load(deps.as_mut().storage).unwrap(), + EpochState::VerificationKeyValidation + ); + + advance_epoch_state(deps.as_mut(), admin_info.clone()).unwrap(); + assert_eq!( + CURRENT_EPOCH_STATE.load(deps.as_mut().storage).unwrap(), + EpochState::VerificationKeyFinalization + ); + + advance_epoch_state(deps.as_mut(), admin_info.clone()).unwrap(); + assert_eq!( + CURRENT_EPOCH_STATE.load(deps.as_mut().storage).unwrap(), + EpochState::InProgress + ); + + advance_epoch_state(deps.as_mut(), admin_info.clone()).unwrap(); + assert_eq!( + CURRENT_EPOCH_STATE.load(deps.as_mut().storage).unwrap(), + EpochState::PublicKeySubmission + ); + } +} diff --git a/contracts/coconut-dkg/src/epoch_state/utils.rs b/contracts/coconut-dkg/src/epoch_state/utils.rs index d78fd49ae3..b944ad361d 100644 --- a/contracts/coconut-dkg/src/epoch_state/utils.rs +++ b/contracts/coconut-dkg/src/epoch_state/utils.rs @@ -1,7 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{ContractError, CURRENT_EPOCH_STATE}; +use crate::epoch_state::storage::CURRENT_EPOCH_STATE; +use crate::error::ContractError; use coconut_dkg_common::types::EpochState; use cosmwasm_std::Storage; @@ -19,3 +20,28 @@ pub(crate) fn check_epoch_state( Ok(()) } } + +#[cfg(test)] +pub(crate) mod test { + use super::*; + use crate::support::tests::helpers::init_contract; + + #[test] + pub fn check_state() { + let mut deps = init_contract(); + + for fixed_state in EpochState::default().all_until(EpochState::InProgress) { + CURRENT_EPOCH_STATE + .save(deps.as_mut().storage, &fixed_state) + .unwrap(); + for against_state in EpochState::default().all_until(EpochState::InProgress) { + let ret = check_epoch_state(deps.as_mut().storage, against_state); + if fixed_state == against_state { + assert!(ret.is_ok()); + } else { + assert!(ret.is_err()); + } + } + } + } +} diff --git a/contracts/coconut-dkg/src/error.rs b/contracts/coconut-dkg/src/error.rs index ad3a3e74ef..089a8b9f27 100644 --- a/contracts/coconut-dkg/src/error.rs +++ b/contracts/coconut-dkg/src/error.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{Addr, StdError, VerificationError}; +use cosmwasm_std::StdError; use cw_controllers::AdminError; use thiserror::Error; @@ -17,24 +17,6 @@ pub enum ContractError { #[error("Group contract invalid address '{addr}'")] InvalidGroup { addr: String }, - #[error("No coin was sent for the deposit, you must send {denom}")] - NoDepositFound { denom: String }, - - #[error("Received multiple coin types")] - MultipleDenoms, - - #[error("Wrong coin denomination, you must send {denom}")] - WrongDenom { denom: String }, - - #[error("Not enough funds sent for deposit. (received {received}, minimum {minimum})")] - InsufficientDeposit { received: u128, minimum: u128 }, - - #[error("Failed to perform ed25519 signature verification - {0}. This dealer will be temporarily blacklisted now.")] - Ed25519VerificationError(#[from] VerificationError), - - #[error("Provided ed25519 signature did not verify correctly. This dealer will be temporarily blacklisted now.")] - InvalidEd25519Signature, - #[error("This potential dealer is not in the coconut signer group")] Unauthorized, @@ -52,11 +34,6 @@ pub enum ContractError { expected_state: String, }, - // we should never ever see this error (famous last words in programming), therefore, I'd want to - // explicitly declare it so that when we ultimate do see it, it's gonna be more informative over "normal" panic - #[error("Somehow our validated address {address} is not using correct bech32 encoding")] - InvalidValidatedAddress { address: Addr }, - #[error("This sender is not a dealer for the current epoch")] NotADealer, diff --git a/contracts/coconut-dkg/src/lib.rs b/contracts/coconut-dkg/src/lib.rs index 7255b96ae8..de93fa7b92 100644 --- a/contracts/coconut-dkg/src/lib.rs +++ b/contracts/coconut-dkg/src/lib.rs @@ -1,146 +1,12 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::dealers::queries::{ - query_current_dealers_paged, query_dealer_details, query_past_dealers_paged, -}; -use crate::dealings::queries::query_dealings_paged; -use crate::epoch_state::queries::query_current_epoch_state; -use crate::error::ContractError; -use crate::state::{State, ADMIN, MULTISIG, STATE}; -use crate::verification_key_shares::queries::query_vk_shares_paged; -use coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -use coconut_dkg_common::types::EpochState; -use cosmwasm_std::{ - entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, -}; -use cw4::Cw4Contract; -use epoch_state::storage::{advance_epoch_state, CURRENT_EPOCH_STATE}; - mod constants; +pub mod contract; mod dealers; mod dealings; mod epoch_state; -mod error; +pub mod error; mod state; +mod support; mod verification_key_shares; - -/// Instantiate the contract. -/// -/// `deps` contains Storage, API and Querier -/// `env` contains block, message and contract info -/// `msg` is the contract initialization message, sort of like a constructor call. -#[entry_point] -pub fn instantiate( - mut deps: DepsMut<'_>, - _env: Env, - _info: MessageInfo, - msg: InstantiateMsg, -) -> Result { - let admin_addr = deps.api.addr_validate(&msg.admin)?; - let multisig_addr = deps.api.addr_validate(&msg.multisig_addr)?; - ADMIN.set(deps.branch(), Some(admin_addr))?; - MULTISIG.set(deps.branch(), Some(multisig_addr.clone()))?; - - let group_addr = Cw4Contract(deps.api.addr_validate(&msg.group_addr).map_err(|_| { - ContractError::InvalidGroup { - addr: msg.group_addr.clone(), - } - })?); - - let state = State { - group_addr, - multisig_addr, - mix_denom: msg.mix_denom, - }; - STATE.save(deps.storage, &state)?; - - CURRENT_EPOCH_STATE.save(deps.storage, &EpochState::default())?; - - Ok(Response::default()) -} - -/// Handle an incoming message -#[entry_point] -pub fn execute( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result { - match msg { - ExecuteMsg::RegisterDealer { - bte_key_with_proof, - announce_address, - } => { - dealers::transactions::try_add_dealer(deps, info, bte_key_with_proof, announce_address) - } - ExecuteMsg::CommitDealing { dealing_bytes } => { - dealings::transactions::try_commit_dealings(deps, info, dealing_bytes) - } - ExecuteMsg::CommitVerificationKeyShare { share } => { - verification_key_shares::transactions::try_commit_verification_key_share( - deps, env, info, share, - ) - } - ExecuteMsg::VerifyVerificationKeyShare { owner } => { - verification_key_shares::transactions::try_verify_verification_key_share( - deps, info, owner, - ) - } - ExecuteMsg::AdvanceEpochState {} => advance_epoch_state(deps, info), - } -} - -#[entry_point] -pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result { - let response = match msg { - QueryMsg::GetCurrentEpochState {} => to_binary(&query_current_epoch_state(deps.storage)?)?, - QueryMsg::GetDealerDetails { dealer_address } => { - to_binary(&query_dealer_details(deps, dealer_address)?)? - } - QueryMsg::GetCurrentDealers { limit, start_after } => { - to_binary(&query_current_dealers_paged(deps, start_after, limit)?)? - } - QueryMsg::GetPastDealers { limit, start_after } => { - to_binary(&query_past_dealers_paged(deps, start_after, limit)?)? - } - QueryMsg::GetDealing { - idx, - limit, - start_after, - } => to_binary(&query_dealings_paged(deps, idx, start_after, limit)?)?, - QueryMsg::GetVerificationKeys { limit, start_after } => { - to_binary(&query_vk_shares_paged(deps, start_after, limit)?)? - } - }; - - Ok(response) -} - -#[entry_point] -pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { - Ok(Default::default()) -} - -#[cfg(test)] -mod tests { - use super::*; - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - - #[test] - fn initialize_contract() { - let mut deps = mock_dependencies(); - let env = mock_env(); - let msg = InstantiateMsg { - group_addr: "group_addr".to_string(), - multisig_addr: "multisig_addr".to_string(), - admin: "admin".to_string(), - mix_denom: "nym".to_string(), - }; - let info = mock_info("creator", &[]); - - let res = instantiate(deps.as_mut(), env.clone(), info, msg); - assert!(res.is_ok()) - } -} diff --git a/contracts/coconut-dkg/src/support/mod.rs b/contracts/coconut-dkg/src/support/mod.rs new file mode 100644 index 0000000000..15f23f8519 --- /dev/null +++ b/contracts/coconut-dkg/src/support/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub mod tests; diff --git a/contracts/coconut-dkg/src/support/tests/fixtures.rs b/contracts/coconut-dkg/src/support/tests/fixtures.rs new file mode 100644 index 0000000000..e0bdee4e4d --- /dev/null +++ b/contracts/coconut-dkg/src/support/tests/fixtures.rs @@ -0,0 +1,32 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use coconut_dkg_common::dealer::{ContractDealing, DealerDetails}; +use coconut_dkg_common::types::ContractSafeBytes; +use coconut_dkg_common::verification_key::ContractVKShare; +use cosmwasm_std::Addr; + +pub const TEST_MIX_DENOM: &str = "unym"; + +pub fn vk_share_fixture(index: u64) -> ContractVKShare { + ContractVKShare { + share: format!("share{}", index), + announce_address: format!("localhost:{}", index), + node_index: index, + owner: Addr::unchecked(format!("owner{}", index)), + verified: index % 2 == 0, + } +} + +pub fn dealing_bytes_fixture() -> ContractSafeBytes { + ContractSafeBytes(vec![]) +} + +pub fn dealer_details_fixture(assigned_index: u64) -> DealerDetails { + DealerDetails { + address: Addr::unchecked(format!("owner{}", assigned_index)), + bte_public_key_with_proof: "".to_string(), + announce_address: "".to_string(), + assigned_index, + } +} diff --git a/contracts/coconut-dkg/src/support/tests/helpers.rs b/contracts/coconut-dkg/src/support/tests/helpers.rs new file mode 100644 index 0000000000..ec46ff0215 --- /dev/null +++ b/contracts/coconut-dkg/src/support/tests/helpers.rs @@ -0,0 +1,27 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::contract::instantiate; +use coconut_dkg_common::msg::InstantiateMsg; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; +use cosmwasm_std::{Empty, MemoryStorage, OwnedDeps}; + +use super::fixtures::TEST_MIX_DENOM; + +pub const ADMIN_ADDRESS: &str = "admin address"; +pub const GROUP_CONTRACT: &str = "group contract address"; +pub const MULTISIG_CONTRACT: &str = "multisig contract address"; + +pub fn init_contract() -> OwnedDeps> { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { + group_addr: String::from(GROUP_CONTRACT), + multisig_addr: String::from(MULTISIG_CONTRACT), + admin: String::from(ADMIN_ADDRESS), + mix_denom: TEST_MIX_DENOM.to_string(), + }; + let env = mock_env(); + let info = mock_info(ADMIN_ADDRESS, &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + deps +} diff --git a/contracts/coconut-dkg/src/support/tests/mod.rs b/contracts/coconut-dkg/src/support/tests/mod.rs new file mode 100644 index 0000000000..1e96465ed5 --- /dev/null +++ b/contracts/coconut-dkg/src/support/tests/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod fixtures; +pub mod helpers; diff --git a/contracts/coconut-dkg/src/verification_key_shares/queries.rs b/contracts/coconut-dkg/src/verification_key_shares/queries.rs index 6739577abb..e9c4d00dca 100644 --- a/contracts/coconut-dkg/src/verification_key_shares/queries.rs +++ b/contracts/coconut-dkg/src/verification_key_shares/queries.rs @@ -36,3 +36,147 @@ pub fn query_vk_shares_paged( start_next_after, }) } + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::support::tests::fixtures::vk_share_fixture; + use crate::support::tests::helpers::init_contract; + use crate::verification_key_shares::storage::{ + VERIFICATION_KEY_SHARES_PAGE_DEFAULT_LIMIT, VERIFICATION_KEY_SHARES_PAGE_MAX_LIMIT, + }; + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::Addr; + + #[test] + fn vk_shares_empty_on_init() { + let deps = init_contract(); + let response = query_vk_shares_paged(deps.as_ref(), None, Option::from(2)).unwrap(); + assert_eq!(0, response.shares.len()); + } + + #[test] + fn vk_shares_paged_retrieval_obeys_limits() { + let mut deps = init_contract(); + let env = mock_env(); + let limit = 2; + for n in 0..1000 { + let vk_share = vk_share_fixture(n); + let sender = Addr::unchecked(format!("owner{}", n)); + VK_SHARES + .save(&mut deps.storage, &sender, &vk_share) + .unwrap(); + } + + let page1 = query_vk_shares_paged(deps.as_ref(), None, Option::from(limit)).unwrap(); + assert_eq!(limit, page1.shares.len() as u32); + } + + #[test] + fn vk_shares_paged_retrieval_has_default_limit() { + let mut deps = init_contract(); + let env = mock_env(); + for n in 0..1000 { + let vk_share = vk_share_fixture(n); + let sender = Addr::unchecked(format!("owner{}", n)); + VK_SHARES + .save(&mut deps.storage, &sender, &vk_share) + .unwrap(); + } + + // query without explicitly setting a limit + let page1 = query_vk_shares_paged(deps.as_ref(), None, None).unwrap(); + + assert_eq!( + VERIFICATION_KEY_SHARES_PAGE_DEFAULT_LIMIT, + page1.shares.len() as u32 + ); + } + + #[test] + fn vk_shares_paged_retrieval_has_max_limit() { + let mut deps = init_contract(); + let env = mock_env(); + for n in 0..1000 { + let vk_share = vk_share_fixture(n); + let sender = Addr::unchecked(format!("owner{}", n)); + VK_SHARES + .save(&mut deps.storage, &sender, &vk_share) + .unwrap(); + } + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000 * VERIFICATION_KEY_SHARES_PAGE_MAX_LIMIT; + let page1 = query_vk_shares_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap(); + + // we default to a decent sized upper bound instead + let expected_limit = VERIFICATION_KEY_SHARES_PAGE_MAX_LIMIT; + assert_eq!(expected_limit, page1.shares.len() as u32); + } + + #[test] + fn vk_shares_pagination_works() { + let mut deps = init_contract(); + let env = mock_env(); + + let vk_share = vk_share_fixture(1); + let sender = Addr::unchecked(format!("owner{}", 1)); + VK_SHARES + .save(&mut deps.storage, &sender, &vk_share) + .unwrap(); + + let per_page = 2; + let page1 = query_vk_shares_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.shares.len()); + + // save another + let vk_share = vk_share_fixture(2); + let sender = Addr::unchecked(format!("owner{}", 2)); + VK_SHARES + .save(&mut deps.storage, &sender, &vk_share) + .unwrap(); + + // page1 should have 2 results on it + let page1 = query_vk_shares_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.shares.len()); + + let vk_share = vk_share_fixture(3); + let sender = Addr::unchecked(format!("owner{}", 3)); + VK_SHARES + .save(&mut deps.storage, &sender, &vk_share) + .unwrap(); + + // page1 still has 2 results + let page1 = query_vk_shares_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.shares.len()); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_vk_shares_paged( + deps.as_ref(), + Option::from(start_after.to_string()), + Option::from(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.shares.len()); + + let vk_share = vk_share_fixture(4); + let sender = Addr::unchecked(format!("owner{}", 4)); + VK_SHARES + .save(&mut deps.storage, &sender, &vk_share) + .unwrap(); + + let page2 = query_vk_shares_paged( + deps.as_ref(), + Option::from(start_after.to_string()), + Option::from(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.shares.len()); + } +} diff --git a/contracts/coconut-dkg/src/verification_key_shares/transactions.rs b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs index d86748e7f4..0f76fc7811 100644 --- a/contracts/coconut-dkg/src/verification_key_shares/transactions.rs +++ b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs @@ -69,3 +69,144 @@ pub fn try_verify_verification_key_share( Ok(Response::default()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::epoch_state::storage::CURRENT_EPOCH_STATE; + use crate::epoch_state::transactions::advance_epoch_state; + use crate::support::tests::helpers; + use crate::support::tests::helpers::{ADMIN_ADDRESS, MULTISIG_CONTRACT}; + use coconut_dkg_common::dealer::DealerDetails; + use coconut_dkg_common::types::EpochState; + use cosmwasm_std::testing::{mock_env, mock_info}; + use cosmwasm_std::Storage; + use cw_controllers::AdminError; + + #[test] + fn commit_vk_share() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let info = mock_info("requester", &[]); + let share = "share".to_string(); + + let ret = try_commit_verification_key_share( + deps.as_mut(), + env.clone(), + info.clone(), + share.clone(), + ) + .unwrap_err(); + assert_eq!( + ret, + ContractError::IncorrectEpochState { + current_state: EpochState::default().to_string(), + expected_state: EpochState::VerificationKeySubmission.to_string() + } + ); + advance_epoch_state(deps.as_mut(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + advance_epoch_state(deps.as_mut(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + let ret = try_commit_verification_key_share( + deps.as_mut(), + env.clone(), + info.clone(), + share.clone(), + ) + .unwrap_err(); + assert_eq!(ret, ContractError::NotADealer); + + let dealer = Addr::unchecked("requester"); + let dealer_details = DealerDetails { + address: dealer.clone(), + bte_public_key_with_proof: String::new(), + announce_address: String::new(), + assigned_index: 1, + }; + dealers_storage::current_dealers() + .save(deps.as_mut().storage, &dealer, &dealer_details) + .unwrap(); + + try_commit_verification_key_share(deps.as_mut(), env.clone(), info.clone(), share.clone()) + .unwrap(); + + let ret = try_commit_verification_key_share( + deps.as_mut(), + env.clone(), + info.clone(), + share.clone(), + ) + .unwrap_err(); + assert_eq!( + ret, + ContractError::AlreadyCommitted { + commitment: String::from("verification key share") + } + ); + } + + #[test] + fn invalid_verify_vk_share() { + let mut deps = helpers::init_contract(); + let info = mock_info("requester", &[]); + 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(); + assert_eq!( + ret, + ContractError::IncorrectEpochState { + current_state: EpochState::default().to_string(), + expected_state: EpochState::VerificationKeyFinalization.to_string() + } + ); + + advance_epoch_state(deps.as_mut(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + advance_epoch_state(deps.as_mut(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + advance_epoch_state(deps.as_mut(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + advance_epoch_state(deps.as_mut(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + + let ret = + try_verify_verification_key_share(deps.as_mut(), info, owner.clone()).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(); + assert_eq!( + ret, + ContractError::NoCommitForOwner { + owner: owner.to_string() + } + ); + } + + #[test] + fn verify_vk_share() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let owner = Addr::unchecked("owner"); + let info = mock_info(owner.as_ref(), &[]); + let share = "share".to_string(); + let multisig_info = mock_info(MULTISIG_CONTRACT, &[]); + + advance_epoch_state(deps.as_mut(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + advance_epoch_state(deps.as_mut(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + + let dealer_details = DealerDetails { + address: owner.clone(), + bte_public_key_with_proof: String::new(), + announce_address: String::new(), + assigned_index: 1, + }; + 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(); + + advance_epoch_state(deps.as_mut(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + advance_epoch_state(deps.as_mut(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + + try_verify_verification_key_share(deps.as_mut(), multisig_info, owner.clone()).unwrap(); + } +} diff --git a/contracts/coconut-test/Cargo.toml b/contracts/coconut-test/Cargo.toml index 3aff6fc368..f9dd795e4b 100644 --- a/contracts/coconut-test/Cargo.toml +++ b/contracts/coconut-test/Cargo.toml @@ -13,6 +13,7 @@ multisig-contract-common = { path = "../../common/cosmwasm-smart-contracts/multi cosmwasm-std = "1.0.0" cosmwasm-storage = "1.0.0" +cw3 = "0.13.4" cw4 = "0.13.4" cw-storage-plus = "0.13.4" cw-controllers = "0.13.4" diff --git a/contracts/coconut-test/src/helpers.rs b/contracts/coconut-test/src/helpers.rs index b8e8ad6d03..319e523c91 100644 --- a/contracts/coconut-test/src/helpers.rs +++ b/contracts/coconut-test/src/helpers.rs @@ -42,9 +42,9 @@ pub fn mock_app(init_funds: &[Coin]) -> App { } pub fn contract_dkg() -> Box> { let contract = ContractWrapper::new( - coconut_dkg::execute, - coconut_dkg::instantiate, - coconut_dkg::query, + coconut_dkg::contract::execute, + coconut_dkg::contract::instantiate, + coconut_dkg::contract::query, ); Box::new(contract) } diff --git a/contracts/coconut-test/src/submit_vk_creates_proposal.rs b/contracts/coconut-test/src/submit_vk_creates_proposal.rs index dd8f59b2da..15bbd65341 100644 --- a/contracts/coconut-test/src/submit_vk_creates_proposal.rs +++ b/contracts/coconut-test/src/submit_vk_creates_proposal.rs @@ -11,15 +11,18 @@ use coconut_dkg_common::msg::ExecuteMsg::{ AdvanceEpochState, CommitVerificationKeyShare, RegisterDealer, }; use coconut_dkg_common::msg::InstantiateMsg as DkgInstantiateMsg; +use coconut_dkg_common::msg::QueryMsg::GetVerificationKeys; +use coconut_dkg_common::verification_key::PagedVKSharesResponse; use cosmwasm_std::{coins, Addr, Decimal}; use cw4::Member; use cw4_group::msg::InstantiateMsg as GroupInstantiateMsg; use cw_multi_test::Executor; use cw_utils::{Duration, Threshold}; +use multisig_contract_common::msg::ExecuteMsg::{Execute, Vote}; use multisig_contract_common::msg::InstantiateMsg as MultisigInstantiateMsg; #[test] -fn dkg_create_proposal() { +fn dkg_proposal() { let init_funds = coins(10000000000, TEST_COIN_DENOM); let mut app = mock_app(&init_funds); let member1 = Member { @@ -47,7 +50,7 @@ fn dkg_create_proposal() { let msg = MultisigInstantiateMsg { group_addr: group_contract_addr.to_string(), threshold: Threshold::AbsolutePercentage { - percentage: Decimal::from_ratio(2u128, 3u128), + percentage: Decimal::from_ratio(1u128, 1u128), }, max_voting_period: Duration::Time(1000), coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), @@ -88,7 +91,7 @@ fn dkg_create_proposal() { }; app.migrate_contract( Addr::unchecked(OWNER), - multisig_contract_addr, + multisig_contract_addr.clone(), &msg, multisig_code_id, ) @@ -115,6 +118,9 @@ fn dkg_create_proposal() { .unwrap(); } + // Proposal needs to be later then the member became part of the group + app.update_block(|block| block.height += 1); + let msg = CommitVerificationKeyShare { share: "share".to_string(), }; @@ -126,6 +132,7 @@ fn dkg_create_proposal() { &vec![], ) .unwrap(); + let proposal_id = res .events .into_iter() @@ -138,5 +145,62 @@ fn dkg_create_proposal() { .value .parse::() .unwrap(); - assert_eq!(1, proposal_id); + + let mut res: PagedVKSharesResponse = app + .wrap() + .query_wasm_smart( + coconut_dkg_contract_addr.clone(), + &GetVerificationKeys { + limit: None, + start_after: None, + }, + ) + .unwrap(); + let share = res.shares.pop().unwrap(); + assert_eq!(share.share, "share".to_string()); + assert_eq!(share.announce_address, "127.0.0.1:8000".to_string()); + assert_eq!(share.node_index, 1); + assert_eq!(share.owner, Addr::unchecked(MEMBER1)); + assert!(!share.verified); + + app.execute_contract( + Addr::unchecked(MEMBER1), + multisig_contract_addr.clone(), + &Vote { + proposal_id, + vote: cw3::Vote::Yes, + }, + &vec![], + ) + .unwrap(); + + for _ in 0..2 { + app.execute_contract( + Addr::unchecked(OWNER), + coconut_dkg_contract_addr.clone(), + &AdvanceEpochState {}, + &vec![], + ) + .unwrap(); + } + + app.execute_contract( + Addr::unchecked(MEMBER1), + multisig_contract_addr.clone(), + &Execute { proposal_id }, + &vec![], + ) + .unwrap(); + + let res: PagedVKSharesResponse = app + .wrap() + .query_wasm_smart( + coconut_dkg_contract_addr, + &GetVerificationKeys { + limit: None, + start_after: None, + }, + ) + .unwrap(); + assert!(res.shares[0].verified); } diff --git a/validator-api/src/coconut/dkg/complaints.rs b/validator-api/src/coconut/dkg/complaints.rs index 8a95645a30..e62d9feac3 100644 --- a/validator-api/src/coconut/dkg/complaints.rs +++ b/validator-api/src/coconut/dkg/complaints.rs @@ -1,11 +1,10 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use dkg::error::DkgError; - +#[derive(Debug, Eq, PartialEq)] pub(crate) enum ComplaintReason { MalformedBTEPublicKey, MissingDealing, - MalformedDealing(DkgError), - DealingVerificationError(DkgError), + MalformedDealing, + DealingVerificationError, } diff --git a/validator-api/src/coconut/dkg/controller.rs b/validator-api/src/coconut/dkg/controller.rs index 9058dc558b..0cd538793b 100644 --- a/validator-api/src/coconut/dkg/controller.rs +++ b/validator-api/src/coconut/dkg/controller.rs @@ -74,7 +74,7 @@ impl DkgController { }) } - async fn handle_epoch_state(&mut self) { + pub(crate) async fn handle_epoch_state(&mut self) { match self.dkg_client.get_current_epoch_state().await { Err(e) => warn!("Could not get current epoch state {}", e), Ok(epoch_state) => { diff --git a/validator-api/src/coconut/dkg/dealing.rs b/validator-api/src/coconut/dkg/dealing.rs index fea2d4e98f..ed61276164 100644 --- a/validator-api/src/coconut/dkg/dealing.rs +++ b/validator-api/src/coconut/dkg/dealing.rs @@ -50,3 +50,101 @@ pub(crate) async fn dealing_exchange( Ok(()) } + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::coconut::tests::DummyClient; + use crate::coconut::KeyPair; + use coconut_dkg_common::dealer::DealerDetails; + use cosmwasm_std::Addr; + use dkg::bte::keys::KeyPair as DkgKeyPair; + use dkg::bte::Params; + use rand::rngs::OsRng; + use std::collections::HashMap; + use std::str::FromStr; + use std::sync::{Arc, RwLock}; + use url::Url; + use validator_client::nymd::AccountId; + + const TEST_VALIDATORS_ADDRESS: [&str; 3] = [ + "n1aq9kakfgwqcufr23lsv644apavcntrsqsk4yus", + "n1s9l3xr4g0rglvk4yctktmck3h4eq0gp6z2e20v", + "n19kl4py32vsk297dm93ezem992cdyzdy4zuc2x6", + ]; + + fn insert_dealers( + params: &Params, + dealer_details_db: &Arc>>, + ) -> Vec { + let mut keypairs = vec![]; + for (idx, addr) in TEST_VALIDATORS_ADDRESS.iter().enumerate() { + let keypair = DkgKeyPair::new(params, OsRng); + let bte_public_key_with_proof = + bs58::encode(&keypair.public_key().to_bytes()).into_string(); + keypairs.push(keypair); + dealer_details_db.write().unwrap().insert( + addr.to_string(), + DealerDetails { + address: Addr::unchecked(*addr), + bte_public_key_with_proof, + announce_address: format!("localhost:80{}", idx), + assigned_index: (idx + 1) as u64, + }, + ); + } + keypairs + } + + #[tokio::test] + async fn 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 dkg_client = DkgClient::new( + DummyClient::new(AccountId::from_str(TEST_VALIDATORS_ADDRESS[0]).unwrap()) + .with_dealer_details(&dealer_details_db) + .with_dealings(&dealings_db), + ); + let params = setup(); + let mut state = State::new( + Url::parse("localhost:8000").unwrap(), + DkgKeyPair::new(¶ms, OsRng), + KeyPair::new(), + ); + state.set_node_index(Some(self_index)); + let keypairs = insert_dealers(¶ms, &dealer_details_db); + + dealing_exchange(&dkg_client, &mut state, OsRng) + .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(), 2); + assert_eq!(state.receiver_index().unwrap(), 1); + let dealings = dealings_db + .read() + .unwrap() + .get(TEST_VALIDATORS_ADDRESS[0]) + .unwrap() + .clone(); + assert_eq!(dealings.len(), TOTAL_DEALINGS); + + dealing_exchange(&dkg_client, &mut state, OsRng) + .await + .unwrap(); + let new_dealings = dealings_db + .read() + .unwrap() + .get(TEST_VALIDATORS_ADDRESS[0]) + .unwrap() + .clone(); + assert_eq!(dealings, new_dealings); + } +} diff --git a/validator-api/src/coconut/dkg/public_key.rs b/validator-api/src/coconut/dkg/public_key.rs index 6669db09ca..4497a3b6b7 100644 --- a/validator-api/src/coconut/dkg/public_key.rs +++ b/validator-api/src/coconut/dkg/public_key.rs @@ -25,8 +25,59 @@ pub(crate) async fn public_key_submission( .register_dealer(bte_key, state.announce_address().to_string()) .await? }; - state.set_node_index(index); + state.set_node_index(Some(index)); info!("DKG: Using node index {}", index); Ok(()) } + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::coconut::tests::DummyClient; + use crate::coconut::KeyPair; + use dkg::bte::keys::KeyPair as DkgKeyPair; + use rand::rngs::OsRng; + use std::str::FromStr; + use url::Url; + use validator_client::nymd::AccountId; + + const TEST_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; + + #[tokio::test] + async fn submit_public_key() { + let dkg_client = DkgClient::new(DummyClient::new( + AccountId::from_str(TEST_VALIDATOR_ADDRESS).unwrap(), + )); + let mut state = State::new( + Url::parse("localhost:8000").unwrap(), + DkgKeyPair::new(&dkg::bte::setup(), OsRng), + KeyPair::new(), + ); + + assert!(dkg_client + .get_self_registered_dealer_details() + .await + .unwrap() + .details + .is_none()); + public_key_submission(&dkg_client, &mut state) + .await + .unwrap(); + let client_idx = dkg_client + .get_self_registered_dealer_details() + .await + .unwrap() + .details + .unwrap() + .assigned_index; + assert_eq!(state.node_index().unwrap(), client_idx); + + // keeps the same index from chain, not calling register_dealer again + state.set_node_index(None); + public_key_submission(&dkg_client, &mut state) + .await + .unwrap(); + assert_eq!(state.node_index().unwrap(), client_idx); + } +} diff --git a/validator-api/src/coconut/dkg/state.rs b/validator-api/src/coconut/dkg/state.rs index f6be2bc8a7..b0b550304a 100644 --- a/validator-api/src/coconut/dkg/state.rs +++ b/validator-api/src/coconut/dkg/state.rs @@ -214,8 +214,8 @@ impl State { self.coconut_keypair.set(coconut_keypair).await } - pub fn set_node_index(&mut self, node_index: NodeIndex) { - self.node_index = Some(node_index); + pub fn set_node_index(&mut self, node_index: Option) { + self.node_index = node_index; } pub fn set_dealers(&mut self, dealers: Vec) { @@ -255,4 +255,9 @@ impl State { pub fn set_executed_proposal(&mut self) { self.executed_proposal = true; } + + #[cfg(test)] + pub fn all_dealers(&self) -> &BTreeMap> { + &self.dealers + } } diff --git a/validator-api/src/coconut/dkg/verification_key.rs b/validator-api/src/coconut/dkg/verification_key.rs index 08c352255d..1971ab9653 100644 --- a/validator-api/src/coconut/dkg/verification_key.rs +++ b/validator-api/src/coconut/dkg/verification_key.rs @@ -37,12 +37,13 @@ async fn deterministic_filter_dealers( BTreeMap::from_iter(dealings.into_iter().filter_map(|contract_dealing| { match Dealing::try_from(&contract_dealing.dealing) { Ok(dealing) => { - if let Err(err) = - dealing.verify(¶ms, threshold, &initial_receivers, None) + if dealing + .verify(¶ms, threshold, &initial_receivers, None) + .is_err() { state.mark_bad_dealer( &contract_dealing.dealer, - ComplaintReason::DealingVerificationError(err), + ComplaintReason::DealingVerificationError, ); None } else if let Some(idx) = @@ -53,10 +54,10 @@ async fn deterministic_filter_dealers( None } } - Err(err) => { + Err(_) => { state.mark_bad_dealer( &contract_dealing.dealer, - ComplaintReason::MalformedDealing(err), + ComplaintReason::MalformedDealing, ); None } @@ -242,3 +243,546 @@ pub(crate) async fn verification_key_finalization( Ok(()) } + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::coconut::dkg::dealing::dealing_exchange; + use crate::coconut::dkg::public_key::public_key_submission; + use crate::coconut::tests::DummyClient; + use crate::coconut::KeyPair; + use coconut_dkg_common::dealer::DealerDetails; + use coconut_dkg_common::verification_key::ContractVKShare; + use contracts_common::dealings::ContractSafeBytes; + use dkg::bte::keys::KeyPair as DkgKeyPair; + use rand::rngs::OsRng; + use rand::Rng; + use std::collections::HashMap; + use std::env::temp_dir; + use std::str::FromStr; + use std::sync::{Arc, RwLock}; + use url::Url; + use validator_client::nymd::AccountId; + + const TEST_VALIDATORS_ADDRESS: [&str; 3] = [ + "n1aq9kakfgwqcufr23lsv644apavcntrsqsk4yus", + "n1s9l3xr4g0rglvk4yctktmck3h4eq0gp6z2e20v", + "n19kl4py32vsk297dm93ezem992cdyzdy4zuc2x6", + ]; + + async fn prepare_clients_and_states( + dealer_details_db: &Arc>>, + dealings_db: &Arc>>>, + proposal_db: &Arc>>, + verification_share_db: &Arc>>, + ) -> Vec<(DkgClient, State)> { + let params = setup(); + let mut clients_and_states = vec![]; + + for addr in TEST_VALIDATORS_ADDRESS { + let dkg_client = DkgClient::new( + DummyClient::new(AccountId::from_str(addr).unwrap()) + .with_dealer_details(dealer_details_db) + .with_dealings(dealings_db) + .with_proposal_db(proposal_db) + .with_verification_share(verification_share_db), + ); + let keypair = DkgKeyPair::new(¶ms, OsRng); + let state = State::new( + Url::parse("localhost:8000").unwrap(), + keypair, + KeyPair::new(), + ); + 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(); + } + for (dkg_client, state) in clients_and_states.iter_mut() { + dealing_exchange(dkg_client, state, OsRng).await.unwrap(); + } + clients_and_states + } + + async fn prepare_clients_and_states_with_submission( + dealer_details_db: &Arc>>, + dealings_db: &Arc>>>, + proposal_db: &Arc>>, + verification_share_db: &Arc>>, + ) -> Vec<(DkgClient, State)> { + let mut clients_and_states = prepare_clients_and_states( + dealer_details_db, + dealings_db, + proposal_db, + verification_share_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) + .await + .unwrap(); + std::fs::remove_file(private_key_path).unwrap(); + std::fs::remove_file(public_key_path).unwrap(); + } + clients_and_states + } + + async fn prepare_clients_and_states_with_validation( + dealer_details_db: &Arc>>, + dealings_db: &Arc>>>, + proposal_db: &Arc>>, + verification_share_db: &Arc>>, + ) -> Vec<(DkgClient, State)> { + let mut clients_and_states = prepare_clients_and_states_with_submission( + dealer_details_db, + dealings_db, + proposal_db, + verification_share_db, + ) + .await; + for (dkg_client, state) in clients_and_states.iter_mut() { + verification_key_validation(dkg_client, state) + .await + .unwrap(); + } + clients_and_states + } + + async fn prepare_clients_and_states_with_finalization( + dealer_details_db: &Arc>>, + dealings_db: &Arc>>>, + proposal_db: &Arc>>, + verification_share_db: &Arc>>, + ) -> Vec<(DkgClient, State)> { + let mut clients_and_states = prepare_clients_and_states_with_validation( + dealer_details_db, + dealings_db, + proposal_db, + verification_share_db, + ) + .await; + for (dkg_client, state) in clients_and_states.iter_mut() { + verification_key_finalization(dkg_client, state) + .await + .unwrap(); + } + clients_and_states + } + + #[tokio::test] + async fn check_dealers_filter_all_good() { + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let verification_share_db = Arc::new(RwLock::new(HashMap::new())); + let mut clients_and_states = prepare_clients_and_states( + &dealer_details_db, + &dealings_db, + &proposal_db, + &verification_share_db, + ) + .await; + for (dkg_client, state) in clients_and_states.iter_mut() { + let filtered = deterministic_filter_dealers(dkg_client, state, 2) + .await + .unwrap(); + assert_eq!(filtered.len(), TOTAL_DEALINGS); + for mapping in filtered.iter() { + assert_eq!(mapping.len(), 3); + } + } + } + + #[tokio::test] + async fn check_dealers_filter_one_bad_dealing() { + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let verification_share_db = Arc::new(RwLock::new(HashMap::new())); + let mut clients_and_states = prepare_clients_and_states( + &dealer_details_db, + &dealings_db, + &proposal_db, + &verification_share_db, + ) + .await; + + // corrupt just one dealing + dealings_db + .write() + .unwrap() + .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + .and_modify(|dealings| { + let mut last = dealings.pop().unwrap(); + last.0.pop(); + dealings.push(last); + }); + + for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { + let filtered = deterministic_filter_dealers(dkg_client, state, 2) + .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] + async fn check_dealers_filter_all_bad_dealings() { + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let verification_share_db = Arc::new(RwLock::new(HashMap::new())); + let mut clients_and_states = prepare_clients_and_states( + &dealer_details_db, + &dealings_db, + &proposal_db, + &verification_share_db, + ) + .await; + + // corrupt all dealings of one address + dealings_db + .write() + .unwrap() + .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + .and_modify(|dealings| { + dealings.iter_mut().for_each(|dealing| { + dealing.0.pop(); + }); + }); + + for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { + let filtered = deterministic_filter_dealers(dkg_client, state, 2) + .await + .unwrap(); + assert_eq!(filtered.len(), TOTAL_DEALINGS); + for mapping in filtered.iter() { + assert_eq!(mapping.len(), 2); + } + 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] + async fn check_dealers_filter_malformed_dealing() { + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let verification_share_db = Arc::new(RwLock::new(HashMap::new())); + let mut clients_and_states = prepare_clients_and_states( + &dealer_details_db, + &dealings_db, + &proposal_db, + &verification_share_db, + ) + .await; + + // corrupt just one dealing + dealings_db + .write() + .unwrap() + .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + .and_modify(|dealings| { + let mut last = dealings.pop().unwrap(); + last.0.pop(); + dealings.push(last); + }); + + for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { + deterministic_filter_dealers(dkg_client, state, 2) + .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) + .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::MalformedDealing); + } + } + + #[tokio::test] + async fn check_dealers_filter_dealing_verification_error() { + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let verification_share_db = Arc::new(RwLock::new(HashMap::new())); + let mut clients_and_states = prepare_clients_and_states( + &dealer_details_db, + &dealings_db, + &proposal_db, + &verification_share_db, + ) + .await; + + // corrupt just one dealing + dealings_db + .write() + .unwrap() + .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + .and_modify(|dealings| { + let mut last = dealings.pop().unwrap(); + last.0.pop(); + last.0.push(42); + dealings.push(last); + }); + + for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { + deterministic_filter_dealers(dkg_client, state, 2) + .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) + .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::DealingVerificationError); + } + } + + #[tokio::test] + async fn partial_keypair_derivation() { + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let verification_share_db = Arc::new(RwLock::new(HashMap::new())); + let mut clients_and_states = prepare_clients_and_states( + &dealer_details_db, + &dealings_db, + &proposal_db, + &verification_share_db, + ) + .await; + for (dkg_client, state) in clients_and_states.iter_mut() { + let filtered = deterministic_filter_dealers(dkg_client, state, 2) + .await + .unwrap(); + assert!(derive_partial_keypair(state, 2, filtered).is_ok()); + } + } + + #[tokio::test] + async fn partial_keypair_derivation_with_threshold() { + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let verification_share_db = Arc::new(RwLock::new(HashMap::new())); + let mut clients_and_states = prepare_clients_and_states( + &dealer_details_db, + &dealings_db, + &proposal_db, + &verification_share_db, + ) + .await; + + // corrupt just one dealing + dealings_db + .write() + .unwrap() + .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + .and_modify(|dealings| { + let mut last = dealings.pop().unwrap(); + last.0.pop(); + dealings.push(last); + }); + + for (dkg_client, state) in clients_and_states.iter_mut().skip(1) { + let filtered = deterministic_filter_dealers(dkg_client, state, 2) + .await + .unwrap(); + assert!(derive_partial_keypair(state, 2, filtered).is_ok()); + } + } + + #[tokio::test] + async fn submit_verification_key() { + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let verification_share_db = Arc::new(RwLock::new(HashMap::new())); + let mut clients_and_states = prepare_clients_and_states_with_submission( + &dealer_details_db, + &dealings_db, + &proposal_db, + &verification_share_db, + ) + .await; + + for (_, state) in clients_and_states.iter_mut() { + assert!(proposal_db + .read() + .unwrap() + .contains_key(&state.proposal_id_value().unwrap())); + assert!(state.coconut_keypair_is_some().await); + } + } + + #[tokio::test] + async fn validate_verification_key() { + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let verification_share_db = Arc::new(RwLock::new(HashMap::new())); + let mut clients_and_states = prepare_clients_and_states_with_validation( + &dealer_details_db, + &dealings_db, + &proposal_db, + &verification_share_db, + ) + .await; + for (_, state) in clients_and_states.iter_mut() { + let proposal = proposal_db + .read() + .unwrap() + .get(&state.proposal_id_value().unwrap()) + .unwrap() + .clone(); + assert_eq!(proposal.status, Status::Passed); + } + } + + #[tokio::test] + async fn validate_verification_key_malformed_share() { + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let verification_share_db = Arc::new(RwLock::new(HashMap::new())); + let mut clients_and_states = prepare_clients_and_states_with_submission( + &dealer_details_db, + &dealings_db, + &proposal_db, + &verification_share_db, + ) + .await; + + verification_share_db + .write() + .unwrap() + .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + .and_modify(|share| share.share.push('x')); + + for (dkg_client, state) in clients_and_states.iter_mut() { + verification_key_validation(dkg_client, state) + .await + .unwrap(); + } + + for (idx, (_, state)) in clients_and_states.iter().enumerate() { + let proposal = proposal_db + .read() + .unwrap() + .get(&state.proposal_id_value().unwrap()) + .unwrap() + .clone(); + if idx == 0 { + assert_eq!(proposal.status, Status::Rejected); + } else { + assert_eq!(proposal.status, Status::Passed); + } + } + } + + #[tokio::test] + async fn validate_verification_key_unpaired_share() { + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let verification_share_db = Arc::new(RwLock::new(HashMap::new())); + let mut clients_and_states = prepare_clients_and_states_with_submission( + &dealer_details_db, + &dealings_db, + &proposal_db, + &verification_share_db, + ) + .await; + + let second_share = verification_share_db + .write() + .unwrap() + .get(TEST_VALIDATORS_ADDRESS[1]) + .unwrap() + .share + .clone(); + verification_share_db + .write() + .unwrap() + .entry(TEST_VALIDATORS_ADDRESS[0].to_string()) + .and_modify(|share| share.share = second_share); + + for (dkg_client, state) in clients_and_states.iter_mut() { + verification_key_validation(dkg_client, state) + .await + .unwrap(); + } + + for (idx, (_, state)) in clients_and_states.iter().enumerate() { + let proposal = proposal_db + .read() + .unwrap() + .get(&state.proposal_id_value().unwrap()) + .unwrap() + .clone(); + if idx == 0 { + assert_eq!(proposal.status, Status::Rejected); + } else { + assert_eq!(proposal.status, Status::Passed); + } + } + } + + #[tokio::test] + async fn finalize_verification_key() { + let dealer_details_db = Arc::new(RwLock::new(HashMap::new())); + let dealings_db = Arc::new(RwLock::new(HashMap::new())); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let verification_share_db = Arc::new(RwLock::new(HashMap::new())); + let clients_and_states = prepare_clients_and_states_with_finalization( + &dealer_details_db, + &dealings_db, + &proposal_db, + &verification_share_db, + ) + .await; + + for (_, state) in clients_and_states.iter() { + let proposal = proposal_db + .read() + .unwrap() + .get(&state.proposal_id_value().unwrap()) + .unwrap() + .clone(); + assert_eq!(proposal.status, Status::Executed); + } + } +} diff --git a/validator-api/src/coconut/mod.rs b/validator-api/src/coconut/mod.rs index 94bc96608d..25bcb4afa0 100644 --- a/validator-api/src/coconut/mod.rs +++ b/validator-api/src/coconut/mod.rs @@ -8,7 +8,7 @@ pub(crate) mod dkg; pub(crate) mod error; pub(crate) mod keypair; #[cfg(test)] -mod tests; +pub(crate) mod tests; use crate::coconut::client::Client as LocalClient; use crate::coconut::deposit::extract_encryption_key; diff --git a/validator-api/src/coconut/tests.rs b/validator-api/src/coconut/tests.rs index 842194b913..66aee4f745 100644 --- a/validator-api/src/coconut/tests.rs +++ b/validator-api/src/coconut/tests.rs @@ -36,48 +36,104 @@ use validator_client::validator_api::routes::{ use crate::coconut::State; use crate::ValidatorApiStorage; use async_trait::async_trait; -use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; -use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState}; +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, EpochState, TOTAL_DEALINGS}; use coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use contracts_common::dealings::ContractSafeBytes; use crypto::asymmetric::{encryption, identity}; use cw3::ProposalResponse; use rand_07::rngs::OsRng; +use rand_07::Rng; use rocket::http::Status; use rocket::local::asynchronous::Client; use std::collections::HashMap; use std::str::FromStr; use std::sync::{Arc, RwLock}; +use validator_client::nymd::cosmwasm_client::logs::Log; use validator_client::nymd::cosmwasm_client::types::ExecuteResult; const TEST_COIN_DENOM: &str = "unym"; const TEST_REWARDING_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; #[derive(Clone, Debug)] -struct DummyClient { +pub(crate) struct DummyClient { validator_address: AccountId, tx_db: Arc>>, proposal_db: Arc>>, spent_credential_db: Arc>>, + + epoch_state: Arc>, + dealer_details: Arc>>, + dealings: Arc>>>, + verification_share: Arc>>, } impl DummyClient { - pub fn new( - validator_address: AccountId, - tx_db: &Arc>>, - proposal_db: &Arc>>, - spent_credential_db: &Arc>>, - ) -> Self { - let tx_db = Arc::clone(tx_db); - let proposal_db = Arc::clone(proposal_db); - let spent_credential_db = Arc::clone(spent_credential_db); + pub fn new(validator_address: AccountId) -> Self { Self { validator_address, - tx_db, - proposal_db, - spent_credential_db, + tx_db: Arc::new(RwLock::new(HashMap::new())), + proposal_db: Arc::new(RwLock::new(HashMap::new())), + spent_credential_db: Arc::new(RwLock::new(HashMap::new())), + epoch_state: Arc::new(RwLock::new(EpochState::default())), + dealer_details: Arc::new(RwLock::new(HashMap::new())), + dealings: Arc::new(RwLock::new(HashMap::new())), + verification_share: Arc::new(RwLock::new(HashMap::new())), } } + + pub fn with_tx_db(mut self, tx_db: &Arc>>) -> Self { + self.tx_db = Arc::clone(tx_db); + self + } + + pub fn with_proposal_db( + mut self, + proposal_db: &Arc>>, + ) -> Self { + self.proposal_db = Arc::clone(proposal_db); + self + } + + pub fn with_spent_credential_db( + mut self, + spent_credential_db: &Arc>>, + ) -> Self { + self.spent_credential_db = Arc::clone(spent_credential_db); + self + } + + pub fn _with_epoch_state(mut self, epoch_state: &Arc>) -> Self { + self.epoch_state = Arc::clone(epoch_state); + self + } + + pub fn with_dealer_details( + mut self, + dealer_details: &Arc>>, + ) -> Self { + self.dealer_details = Arc::clone(dealer_details); + self + } + + pub fn with_dealings( + mut self, + dealings: &Arc>>>, + ) -> Self { + self.dealings = Arc::clone(dealings); + self + } + + pub fn with_verification_share( + mut self, + verification_share: &Arc>>, + ) -> Self { + self.verification_share = Arc::clone(verification_share); + self + } } #[async_trait] @@ -107,7 +163,7 @@ impl super::client::Client for DummyClient { } async fn list_proposals(&self) -> Result> { - todo!() + Ok(self.proposal_db.read().unwrap().values().cloned().collect()) } async fn get_spent_credential( @@ -125,23 +181,52 @@ impl super::client::Client for DummyClient { } async fn get_current_epoch_state(&self) -> Result { - todo!() + Ok(*self.epoch_state.read().unwrap()) } async fn get_self_registered_dealer_details(&self) -> Result { - todo!() + Ok(DealerDetailsResponse { + details: self + .dealer_details + .read() + .unwrap() + .get(self.validator_address.as_ref()) + .cloned(), + dealer_type: DealerType::Current, + }) } async fn get_current_dealers(&self) -> Result> { - todo!() + Ok(self + .dealer_details + .read() + .unwrap() + .values() + .cloned() + .collect()) } - async fn get_dealings(&self, _idx: usize) -> Result> { - todo!() + async fn get_dealings(&self, idx: usize) -> Result> { + Ok(self + .dealings + .read() + .unwrap() + .iter() + .map(|(dealer, dealings)| ContractDealing { + dealing: dealings.get(idx).unwrap().clone(), + dealer: Addr::unchecked(dealer), + }) + .collect()) } async fn get_verification_key_shares(&self) -> Result> { - todo!() + Ok(self + .verification_share + .read() + .unwrap() + .values() + .cloned() + .collect()) } async fn vote_proposal( @@ -151,36 +236,134 @@ impl super::client::Client for DummyClient { _fee: Option, ) -> Result<()> { if let Some(proposal) = self.proposal_db.write().unwrap().get_mut(&proposal_id) { - if vote_yes { - proposal.status = cw3::Status::Passed; - } else { - proposal.status = cw3::Status::Rejected; + // for now, just suppose that first vote is honest + if proposal.status == cw3::Status::Open { + if vote_yes { + proposal.status = cw3::Status::Passed; + } else { + proposal.status = cw3::Status::Rejected; + } } } Ok(()) } - async fn execute_proposal(&self, _proposal_id: u64) -> Result<()> { - todo!() + async fn execute_proposal(&self, proposal_id: u64) -> Result<()> { + self.proposal_db + .write() + .unwrap() + .entry(proposal_id) + .and_modify(|prop| { + if prop.status == cw3::Status::Passed { + prop.status = cw3::Status::Executed + } + }); + Ok(()) } async fn register_dealer( &self, - _bte_key: EncodedBTEPublicKeyWithProof, - _announce_address: String, + bte_public_key_with_proof: EncodedBTEPublicKeyWithProof, + announce_address: String, ) -> Result { - todo!() + 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, + }, + ); + Ok(ExecuteResult { + logs: vec![Log { + msg_index: 0, + events: vec![cosmwasm_std::Event::new("wasm") + .add_attribute(NODE_INDEX, assigned_index.to_string())], + }], + data: Default::default(), + transaction_hash: Hash::new([0; 32]), + gas_info: Default::default(), + }) } - async fn submit_dealing(&self, _dealing_bytes: ContractSafeBytes) -> Result { - todo!() + async fn submit_dealing(&self, dealing_bytes: ContractSafeBytes) -> Result { + self.dealings + .write() + .unwrap() + .entry(self.validator_address.to_string()) + .and_modify(|v| { + if v.len() < TOTAL_DEALINGS { + v.push(dealing_bytes.clone()) + } + }) + .or_insert_with(|| vec![dealing_bytes]); + + Ok(ExecuteResult { + logs: vec![], + data: Default::default(), + transaction_hash: Hash::new([0; 32]), + gas_info: Default::default(), + }) } async fn submit_verification_key_share( &self, - _share: VerificationKeyShare, + share: VerificationKeyShare, ) -> Result { - todo!() + let dealer_details = self + .dealer_details + .read() + .unwrap() + .get(self.validator_address.as_ref()) + .unwrap() + .clone(); + self.verification_share.write().unwrap().insert( + self.validator_address.to_string(), + ContractVKShare { + share, + announce_address: dealer_details.announce_address.clone(), + node_index: dealer_details.assigned_index, + owner: Addr::unchecked(self.validator_address.to_string()), + verified: false, + }, + ); + let proposal_id = OsRng.gen(); + let verify_vk_share_req = coconut_dkg_common::msg::ExecuteMsg::VerifyVerificationKeyShare { + owner: Addr::unchecked(self.validator_address.as_ref()), + }; + let verify_vk_share_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: String::new(), + msg: to_binary(&verify_vk_share_req).unwrap(), + funds: vec![], + }); + let proposal = ProposalResponse { + id: proposal_id, + title: String::new(), + description: String::new(), + msgs: vec![verify_vk_share_msg], + status: cw3::Status::Open, + expires: cw_utils::Expiration::Never {}, + threshold: cw_utils::ThresholdResponse::AbsolutePercentage { + percentage: Decimal::from_ratio(2u32, 3u32), + total_weight: 100, + }, + }; + self.proposal_db + .write() + .unwrap() + .insert(proposal_id, proposal); + Ok(ExecuteResult { + logs: vec![Log { + msg_index: 0, + events: vec![cosmwasm_std::Event::new("wasm") + .add_attribute(DKG_PROPOSAL_ID, proposal_id.to_string())], + }], + data: Default::default(), + transaction_hash: Hash::new([0; 32]), + gas_info: Default::default(), + }) } } @@ -267,12 +450,9 @@ async fn signed_before() { .write() .unwrap() .insert(tx_hash.to_string(), tx_entry.clone()); - let nymd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - &tx_db, - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - ); + let nymd_client = + DummyClient::new(AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap()) + .with_tx_db(&tx_db); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); let staged_key_pair = crate::coconut::KeyPair::new(); staged_key_pair.set(key_pair).await; @@ -334,12 +514,8 @@ async fn signed_before() { #[tokio::test] async fn state_functions() { - let nymd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - ); + let nymd_client = + DummyClient::new(AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap()); let params = Parameters::new(4).unwrap(); let key_pair = ttp_keygen(¶ms, 1, 1).unwrap().remove(0); let mut db_dir = std::env::temp_dir(); @@ -511,12 +687,9 @@ async fn blind_sign_correct() { .write() .unwrap() .insert(tx_hash.to_string(), tx_entry.clone()); - let nymd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - &tx_db, - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - ); + let nymd_client = + DummyClient::new(AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap()) + .with_tx_db(&tx_db); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); let staged_key_pair = crate::coconut::KeyPair::new(); staged_key_pair.set(key_pair).await; @@ -592,12 +765,8 @@ async fn signature_test() { let mut db_dir = std::env::temp_dir(); db_dir.push(&key_pair.verification_key().to_bs58()[..8]); let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); - let nymd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - ); + let nymd_client = + DummyClient::new(AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap()); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); let staged_key_pair = crate::coconut::KeyPair::new(); staged_key_pair.set(key_pair).await; @@ -663,12 +832,9 @@ async fn verification_of_bandwidth_credential() { let validator_address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(); let proposal_db = Arc::new(RwLock::new(HashMap::new())); let spent_credential_db = Arc::new(RwLock::new(HashMap::new())); - let nymd_client = DummyClient::new( - validator_address.clone(), - &Arc::new(RwLock::new(HashMap::new())), - &proposal_db, - &spent_credential_db, - ); + let nymd_client = DummyClient::new(validator_address.clone()) + .with_proposal_db(&proposal_db) + .with_spent_credential_db(&spent_credential_db); let mut db_dir = std::env::temp_dir(); let params = Parameters::new(4).unwrap(); let mut key_pairs = ttp_keygen(¶ms, 1, 1).unwrap();