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
This commit is contained in:
Bogdan-Ștefan Neacşu
2022-11-29 18:32:37 +02:00
committed by durch
parent 9a8218905e
commit 5c841de6ae
34 changed files with 2194 additions and 283 deletions
@@ -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<cosmwasm_std::Event>,
pub events: Vec<cosmwasm_std::Event>,
}
/// Searches in logs for the first event of the given event type and in that event
@@ -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<u8>);
pub struct ContractSafeBytes(pub Vec<u8>);
impl Deref for ContractSafeBytes {
type Target = Vec<u8>;
+3
View File
@@ -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",
+1 -1
View File
@@ -25,7 +25,7 @@ impl<'a> IndexList<SpendCredential> 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 {
+5 -1
View File
@@ -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"
thiserror = "1.0.23"
[dev-dependencies]
cw-multi-test = { version = "0.13.4" }
cw4-group = { path = "../multisig/cw4-group" }
+264
View File
@@ -0,0 +1,264 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<Response, ContractError> {
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<Response, ContractError> {
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<QueryResponse, ContractError> {
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<Response, ContractError> {
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::<u64>()
.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());
}
}
+164 -3
View File
@@ -10,7 +10,7 @@ fn query_dealers(
deps: Deps<'_>,
start_after: Option<String>,
limit: Option<u32>,
underlying_map: IndexedDealersMap<'_>,
underlying_map: &IndexedDealersMap<'_>,
) -> StdResult<PagedDealerResponse> {
let limit = limit
.unwrap_or(storage::DEALERS_PAGE_DEFAULT_LIMIT)
@@ -55,7 +55,7 @@ pub fn query_current_dealers_paged(
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<PagedDealerResponse> {
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<String>,
limit: Option<u32>,
) -> StdResult<PagedDealerResponse> {
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);
}
}
}
@@ -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(),
}
);
}
}
@@ -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());
}
}
}
@@ -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"),
}
);
}
}
@@ -3,4 +3,5 @@
pub mod queries;
pub mod storage;
pub mod transactions;
pub mod utils;
@@ -1,13 +1,28 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<EpochState, ContractError> {
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);
}
}
@@ -1,29 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<EpochState, ContractError> {
CURRENT_EPOCH_STATE
.load(storage)
.map_err(|_| ContractError::EpochNotInitialised)
}
pub(crate) fn advance_epoch_state(
deps: DepsMut<'_>,
info: MessageInfo,
) -> Result<Response, ContractError> {
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())
}
@@ -0,0 +1,78 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<Response, ContractError> {
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
);
}
}
+27 -1
View File
@@ -1,7 +1,8 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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());
}
}
}
}
}
+1 -24
View File
@@ -1,7 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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,
+3 -137
View File
@@ -1,146 +1,12 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<Response, ContractError> {
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<Response, ContractError> {
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<QueryResponse, ContractError> {
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<Response, ContractError> {
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())
}
}
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(test)]
pub mod tests;
@@ -0,0 +1,32 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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,
}
}
@@ -0,0 +1,27 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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<MemoryStorage, MockApi, MockQuerier<Empty>> {
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
}
@@ -0,0 +1,5 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod fixtures;
pub mod helpers;
@@ -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());
}
}
@@ -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();
}
}
+1
View File
@@ -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"
+3 -3
View File
@@ -42,9 +42,9 @@ pub fn mock_app(init_funds: &[Coin]) -> App {
}
pub fn contract_dkg() -> Box<dyn Contract<Empty>> {
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)
}
@@ -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::<u64>()
.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);
}
+3 -4
View File
@@ -1,11 +1,10 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// 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,
}
+1 -1
View File
@@ -74,7 +74,7 @@ impl<R: RngCore + Clone> DkgController<R> {
})
}
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) => {
+98
View File
@@ -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<RwLock<HashMap<String, DealerDetails>>>,
) -> Vec<DkgKeyPair> {
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(&params, OsRng),
KeyPair::new(),
);
state.set_node_index(Some(self_index));
let keypairs = insert_dealers(&params, &dealer_details_db);
dealing_exchange(&dkg_client, &mut state, OsRng)
.await
.unwrap();
assert_eq!(
state.current_dealers_by_idx().values().collect::<Vec<_>>(),
keypairs
.iter()
.map(|k| k.public_key().public_key())
.collect::<Vec<_>>()
);
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);
}
}
+52 -1
View File
@@ -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);
}
}
+7 -2
View File
@@ -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<NodeIndex>) {
self.node_index = node_index;
}
pub fn set_dealers(&mut self, dealers: Vec<DealerDetails>) {
@@ -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<Addr, Result<DkgParticipant, ComplaintReason>> {
&self.dealers
}
}
@@ -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(&params, threshold, &initial_receivers, None)
if dealing
.verify(&params, 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<RwLock<HashMap<String, DealerDetails>>>,
dealings_db: &Arc<RwLock<HashMap<String, Vec<ContractSafeBytes>>>>,
proposal_db: &Arc<RwLock<HashMap<u64, ProposalResponse>>>,
verification_share_db: &Arc<RwLock<HashMap<String, ContractVKShare>>>,
) -> 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(&params, 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<RwLock<HashMap<String, DealerDetails>>>,
dealings_db: &Arc<RwLock<HashMap<String, Vec<ContractSafeBytes>>>>,
proposal_db: &Arc<RwLock<HashMap<u64, ProposalResponse>>>,
verification_share_db: &Arc<RwLock<HashMap<String, ContractVKShare>>>,
) -> 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<RwLock<HashMap<String, DealerDetails>>>,
dealings_db: &Arc<RwLock<HashMap<String, Vec<ContractSafeBytes>>>>,
proposal_db: &Arc<RwLock<HashMap<u64, ProposalResponse>>>,
verification_share_db: &Arc<RwLock<HashMap<String, ContractVKShare>>>,
) -> 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<RwLock<HashMap<String, DealerDetails>>>,
dealings_db: &Arc<RwLock<HashMap<String, Vec<ContractSafeBytes>>>>,
proposal_db: &Arc<RwLock<HashMap<u64, ProposalResponse>>>,
verification_share_db: &Arc<RwLock<HashMap<String, ContractVKShare>>>,
) -> 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);
}
}
}
+1 -1
View File
@@ -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;
+231 -65
View File
@@ -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<RwLock<HashMap<String, TxResponse>>>,
proposal_db: Arc<RwLock<HashMap<u64, ProposalResponse>>>,
spent_credential_db: Arc<RwLock<HashMap<String, SpendCredentialResponse>>>,
epoch_state: Arc<RwLock<EpochState>>,
dealer_details: Arc<RwLock<HashMap<String, DealerDetails>>>,
dealings: Arc<RwLock<HashMap<String, Vec<ContractSafeBytes>>>>,
verification_share: Arc<RwLock<HashMap<String, ContractVKShare>>>,
}
impl DummyClient {
pub fn new(
validator_address: AccountId,
tx_db: &Arc<RwLock<HashMap<String, TxResponse>>>,
proposal_db: &Arc<RwLock<HashMap<u64, ProposalResponse>>>,
spent_credential_db: &Arc<RwLock<HashMap<String, SpendCredentialResponse>>>,
) -> 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<RwLock<HashMap<String, TxResponse>>>) -> Self {
self.tx_db = Arc::clone(tx_db);
self
}
pub fn with_proposal_db(
mut self,
proposal_db: &Arc<RwLock<HashMap<u64, ProposalResponse>>>,
) -> Self {
self.proposal_db = Arc::clone(proposal_db);
self
}
pub fn with_spent_credential_db(
mut self,
spent_credential_db: &Arc<RwLock<HashMap<String, SpendCredentialResponse>>>,
) -> Self {
self.spent_credential_db = Arc::clone(spent_credential_db);
self
}
pub fn _with_epoch_state(mut self, epoch_state: &Arc<RwLock<EpochState>>) -> Self {
self.epoch_state = Arc::clone(epoch_state);
self
}
pub fn with_dealer_details(
mut self,
dealer_details: &Arc<RwLock<HashMap<String, DealerDetails>>>,
) -> Self {
self.dealer_details = Arc::clone(dealer_details);
self
}
pub fn with_dealings(
mut self,
dealings: &Arc<RwLock<HashMap<String, Vec<ContractSafeBytes>>>>,
) -> Self {
self.dealings = Arc::clone(dealings);
self
}
pub fn with_verification_share(
mut self,
verification_share: &Arc<RwLock<HashMap<String, ContractVKShare>>>,
) -> 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<Vec<ProposalResponse>> {
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<EpochState> {
todo!()
Ok(*self.epoch_state.read().unwrap())
}
async fn get_self_registered_dealer_details(&self) -> Result<DealerDetailsResponse> {
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<Vec<DealerDetails>> {
todo!()
Ok(self
.dealer_details
.read()
.unwrap()
.values()
.cloned()
.collect())
}
async fn get_dealings(&self, _idx: usize) -> Result<Vec<ContractDealing>> {
todo!()
async fn get_dealings(&self, idx: usize) -> Result<Vec<ContractDealing>> {
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<Vec<ContractVKShare>> {
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<Fee>,
) -> 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<ExecuteResult> {
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<ExecuteResult> {
todo!()
async fn submit_dealing(&self, dealing_bytes: ContractSafeBytes) -> Result<ExecuteResult> {
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<ExecuteResult> {
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(&params, 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(&params, 1, 1).unwrap();