diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs index 50152ba409..7ee3f91eba 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs @@ -1,10 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::types::{ - DealingIndex, EncodedBTEPublicKeyWithProof, EpochId, NodeIndex, PartialContractDealing, - PartialContractDealingData, -}; +use crate::types::{EncodedBTEPublicKeyWithProof, NodeIndex}; use cosmwasm_schema::cw_serde; use cosmwasm_std::Addr; @@ -68,53 +65,3 @@ impl PagedDealerResponse { } } } - -#[cw_serde] -pub struct DealingResponse { - pub epoch_id: EpochId, - - pub dealer: Addr, - - pub dealing_index: DealingIndex, - - pub dealing: Option, -} - -#[cw_serde] -pub struct DealingStatusResponse { - pub epoch_id: EpochId, - - pub dealer: Addr, - - pub dealing_index: DealingIndex, - - pub dealing_submitted: bool, -} - -#[cw_serde] -pub struct PagedDealingsResponse { - pub epoch_id: EpochId, - - pub dealer: Addr, - - pub dealings: Vec, - - /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, -} - -impl PagedDealingsResponse { - pub fn new( - epoch_id: EpochId, - dealer: Addr, - dealings: Vec, - start_next_after: Option, - ) -> Self { - PagedDealingsResponse { - epoch_id, - dealer, - dealings, - start_next_after, - } - } -} diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealing.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealing.rs new file mode 100644 index 0000000000..849c093d7f --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealing.rs @@ -0,0 +1,198 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::{ChunkIndex, DealingIndex, EpochId, PartialContractDealingData}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Addr; +use std::collections::BTreeMap; + +/// Defines the maximum size of a dealing chunk. Currently set to 2kB +pub const MAX_DEALING_CHUNK_SIZE: usize = 2048; + +/// Defines the maximum size of a full dealing. +/// Currently set to 100kB (which is enough for a dealing created for 100 parties) +pub const MAX_DEALING_SIZE: usize = 102400; + +pub const MAX_DEALING_CHUNKS: usize = MAX_DEALING_SIZE / MAX_DEALING_CHUNK_SIZE; + +// 2 public attributes, 2 private attributes, 1 fixed for coconut credential +pub const DEFAULT_DEALINGS: usize = 2 + 2 + 1; + +#[cw_serde] +pub struct DealingChunkInfo { + pub size: usize, +} + +impl DealingChunkInfo { + pub fn new(size: usize) -> Self { + DealingChunkInfo { size } + } + + pub fn construct(dealing_len: usize, chunk_size: usize) -> Vec { + let (full_chunks, overflow) = (dealing_len / chunk_size, dealing_len % chunk_size); + + let mut chunks = Vec::new(); + for _ in 0..full_chunks { + chunks.push(DealingChunkInfo::new(chunk_size)); + } + + if overflow != 0 { + chunks.push(DealingChunkInfo::new(overflow)); + } + + chunks + } +} + +#[cw_serde] +pub struct SubmittedChunk { + pub info: DealingChunkInfo, + + // this field is updated by the contract itself to indicate when this particular chunk has been received + pub submission_height: Option, +} + +impl From for SubmittedChunk { + fn from(value: DealingChunkInfo) -> Self { + SubmittedChunk::new(value) + } +} + +impl SubmittedChunk { + pub fn new(info: DealingChunkInfo) -> Self { + SubmittedChunk { + info, + submission_height: None, + } + } +} + +#[cw_serde] +pub struct DealingMetadata { + pub dealing_index: DealingIndex, + + pub submitted_chunks: BTreeMap, +} + +impl DealingMetadata { + pub fn new(dealing_index: DealingIndex, chunks: Vec) -> Self { + DealingMetadata { + dealing_index, + submitted_chunks: chunks + .into_iter() + .enumerate() + .map(|(id, chunk)| (id as ChunkIndex, chunk.into())) + .collect(), + } + } + + pub fn is_complete(&self) -> bool { + self.submitted_chunks + .values() + .all(|c| c.submission_height.is_some()) + } + + pub fn total_size(&self) -> usize { + self.submitted_chunks.values().map(|c| c.info.size).sum() + } +} + +#[cw_serde] +pub struct PartialContractDealing { + pub dealing_index: DealingIndex, + pub chunk_index: ChunkIndex, + pub data: PartialContractDealingData, +} + +impl PartialContractDealing { + pub fn new( + dealing_index: DealingIndex, + chunk_index: ChunkIndex, + data: PartialContractDealingData, + ) -> Self { + PartialContractDealing { + dealing_index, + chunk_index, + data, + } + } +} + +#[cw_serde] +pub struct DealingMetadataResponse { + pub epoch_id: EpochId, + + pub dealer: Addr, + + pub dealing_index: DealingIndex, + + pub metadata: Option, +} + +#[cw_serde] +pub struct DealingChunkResponse { + pub epoch_id: EpochId, + + pub dealer: Addr, + + pub dealing_index: DealingIndex, + + pub chunk_index: ChunkIndex, + + pub chunk: Option, +} + +#[cw_serde] +pub struct DealingChunkStatusResponse { + pub epoch_id: EpochId, + + pub dealer: Addr, + + pub dealing_index: DealingIndex, + + pub chunk_index: ChunkIndex, + + pub submission_height: Option, +} + +#[cw_serde] +pub struct DealingStatusResponse { + pub epoch_id: EpochId, + + pub dealer: Addr, + + pub dealing_index: DealingIndex, + + pub full_dealing_submitted: bool, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn chunking_dealings() { + const CHUNK_SIZE: usize = 512; + + let test_cases = [ + (CHUNK_SIZE - 10, CHUNK_SIZE, 1), + (CHUNK_SIZE, CHUNK_SIZE, 1), + (CHUNK_SIZE + 10, CHUNK_SIZE, 2), + (CHUNK_SIZE * 2, CHUNK_SIZE, 2), + (CHUNK_SIZE * 2 + 1, CHUNK_SIZE, 3), + (CHUNK_SIZE * 10 + 42, CHUNK_SIZE, 11), + ]; + + for (dealing_len, chunk_size, expected_chunks) in test_cases { + let chunks = DealingChunkInfo::construct(dealing_len, chunk_size); + assert_eq!(expected_chunks, chunks.len()); + assert_eq!(dealing_len, chunks.iter().map(|c| c.size).sum::()); + + let mut expected_last = dealing_len % chunk_size; + if expected_last == 0 { + expected_last = chunk_size; + } + assert_eq!(chunks.last().unwrap().size, expected_last); + } + } +} diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs index 545451eeb6..cd28eb9eb2 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs @@ -1,4 +1,8 @@ +// Copyright 2022-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + pub mod dealer; +pub mod dealing; pub mod event_attributes; pub mod msg; pub mod types; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs index f92b8b3ea6..95a7ae8445 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -1,23 +1,24 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::dealing::{DealingChunkInfo, PartialContractDealing}; use crate::types::{ - DealingIndex, EncodedBTEPublicKeyWithProof, EpochId, PartialContractDealing, TimeConfiguration, + ChunkIndex, DealingIndex, EncodedBTEPublicKeyWithProof, EpochId, TimeConfiguration, }; use crate::verification_key::VerificationKeyShare; +use contracts_common::IdentityKey; use cosmwasm_schema::cw_serde; -use cosmwasm_std::Addr; #[cfg(feature = "schema")] use crate::{ - dealer::{ - DealerDetailsResponse, DealingResponse, DealingStatusResponse, PagedDealerResponse, - PagedDealingsResponse, + dealer::{DealerDetailsResponse, PagedDealerResponse}, + dealing::{ + DealingChunkResponse, DealingChunkStatusResponse, DealingMetadataResponse, + DealingStatusResponse, }, types::{Epoch, InitialReplacementData, State}, verification_key::{PagedVKSharesResponse, VkShareResponse}, }; -use contracts_common::IdentityKey; #[cfg(feature = "schema")] use cosmwasm_schema::QueryResponses; @@ -44,8 +45,14 @@ pub enum ExecuteMsg { resharing: bool, }, - CommitDealing { - dealing: PartialContractDealing, + CommitDealingsMetadata { + dealing_index: DealingIndex, + chunks: Vec, + resharing: bool, + }, + + CommitDealingsChunk { + chunk: PartialContractDealing, resharing: bool, }, @@ -55,8 +62,7 @@ pub enum ExecuteMsg { }, VerifyVerificationKeyShare { - // TODO: this should be using a String... - owner: Addr, + owner: String, resharing: bool, }, @@ -95,6 +101,13 @@ pub enum QueryMsg { start_after: Option, }, + #[cfg_attr(feature = "schema", returns(DealingMetadataResponse))] + GetDealingsMetadata { + epoch_id: EpochId, + dealer: String, + dealing_index: DealingIndex, + }, + #[cfg_attr(feature = "schema", returns(DealingStatusResponse))] GetDealingStatus { epoch_id: EpochId, @@ -102,19 +115,20 @@ pub enum QueryMsg { dealing_index: DealingIndex, }, - #[cfg_attr(feature = "schema", returns(DealingResponse))] - GetDealing { + #[cfg_attr(feature = "schema", returns(DealingChunkStatusResponse))] + GetDealingChunkStatus { epoch_id: EpochId, dealer: String, dealing_index: DealingIndex, + chunk_index: ChunkIndex, }, - #[cfg_attr(feature = "schema", returns(PagedDealingsResponse))] - GetDealings { + #[cfg_attr(feature = "schema", returns(DealingChunkResponse))] + GetDealingChunk { epoch_id: EpochId, dealer: String, - limit: Option, - start_after: Option, + dealing_index: DealingIndex, + chunk_index: ChunkIndex, }, #[cfg_attr(feature = "schema", returns(VkShareResponse))] diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs index 0ccbac4f5a..82dff337d4 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use cosmwasm_schema::cw_serde; -use std::collections::BTreeMap; use std::fmt::{Display, Formatter}; use std::str::FromStr; @@ -22,102 +21,6 @@ pub type DealingIndex = u32; pub type ChunkIndex = u16; pub type PartialContractDealingData = ContractSafeBytes; -/// Defines the maximum size of a dealing chunk. Currently set to 2kB -pub const MAX_DEALING_CHUNK_SIZE: usize = 2048; - -/// Defines the maximum size of a full dealing. -/// Currently set to 100kB (which is enough for a dealing created for 100 parties) -pub const MAX_DEALING_SIZE: usize = 102400; - -pub const MAX_DEALING_CHUNKS: usize = MAX_DEALING_SIZE / MAX_DEALING_CHUNK_SIZE; - -// 2 public attributes, 2 private attributes, 1 fixed for coconut credential -pub const DEFAULT_DEALINGS: usize = 2 + 2 + 1; - -#[cw_serde] -pub struct SubmittedChunk { - pub size: usize, - - // this field is updated by the contract itself to indicate when this particular chunk has been received - pub submission_height: Option, -} - -impl SubmittedChunk { - pub fn new(size: usize) -> Self { - SubmittedChunk { - size, - submission_height: None, - } - } -} - -#[cw_serde] -pub struct DealingMetadata { - pub dealing_index: DealingIndex, - - pub submitted_chunks: BTreeMap, -} - -impl DealingMetadata { - pub fn new(dealing_index: DealingIndex, dealing_len: usize, chunk_size: usize) -> Self { - let (full_chunks, overflow) = (dealing_len / chunk_size, dealing_len % chunk_size); - - let mut submitted_chunks = BTreeMap::new(); - for id in 0..full_chunks { - submitted_chunks.insert(id as ChunkIndex, SubmittedChunk::new(chunk_size)); - } - - if overflow != 0 { - submitted_chunks.insert(full_chunks as ChunkIndex, SubmittedChunk::new(overflow)); - } - - DealingMetadata { - dealing_index, - submitted_chunks, - } - } - - pub fn is_complete(&self) -> bool { - self.submitted_chunks - .values() - .all(|c| c.submission_height.is_some()) - } - - pub fn total_size(&self) -> usize { - self.submitted_chunks.values().map(|c| c.size).sum() - } -} - -#[cw_serde] -pub struct PartialContractDealing { - pub dealing_index: DealingIndex, - pub chunk_index: ChunkIndex, - pub data: PartialContractDealingData, -} - -impl PartialContractDealing { - pub fn new( - dealing_index: DealingIndex, - chunk_index: ChunkIndex, - data: PartialContractDealingData, - ) -> Self { - PartialContractDealing { - dealing_index, - chunk_index, - data, - } - } -} - -// impl From<(DealingIndex, ContractDealing)> for PartialContractDealing { -// fn from(value: (DealingIndex, ContractDealing)) -> Self { -// PartialContractDealing { -// dealing_index: value.0, -// data: value.1, -// } -// } -// } - #[cw_serde] pub struct InitialReplacementData { pub initial_dealers: Vec, @@ -350,37 +253,3 @@ impl EpochState { *self == EpochState::InProgress } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn dealing_metadata() { - const CHUNK_SIZE: usize = 512; - - let test_cases = [ - (CHUNK_SIZE - 10, CHUNK_SIZE, 1), - (CHUNK_SIZE, CHUNK_SIZE, 1), - (CHUNK_SIZE + 10, CHUNK_SIZE, 2), - (CHUNK_SIZE * 2, CHUNK_SIZE, 2), - (CHUNK_SIZE * 2 + 1, CHUNK_SIZE, 3), - (CHUNK_SIZE * 10 + 42, CHUNK_SIZE, 11), - ]; - - for (dealing_len, chunk_size, expected_chunks) in test_cases { - let meta = DealingMetadata::new(42, dealing_len, chunk_size); - assert_eq!(expected_chunks, meta.submitted_chunks.len()); - assert_eq!(dealing_len, meta.total_size()); - - let mut expected_last = dealing_len % chunk_size; - if expected_last == 0 { - expected_last = chunk_size; - } - assert_eq!( - meta.submitted_chunks.last_key_value().unwrap().1.size, - expected_last - ); - } - } -} diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs index 3137a5bdd9..e3a66a78a4 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs @@ -43,7 +43,10 @@ pub fn to_cosmos_msg( multisig_addr: String, expiration_time: Timestamp, ) -> StdResult { - let verify_vk_share_req = ExecuteMsg::VerifyVerificationKeyShare { owner, resharing }; + let verify_vk_share_req = ExecuteMsg::VerifyVerificationKeyShare { + owner: owner.to_string(), + resharing, + }; let verify_vk_share_msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: coconut_dkg_addr, msg: to_binary(&verify_vk_share_req)?, @@ -67,7 +70,7 @@ pub fn to_cosmos_msg( // DKG SAFETY: // each legit verification proposal will only contain a single execute msg, // if they have more than one, we can safely ignore it -pub fn owner_from_cosmos_msgs(msgs: &[CosmosMsg]) -> Option { +pub fn owner_from_cosmos_msgs(msgs: &[CosmosMsg]) -> Option { if msgs.len() != 1 { return None; } diff --git a/contracts/coconut-dkg/src/contract.rs b/contracts/coconut-dkg/src/contract.rs index 84af041d0a..4680abc0f3 100644 --- a/contracts/coconut-dkg/src/contract.rs +++ b/contracts/coconut-dkg/src/contract.rs @@ -5,8 +5,10 @@ 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_dealing, query_dealing_status, query_dealings_paged}; -use crate::dealings::transactions::try_commit_dealings_chunk; +use crate::dealings::queries::{ + query_dealing_chunk, query_dealing_chunk_status, query_dealing_metadata, query_dealing_status, +}; +use crate::dealings::transactions::{try_commit_dealings_chunk, try_submit_dealings_metadata}; use crate::epoch_state::queries::{ query_current_epoch, query_current_epoch_threshold, query_initial_dealers, }; @@ -100,8 +102,13 @@ pub fn execute( announce_address, resharing, ), - ExecuteMsg::CommitDealing { dealing, resharing } => { - try_commit_dealings_chunk(deps, info, dealing, resharing) + ExecuteMsg::CommitDealingsMetadata { + dealing_index, + chunks, + resharing, + } => try_submit_dealings_metadata(deps, info, dealing_index, chunks, resharing), + ExecuteMsg::CommitDealingsChunk { chunk, resharing } => { + try_commit_dealings_chunk(deps, env, info, chunk, resharing) } ExecuteMsg::CommitVerificationKeyShare { share, resharing } => { try_commit_verification_key_share(deps, env, info, share, resharing) @@ -132,6 +139,16 @@ pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result { to_binary(&query_past_dealers_paged(deps, start_after, limit)?)? } + QueryMsg::GetDealingsMetadata { + epoch_id, + dealer, + dealing_index, + } => to_binary(&query_dealing_metadata( + deps, + epoch_id, + dealer, + dealing_index, + )?)?, QueryMsg::GetDealingStatus { epoch_id, dealer, @@ -142,22 +159,29 @@ pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result to_binary(&query_dealing(deps, epoch_id, dealer, dealing_index)?)?, - QueryMsg::GetDealings { - epoch_id, - dealer, - limit, - start_after, - } => to_binary(&query_dealings_paged( + chunk_index, + } => to_binary(&query_dealing_chunk_status( deps, epoch_id, dealer, - start_after, - limit, + dealing_index, + chunk_index, + )?)?, + QueryMsg::GetDealingChunk { + epoch_id, + dealer, + dealing_index, + chunk_index, + } => to_binary(&query_dealing_chunk( + deps, + epoch_id, + dealer, + dealing_index, + chunk_index, )?)?, QueryMsg::GetVerificationKey { owner, epoch_id } => { to_binary(&query_vk_share(deps, owner, epoch_id)?)? @@ -207,8 +231,9 @@ mod tests { use cosmwasm_std::{coins, Addr}; use cw4::Member; use cw_multi_test::{App, AppBuilder, AppResponse, ContractWrapper, Executor}; + use nym_coconut_dkg_common::dealing::DEFAULT_DEALINGS; use nym_coconut_dkg_common::msg::ExecuteMsg::{InitiateDkg, RegisterDealer}; - use nym_coconut_dkg_common::types::{NodeIndex, DEFAULT_DEALINGS}; + use nym_coconut_dkg_common::types::NodeIndex; use nym_group_contract_common::msg::InstantiateMsg as GroupInstantiateMsg; fn instantiate_with_group(app: &mut App, members: &[Addr]) -> Addr { diff --git a/contracts/coconut-dkg/src/dealings/queries.rs b/contracts/coconut-dkg/src/dealings/queries.rs index 921474473e..a83fcfb284 100644 --- a/contracts/coconut-dkg/src/dealings/queries.rs +++ b/contracts/coconut-dkg/src/dealings/queries.rs @@ -1,17 +1,33 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::dealings::storage; -use crate::dealings::storage::StoredDealing; +use crate::dealings::storage::{StoredDealing, DEALINGS_METADATA}; use cosmwasm_std::{Deps, StdResult}; -use cw_storage_plus::Bound; -use nym_coconut_dkg_common::dealer::{ - DealingResponse, DealingStatusResponse, PagedDealingsResponse, +use nym_coconut_dkg_common::dealing::{ + DealingChunkResponse, DealingChunkStatusResponse, DealingMetadataResponse, + DealingStatusResponse, }; -use nym_coconut_dkg_common::types::{DealingIndex, EpochId}; +use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId}; -// this does almost the same as query_dealing but doesn't return the actual dealing to make it easier on the validator -// so it wouldn't need to deal with the deserialization +/// Get the metadata associated with the particular dealing +pub fn query_dealing_metadata( + deps: Deps<'_>, + epoch_id: EpochId, + dealer: String, + dealing_index: DealingIndex, +) -> StdResult { + let dealer = deps.api.addr_validate(&dealer)?; + let metadata = DEALINGS_METADATA.may_load(deps.storage, (epoch_id, &dealer, dealing_index))?; + + Ok(DealingMetadataResponse { + epoch_id, + dealer, + dealing_index, + metadata, + }) +} + +/// Get the status of particular dealing, i.e. whether it has been fully submitted. pub fn query_dealing_status( deps: Deps<'_>, epoch_id: EpochId, @@ -19,61 +35,70 @@ pub fn query_dealing_status( dealing_index: DealingIndex, ) -> StdResult { let dealer = deps.api.addr_validate(&dealer)?; - let dealing_submitted = StoredDealing::exists(deps.storage, epoch_id, &dealer, dealing_index); + let metadata = DEALINGS_METADATA.may_load(deps.storage, (epoch_id, &dealer, dealing_index))?; + + let full_dealing_submitted = if let Some(metadata) = metadata { + metadata.is_complete() + } else { + false + }; Ok(DealingStatusResponse { epoch_id, dealer, dealing_index, - dealing_submitted, + full_dealing_submitted, }) } -pub fn query_dealing( +/// Get the status of particular chunk, i.e. whether (and when) it has been fully submitted. +pub fn query_dealing_chunk_status( deps: Deps<'_>, epoch_id: EpochId, dealer: String, dealing_index: DealingIndex, -) -> StdResult { - todo!() - // let dealer = deps.api.addr_validate(&dealer)?; - // let dealing = StoredDealing::read(deps.storage, epoch_id, &dealer, dealing_index); - // - // Ok(DealingResponse { - // epoch_id, - // dealer, - // dealing_index, - // dealing, - // }) + chunk_index: ChunkIndex, +) -> StdResult { + let dealer = deps.api.addr_validate(&dealer)?; + let metadata = DEALINGS_METADATA.may_load(deps.storage, (epoch_id, &dealer, dealing_index))?; + + let submission_height = if let Some(metadata) = metadata { + if let Some(chunk) = metadata.submitted_chunks.get(&chunk_index) { + chunk.submission_height + } else { + None + } + } else { + None + }; + + Ok(DealingChunkStatusResponse { + epoch_id, + dealer, + dealing_index, + chunk_index, + submission_height, + }) } -pub fn query_dealings_paged( +/// Get the particular chunk of the dealing. +pub fn query_dealing_chunk( deps: Deps<'_>, epoch_id: EpochId, dealer: String, - start_after: Option, - limit: Option, -) -> StdResult { - todo!() - // let limit = limit - // .unwrap_or(storage::DEALINGS_PAGE_DEFAULT_LIMIT) - // .min(storage::DEALINGS_PAGE_MAX_LIMIT); - // - // let dealer = deps.api.addr_validate(&dealer)?; - // let start = start_after.map(Bound::exclusive); - // - // let dealings = StoredDealing::prefix_range(deps.storage, (epoch_id, &dealer), start) - // .take(limit as usize) - // .collect::>>()?; - // - // let start_next_after = dealings.last().map(|dealing| dealing.dealing_index); - // - // Ok(PagedDealingsResponse::new( - // epoch_id, - // dealer, - // dealings, - // start_next_after, - // )) + dealing_index: DealingIndex, + chunk_index: ChunkIndex, +) -> StdResult { + let dealer = deps.api.addr_validate(&dealer)?; + let chunk = StoredDealing::read(deps.storage, epoch_id, &dealer, dealing_index, chunk_index); + + Ok(DealingChunkResponse { + epoch_id, + dealer, + dealing_index, + chunk_index, + chunk, + }) } // #[cfg(test)] diff --git a/contracts/coconut-dkg/src/dealings/storage.rs b/contracts/coconut-dkg/src/dealings/storage.rs index 8946a449f3..ee3eda01ac 100644 --- a/contracts/coconut-dkg/src/dealings/storage.rs +++ b/contracts/coconut-dkg/src/dealings/storage.rs @@ -1,21 +1,18 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::error::ContractError; use cosmwasm_std::{Addr, Order, Record, StdResult, Storage}; use cw_storage_plus::{Bound, Key, KeyDeserialize, Map, Path, Prefix, Prefixer, PrimaryKey}; +use nym_coconut_dkg_common::dealing::{DealingMetadata, PartialContractDealing}; use nym_coconut_dkg_common::types::{ - ChunkIndex, ContractSafeBytes, DealingIndex, DealingMetadata, EpochId, PartialContractDealing, - PartialContractDealingData, + ChunkIndex, ContractSafeBytes, DealingIndex, EpochId, PartialContractDealingData, }; -pub(crate) const DEALINGS_PAGE_MAX_LIMIT: u32 = 2; -pub(crate) const DEALINGS_PAGE_DEFAULT_LIMIT: u32 = 1; - type Dealer<'a> = &'a Addr; /// Metadata for a dealing for given `EpochId`, submitted by particular `Dealer` for given `DealingIndex`. -const DEALINGS_METADATA: Map<(EpochId, Dealer, DealingIndex), DealingMetadata> = +pub(crate) const DEALINGS_METADATA: Map<(EpochId, Dealer, DealingIndex), DealingMetadata> = Map::new("dealings_metadata"); pub(crate) fn metadata_exists( @@ -61,7 +58,7 @@ pub(crate) fn store_metadata( Ok(DEALINGS_METADATA.save(storage, (epoch_id, dealer, dealing_index), metadata)?) } -// dealings are stored in a multilevel map with the following hierarchy: +// dealings data is stored in a multilevel map with the following hierarchy: // - epoch-id: // - issuer-address: // - dealing id: @@ -69,7 +66,6 @@ pub(crate) fn store_metadata( // - dealing content // NOTE: we're storing raw bytes bypassing serialization, so we can't use the `Map` type, // thus make sure you always use the below methods for using the storage! - pub(crate) struct StoredDealing; impl StoredDealing { @@ -77,13 +73,12 @@ impl StoredDealing { fn deserialize_dealing_record( kv: Record, - ) -> StdResult<(DealingIndex, PartialContractDealingData)> { - todo!() - // let (k, v) = kv; - // let index = ::from_vec(k)?; - // let data = ContractSafeBytes(v); - // - // Ok((index, data)) + ) -> StdResult<(ChunkIndex, PartialContractDealingData)> { + let (k, v) = kv; + let index = ::from_vec(k)?; + let data = ContractSafeBytes(v); + + Ok((index, data)) } fn storage_key( @@ -102,25 +97,37 @@ impl StoredDealing { ) } - // fn prefix(prefix: (EpochId, Dealer)) -> Prefix { - // todo!() - // // Prefix::with_deserialization_functions( - // // Self::NAMESPACE, - // // &prefix.prefix(), - // // &[], - // // // explicitly panic to make sure we're never attempting to call an unexpected deserializer on our data - // // |_, _, kv| Self::deserialize_dealing_record(kv), - // // |_, _, _| panic!("attempted to call custom de_fn_v"), - // // ) - // } + fn prefix( + prefix: (EpochId, Dealer, DealingIndex), + ) -> Prefix { + Prefix::with_deserialization_functions( + Self::NAMESPACE, + &prefix.prefix(), + &[], + // explicitly panic to make sure we're never attempting to call an unexpected deserializer on our data + |_, _, kv| Self::deserialize_dealing_record(kv), + |_, _, _| panic!("attempted to call custom de_fn_v"), + ) + } pub(crate) fn exists( storage: &dyn Storage, epoch_id: EpochId, dealer: &Addr, dealing_index: DealingIndex, - ) -> bool { - todo!() + chunk_index: ChunkIndex, + ) -> StdResult { + // whenever the dealing is saved, the metadata is appropriately updated + // reading metadata is way cheaper than the dealing chunk itself + let Some(metadata) = + DEALINGS_METADATA.may_load(storage, (epoch_id, dealer, dealing_index))? + else { + return Ok(false); + }; + let Some(chunk_info) = metadata.submitted_chunks.get(&chunk_index) else { + return Ok(false); + }; + Ok(chunk_info.submission_height.is_some()) // StoredDealing::storage_key(epoch_id, dealer, dealing_index).has(storage) } @@ -128,12 +135,16 @@ impl StoredDealing { storage: &mut dyn Storage, epoch_id: EpochId, dealer: Dealer, - dealing: PartialContractDealing, + dealng_chunk: PartialContractDealing, ) { // NOTE: we're storing bytes directly here! - let storage_key = - Self::storage_key(epoch_id, dealer, dealing.dealing_index, dealing.chunk_index); - storage.set(&storage_key, dealing.data.as_slice()); + let storage_key = Self::storage_key( + epoch_id, + dealer, + dealng_chunk.dealing_index, + dealng_chunk.chunk_index, + ); + storage.set(&storage_key, dealng_chunk.data.as_slice()); } pub(crate) fn read( @@ -147,209 +158,264 @@ impl StoredDealing { storage.get(&storage_key).map(ContractSafeBytes) } - // pub(crate) fn prefix_range<'a>( - // storage: &'a dyn Storage, - // prefix: (EpochId, Dealer), - // start: Option>, - // ) -> impl Iterator> + 'a { - // vec![].into_iter() - // // todo!() - // // Self::prefix(prefix) - // // .range(storage, start, None, Order::Ascending) - // // .map(|maybe_record| maybe_record.map(Into::into)) - // } + pub(crate) fn prefix_range<'a>( + storage: &'a dyn Storage, + prefix: (EpochId, Dealer, DealingIndex), + start: Option>, + ) -> impl Iterator> + 'a { + let dealing_index = prefix.2; + Self::prefix(prefix) + .range(storage, start, None, Order::Ascending) + .map(move |maybe_record| { + maybe_record.map(|(chunk_index, data)| PartialContractDealing { + dealing_index, + chunk_index, + data, + }) + }) + } // iterate over all values, only to be used in tests due to the amount of data being returned #[cfg(test)] + #[allow(clippy::type_complexity)] pub(crate) fn unchecked_all_entries( storage: &dyn Storage, - ) -> Vec<((EpochId, Addr, DealingIndex), PartialContractDealingData)> { - todo!() - // type StorageKey<'a> = (EpochId, Dealer<'a>, DealingIndex); - // - // let empty_prefix: Prefix = - // Prefix::with_deserialization_functions( - // Self::NAMESPACE, - // &[], - // &[], - // |_, _, kv| StorageKey::from_vec(kv.0).map(|kt| (kt, ContractSafeBytes(kv.1))), - // |_, _, _| unimplemented!(), - // ); - // - // empty_prefix - // .range(storage, None, None, Order::Ascending) - // .collect::>() - // .unwrap() + ) -> Vec<( + (EpochId, Addr, (DealingIndex, ChunkIndex)), + PartialContractDealingData, + )> { + type StorageKey<'a> = (EpochId, Dealer<'a>, (DealingIndex, ChunkIndex)); + + let empty_prefix: Prefix = + Prefix::with_deserialization_functions( + Self::NAMESPACE, + &[], + &[], + |_, _, kv| StorageKey::from_vec(kv.0).map(|kt| (kt, ContractSafeBytes(kv.1))), + |_, _, _| unimplemented!(), + ); + + empty_prefix + .range(storage, None, None, Order::Ascending) + .collect::>() + .unwrap() } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::support::tests::helpers::init_contract; -// use std::collections::HashMap; -// -// fn dealing_data( -// epoch_id: EpochId, -// dealer: Dealer, -// dealing_index: DealingIndex, -// ) -> PartialContractDealingData { -// ContractSafeBytes( -// format!("{epoch_id},{dealer},{dealing_index}") -// .as_bytes() -// .to_vec(), -// ) -// } -// -// #[test] -// fn saving_dealing() { -// let mut deps = init_contract(); -// -// // make sure to check all combinations of epoch id, dealer address and dealing index to ensure nothing overlaps -// let epochs = [54, 423, 754]; -// let dealers = [ -// Addr::unchecked("dealer1"), -// Addr::unchecked("dealer2"), -// Addr::unchecked("dealer3"), -// Addr::unchecked("dealer4"), -// Addr::unchecked("dealer5"), -// ]; -// let dealing_indices = [0, 1, 2, 3, 4, 5, 6, 7]; -// -// for epoch_id in &epochs { -// for dealer in &dealers { -// for dealing_index in &dealing_indices { -// assert!(!StoredDealing::exists( -// &deps.storage, -// *epoch_id, -// dealer, -// *dealing_index -// )); -// -// StoredDealing::save( -// deps.as_mut().storage, -// *epoch_id, -// dealer, -// PartialContractDealing { -// dealing_index: *dealing_index, -// data: dealing_data(*epoch_id, dealer, *dealing_index), -// }, -// ) -// } -// } -// } -// -// let all: HashMap<_, _> = StoredDealing::unchecked_all_entries(&deps.storage) -// .into_iter() -// .collect(); -// assert_eq!( -// all.len(), -// epochs.len() * dealers.len() * dealing_indices.len() -// ); -// -// for epoch_id in &epochs { -// for dealer in &dealers { -// for dealing_index in &dealing_indices { -// assert!(StoredDealing::exists( -// &deps.storage, -// *epoch_id, -// dealer, -// *dealing_index -// )); -// -// let content = -// StoredDealing::read(&deps.storage, *epoch_id, dealer, *dealing_index) -// .unwrap(); -// let expected = dealing_data(*epoch_id, dealer, *dealing_index); -// assert_eq!(expected, content); -// assert_eq!( -// &expected, -// all.get(&(*epoch_id, dealer.clone(), *dealing_index)) -// .unwrap() -// ); -// } -// } -// } -// } -// -// #[test] -// fn iterating_over_dealings() { -// let mut deps = init_contract(); -// -// let epochs = [54, 423, 754]; -// let dealers = [ -// Addr::unchecked("dealer1"), -// Addr::unchecked("dealer2"), -// Addr::unchecked("dealer3"), -// Addr::unchecked("dealer4"), -// Addr::unchecked("dealer5"), -// ]; -// let dealing_indices = [0, 1, 2, 3, 4, 5, 6, 7]; -// -// for epoch_id in &epochs { -// for dealer in &dealers { -// for dealing_index in &dealing_indices { -// StoredDealing::save( -// deps.as_mut().storage, -// *epoch_id, -// dealer, -// PartialContractDealing { -// dealing_index: *dealing_index, -// data: dealing_data(*epoch_id, dealer, *dealing_index), -// }, -// ) -// } -// } -// } -// -// // remember, we're not testing the iterator implementation -// -// // nothing under epoch 0 -// let dealings = -// StoredDealing::prefix_range(&deps.storage, (0, &dealers[0]), None).collect::>(); -// assert!(dealings.is_empty()); -// -// // nothing for dealer "foo" -// let foo = Addr::unchecked("foo"); -// let dealings = -// StoredDealing::prefix_range(&deps.storage, (epochs[0], &foo), None).collect::>(); -// assert!(dealings.is_empty()); -// -// let all = StoredDealing::prefix_range(&deps.storage, (epochs[0], &dealers[0]), None) -// .collect::>(); -// assert_eq!(all.len(), dealing_indices.len()); -// -// for (i, dealing) in all.iter().enumerate() { -// let expected = dealing_data(epochs[0], &dealers[0], dealing_indices[i]); -// assert_eq!(expected, dealing.as_ref().unwrap().data); -// assert_eq!(dealing_indices[i], dealing.as_ref().unwrap().dealing_index); -// } -// -// // for sanity sake, check another dealer with different epoch -// let all_other = StoredDealing::prefix_range(&deps.storage, (epochs[2], &dealers[3]), None) -// .collect::>(); -// assert_eq!(all_other.len(), dealing_indices.len()); -// -// for (i, dealing) in all_other.iter().enumerate() { -// let expected = dealing_data(epochs[2], &dealers[3], dealing_indices[i]); -// assert_eq!(expected, dealing.as_ref().unwrap().data); -// assert_eq!(dealing_indices[i], dealing.as_ref().unwrap().dealing_index); -// } -// -// let without_first = StoredDealing::prefix_range( -// &deps.storage, -// (epochs[0], &dealers[0]), -// Some(Bound::exclusive(dealing_indices[0])), -// ) -// .collect::>(); -// assert_eq!(&all[1..], without_first); -// -// let mid = StoredDealing::prefix_range( -// &deps.storage, -// (epochs[0], &dealers[0]), -// Some(Bound::inclusive(dealing_indices[3])), -// ) -// .collect::>(); -// assert_eq!(&all[3..], mid); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::helpers::init_contract; + use std::collections::HashMap; + + fn dealing_data( + epoch_id: EpochId, + dealer: Dealer, + dealing_index: DealingIndex, + chunk_index: ChunkIndex, + ) -> PartialContractDealingData { + ContractSafeBytes( + format!("{epoch_id},{dealer},{dealing_index},{chunk_index}") + .as_bytes() + .to_vec(), + ) + } + + #[test] + fn saving_dealing_chunks() { + let mut deps = init_contract(); + + fn exists_in_storage( + storage: &dyn Storage, + epoch_id: EpochId, + dealer: Dealer, + dealing_index: DealingIndex, + chunk_index: ChunkIndex, + ) -> bool { + StoredDealing::storage_key(epoch_id, dealer, dealing_index, chunk_index).has(storage) + } + + // make sure to check all combinations of epoch id, dealer address and dealing index to ensure nothing overlaps + let epochs = [54, 423, 754]; + let dealers = [ + Addr::unchecked("dealer1"), + Addr::unchecked("dealer2"), + Addr::unchecked("dealer3"), + Addr::unchecked("dealer4"), + Addr::unchecked("dealer5"), + ]; + let dealing_indices = [0, 1, 2, 3, 4, 5, 6, 7]; + let chunk_indices = [0, 1, 2, 3, 4]; + + for epoch_id in &epochs { + for dealer in &dealers { + for dealing_index in &dealing_indices { + for chunk_index in &chunk_indices { + assert!(!exists_in_storage( + &deps.storage, + *epoch_id, + dealer, + *dealing_index, + *chunk_index + )); + + StoredDealing::save( + deps.as_mut().storage, + *epoch_id, + dealer, + PartialContractDealing { + dealing_index: *dealing_index, + chunk_index: *chunk_index, + data: dealing_data(*epoch_id, dealer, *dealing_index, *chunk_index), + }, + ) + } + } + } + } + + let all: HashMap<_, _> = StoredDealing::unchecked_all_entries(&deps.storage) + .into_iter() + .collect(); + assert_eq!( + all.len(), + epochs.len() * dealers.len() * dealing_indices.len() * chunk_indices.len() + ); + + for epoch_id in &epochs { + for dealer in &dealers { + for dealing_index in &dealing_indices { + for chunk_index in &chunk_indices { + assert!(exists_in_storage( + &deps.storage, + *epoch_id, + dealer, + *dealing_index, + *chunk_index + )); + + let content = StoredDealing::read( + &deps.storage, + *epoch_id, + dealer, + *dealing_index, + *chunk_index, + ) + .unwrap(); + let expected = + dealing_data(*epoch_id, dealer, *dealing_index, *chunk_index); + assert_eq!(expected, content); + assert_eq!( + &expected, + all.get(&(*epoch_id, dealer.clone(), (*dealing_index, *chunk_index))) + .unwrap() + ); + } + } + } + } + } + + #[test] + fn iterating_over_dealing_chunks() { + let mut deps = init_contract(); + + let epochs = [54, 423, 754]; + let dealers = [ + Addr::unchecked("dealer1"), + Addr::unchecked("dealer2"), + Addr::unchecked("dealer3"), + Addr::unchecked("dealer4"), + Addr::unchecked("dealer5"), + ]; + let dealing_indices = [0, 1, 2, 3, 4, 5, 6, 7]; + let chunk_indices = [0, 1, 2, 3, 4]; + + for epoch_id in &epochs { + for dealer in &dealers { + for dealing_index in &dealing_indices { + for chunk_index in &chunk_indices { + StoredDealing::save( + deps.as_mut().storage, + *epoch_id, + dealer, + PartialContractDealing { + dealing_index: *dealing_index, + chunk_index: *chunk_index, + data: dealing_data(*epoch_id, dealer, *dealing_index, *chunk_index), + }, + ) + } + } + } + } + + // remember, we're not testing the iterator implementation + + // nothing under epoch 0 + let dealings = + StoredDealing::prefix_range(&deps.storage, (0, &dealers[0], dealing_indices[0]), None) + .collect::>(); + assert!(dealings.is_empty()); + + // nothing for dealer "foo" + let foo = Addr::unchecked("foo"); + let dealings = + StoredDealing::prefix_range(&deps.storage, (epochs[0], &foo, dealing_indices[0]), None) + .collect::>(); + assert!(dealings.is_empty()); + + // nothing for dealing index 99 + let dealings = + StoredDealing::prefix_range(&deps.storage, (epochs[0], &dealers[0], 99), None) + .collect::>(); + assert!(dealings.is_empty()); + + let all = StoredDealing::prefix_range( + &deps.storage, + (epochs[0], &dealers[0], dealing_indices[0]), + None, + ) + .collect::>(); + assert_eq!(all.len(), chunk_indices.len()); + + for (i, dealing) in all.iter().enumerate() { + let expected = + dealing_data(epochs[0], &dealers[0], dealing_indices[0], chunk_indices[i]); + assert_eq!(expected, dealing.as_ref().unwrap().data); + assert_eq!(chunk_indices[i], dealing.as_ref().unwrap().chunk_index); + } + + // for sanity sake, check another dealer with different epoch and different dealing index + let all_other = StoredDealing::prefix_range( + &deps.storage, + (epochs[2], &dealers[3], dealing_indices[4]), + None, + ) + .collect::>(); + assert_eq!(all_other.len(), chunk_indices.len()); + + for (i, dealing) in all_other.iter().enumerate() { + let expected = + dealing_data(epochs[2], &dealers[3], dealing_indices[4], chunk_indices[i]); + assert_eq!(expected, dealing.as_ref().unwrap().data); + assert_eq!(chunk_indices[i], dealing.as_ref().unwrap().chunk_index); + } + + let without_first = StoredDealing::prefix_range( + &deps.storage, + (epochs[0], &dealers[0], dealing_indices[0]), + Some(Bound::exclusive(chunk_indices[0])), + ) + .collect::>(); + assert_eq!(&all[1..], without_first); + + let mid = StoredDealing::prefix_range( + &deps.storage, + (epochs[0], &dealers[0], dealing_indices[0]), + Some(Bound::inclusive(chunk_indices[3])), + ) + .collect::>(); + assert_eq!(&all[3..], mid); + } +} diff --git a/contracts/coconut-dkg/src/dealings/transactions.rs b/contracts/coconut-dkg/src/dealings/transactions.rs index cd2d32a758..cc80cda31e 100644 --- a/contracts/coconut-dkg/src/dealings/transactions.rs +++ b/contracts/coconut-dkg/src/dealings/transactions.rs @@ -1,4 +1,4 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::dealers::storage as dealers_storage; @@ -10,9 +10,10 @@ use crate::epoch_state::utils::check_epoch_state; use crate::error::ContractError; use crate::state::storage::STATE; use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Storage}; -use nym_coconut_dkg_common::types::{ - DealingMetadata, EpochState, PartialContractDealing, MAX_DEALING_CHUNKS, +use nym_coconut_dkg_common::dealing::{ + DealingChunkInfo, DealingMetadata, PartialContractDealing, MAX_DEALING_CHUNKS, }; +use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochState}; // make sure the epoch is in the dealing exchange and the message sender is a valid dealer for this epoch fn ensure_permission( @@ -43,7 +44,8 @@ fn ensure_permission( pub fn try_submit_dealings_metadata( deps: DepsMut, info: MessageInfo, - metadata: DealingMetadata, + dealing_index: DealingIndex, + chunks: Vec, resharing: bool, ) -> Result { ensure_permission(deps.storage, &info.sender, resharing)?; @@ -52,85 +54,80 @@ pub fn try_submit_dealings_metadata( let epoch = CURRENT_EPOCH.load(deps.storage)?; // don't allow overwriting existing metadata - if metadata_exists( - deps.storage, - epoch.epoch_id, - &info.sender, - metadata.dealing_index, - ) { + if metadata_exists(deps.storage, epoch.epoch_id, &info.sender, dealing_index) { return Err(ContractError::MetadataAlreadyExists { epoch_id: epoch.epoch_id, dealer: info.sender, - dealing_index: metadata.dealing_index, - }); - } - - // make sure the metadata is not empty - if metadata.submitted_chunks.is_empty() { - return Err(ContractError::EmptyMetadata { - epoch_id: epoch.epoch_id, - dealer: info.sender, - dealing_index: metadata.dealing_index, - }); - } - - // make sure the dealing has non-zero size - if metadata.total_size() == 0 { - return Err(ContractError::EmptyMetadata { - epoch_id: epoch.epoch_id, - dealer: info.sender, - dealing_index: metadata.dealing_index, + dealing_index, }); } // make sure the dealing index is in the allowed range // note: dealing indexing starts from 0 - if metadata.dealing_index >= state.key_size { + if dealing_index >= state.key_size { return Err(ContractError::DealingOutOfRange { epoch_id: epoch.epoch_id, dealer: info.sender, - index: metadata.dealing_index, + index: dealing_index, key_size: state.key_size, }); } - let chunks = metadata.submitted_chunks.len(); + // make sure the metadata is not empty + if chunks.is_empty() { + return Err(ContractError::EmptyMetadata { + epoch_id: epoch.epoch_id, + dealer: info.sender, + dealing_index, + }); + } + + // make sure the chunks are non empty + if chunks.iter().any(|c| c.size == 0) { + return Err(ContractError::EmptyMetadata { + epoch_id: epoch.epoch_id, + dealer: info.sender, + dealing_index, + }); + } // make sure the number of dealing chunks is in the allowed range // to prevent somebody splitting their dealings into 10B chunks - if chunks > MAX_DEALING_CHUNKS { + if chunks.len() > MAX_DEALING_CHUNKS { return Err(ContractError::TooFragmentedMetadata { epoch_id: epoch.epoch_id, dealer: info.sender, - dealing_index: metadata.dealing_index, - chunks, + dealing_index, + chunks: chunks.len(), }); } // make sure all chunks, but the last one, have the same size - // SAFETY: we checked for whether `metadata.submitted_chunks` is empty and returned an error in that case + // SAFETY: we checked for whether `chunks` is empty and returned an error in that case #[allow(clippy::unwrap_used)] - let first_chunk_size = metadata.submitted_chunks.first_key_value().unwrap().1.size; + let first_chunk_size = chunks.first().unwrap().size; - for (&chunk_index, chunk_info) in metadata.submitted_chunks.iter().take(chunks - 1) { + for (chunk_index, chunk_info) in chunks.iter().enumerate().take(chunks.len() - 1) { if chunk_info.size != first_chunk_size { return Err(ContractError::UnevenChunkSplit { epoch_id: epoch.epoch_id, dealer: info.sender, - dealing_index: metadata.dealing_index, - chunk_index, + dealing_index, + chunk_index: chunk_index as ChunkIndex, first_chunk_size, size: chunk_info.size, }); } } - // finally, store the metadata + // finally, construct and store the metadata + let metadata = DealingMetadata::new(dealing_index, chunks); + store_metadata( deps.storage, epoch.epoch_id, &info.sender, - metadata.dealing_index, + dealing_index, &metadata, )?; @@ -141,7 +138,7 @@ pub fn try_commit_dealings_chunk( deps: DepsMut<'_>, env: Env, info: MessageInfo, - dealing: PartialContractDealing, + chunk: PartialContractDealing, resharing: bool, ) -> Result { ensure_permission(deps.storage, &info.sender, resharing)?; @@ -153,16 +150,16 @@ pub fn try_commit_dealings_chunk( deps.storage, epoch.epoch_id, &info.sender, - dealing.dealing_index, + chunk.dealing_index, )?; // check if the received chunk is within the declared range - let Some(submission_status) = metadata.submitted_chunks.get_mut(&dealing.chunk_index) else { + let Some(submission_status) = metadata.submitted_chunks.get_mut(&chunk.chunk_index) else { return Err(ContractError::DealingChunkNotInMetadata { epoch_id: epoch.epoch_id, dealer: info.sender, - dealing_index: dealing.dealing_index, - chunk_index: dealing.chunk_index, + dealing_index: chunk.dealing_index, + chunk_index: chunk.chunk_index, }); }; @@ -171,8 +168,8 @@ pub fn try_commit_dealings_chunk( return Err(ContractError::DealingChunkAlreadyCommitted { epoch_id: epoch.epoch_id, dealer: info.sender, - dealing_index: dealing.dealing_index, - chunk_index: dealing.chunk_index, + dealing_index: chunk.dealing_index, + chunk_index: chunk.chunk_index, block_height: submission_height, }); } @@ -183,12 +180,12 @@ pub fn try_commit_dealings_chunk( deps.storage, epoch.epoch_id, &info.sender, - dealing.dealing_index, + chunk.dealing_index, &metadata, )?; // store the dealing - StoredDealing::save(deps.storage, epoch.epoch_id, &info.sender, dealing); + StoredDealing::save(deps.storage, epoch.epoch_id, &info.sender, chunk); Ok(Response::new()) } @@ -205,7 +202,7 @@ pub(crate) mod tests { use cosmwasm_std::Addr; use nym_coconut_dkg_common::dealer::DealerDetails; use nym_coconut_dkg_common::types::{ - ContractSafeBytes, InitialReplacementData, TimeConfiguration, DEFAULT_DEALINGS, + ContractSafeBytes, InitialReplacementData, TimeConfiguration, }; #[test] diff --git a/contracts/coconut-dkg/src/error.rs b/contracts/coconut-dkg/src/error.rs index 623e28cafe..6f58d98d6f 100644 --- a/contracts/coconut-dkg/src/error.rs +++ b/contracts/coconut-dkg/src/error.rs @@ -3,7 +3,8 @@ use cosmwasm_std::{Addr, StdError}; use cw_controllers::AdminError; -use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId, MAX_DEALING_CHUNKS}; +use nym_coconut_dkg_common::dealing::MAX_DEALING_CHUNKS; +use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId}; use thiserror::Error; /// Custom errors for contract failure conditions. diff --git a/contracts/coconut-dkg/src/support/tests/fixtures.rs b/contracts/coconut-dkg/src/support/tests/fixtures.rs index 153190d289..67775ce27e 100644 --- a/contracts/coconut-dkg/src/support/tests/fixtures.rs +++ b/contracts/coconut-dkg/src/support/tests/fixtures.rs @@ -1,9 +1,10 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use cosmwasm_std::Addr; use nym_coconut_dkg_common::dealer::DealerDetails; -use nym_coconut_dkg_common::types::{ContractSafeBytes, PartialContractDealing}; +use nym_coconut_dkg_common::dealing::PartialContractDealing; +use nym_coconut_dkg_common::types::ContractSafeBytes; use nym_coconut_dkg_common::verification_key::ContractVKShare; pub const TEST_MIX_DENOM: &str = "unym"; @@ -24,11 +25,11 @@ pub fn dealing_bytes_fixture() -> ContractSafeBytes { } pub fn partial_dealing_fixture() -> PartialContractDealing { - todo!() - // PartialContractDealing { - // dealing_index: 0, - // data: ContractSafeBytes(vec![1, 2, 3]), - // } + PartialContractDealing { + chunk_index: 0, + dealing_index: 0, + data: ContractSafeBytes(vec![1, 2, 3]), + } } pub fn dealer_details_fixture(assigned_index: u64) -> DealerDetails { diff --git a/contracts/coconut-dkg/src/support/tests/helpers.rs b/contracts/coconut-dkg/src/support/tests/helpers.rs index 31c8d272fb..b6b241dd4a 100644 --- a/contracts/coconut-dkg/src/support/tests/helpers.rs +++ b/contracts/coconut-dkg/src/support/tests/helpers.rs @@ -9,8 +9,9 @@ use cosmwasm_std::{ QuerierResult, SystemResult, WasmQuery, }; use cw4::{Cw4QueryMsg, Member, MemberListResponse, MemberResponse}; +use nym_coconut_dkg_common::dealing::DEFAULT_DEALINGS; use nym_coconut_dkg_common::msg::InstantiateMsg; -use nym_coconut_dkg_common::types::{DealerDetails, DEFAULT_DEALINGS}; +use nym_coconut_dkg_common::types::DealerDetails; use std::sync::Mutex; use super::fixtures::TEST_MIX_DENOM; diff --git a/contracts/coconut-dkg/src/verification_key_shares/transactions.rs b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs index e455ac3430..8ff202ec04 100644 --- a/contracts/coconut-dkg/src/verification_key_shares/transactions.rs +++ b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs @@ -8,7 +8,7 @@ use crate::epoch_state::utils::check_epoch_state; use crate::error::ContractError; use crate::state::storage::{MULTISIG, STATE}; use crate::verification_key_shares::storage::vk_shares; -use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use nym_coconut_dkg_common::types::EpochState; use nym_coconut_dkg_common::verification_key::{ to_cosmos_msg, ContractVKShare, VerificationKeyShare, @@ -66,9 +66,11 @@ pub fn try_commit_verification_key_share( pub fn try_verify_verification_key_share( deps: DepsMut<'_>, info: MessageInfo, - owner: Addr, + owner: String, resharing: bool, ) -> Result { + let owner = deps.api.addr_validate(&owner)?; + check_epoch_state( deps.storage, EpochState::VerificationKeyFinalization { resharing }, @@ -96,6 +98,7 @@ mod tests { use crate::support::tests::helpers; use crate::support::tests::helpers::{add_fixture_dealer, ADMIN_ADDRESS, MULTISIG_CONTRACT}; use cosmwasm_std::testing::{mock_env, mock_info}; + use cosmwasm_std::Addr; use cw_controllers::AdminError; use nym_coconut_dkg_common::dealer::DealerDetails; use nym_coconut_dkg_common::types::{EpochState, TimeConfiguration}; @@ -233,7 +236,7 @@ mod tests { try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); let info = mock_info("requester", &[]); - let owner = Addr::unchecked("owner"); + let owner = "owner".to_string(); let multisig_info = mock_info(MULTISIG_CONTRACT, &[]); let ret = @@ -291,7 +294,7 @@ mod tests { let mut env = mock_env(); try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); - let owner = Addr::unchecked("owner"); + let owner = "owner".to_string(); let info = mock_info(owner.as_ref(), &[]); let share = "share".to_string(); let multisig_info = mock_info(MULTISIG_CONTRACT, &[]); @@ -309,14 +312,18 @@ mod tests { advance_epoch_state(deps.as_mut(), env.clone()).unwrap(); let dealer_details = DealerDetails { - address: owner.clone(), + address: Addr::unchecked(&owner), bte_public_key_with_proof: String::new(), ed25519_identity: String::new(), announce_address: String::new(), assigned_index: 1, }; dealers_storage::current_dealers() - .save(deps.as_mut().storage, &owner, &dealer_details) + .save( + deps.as_mut().storage, + &Addr::unchecked(&owner), + &dealer_details, + ) .unwrap(); try_commit_verification_key_share(deps.as_mut(), env.clone(), info, share, false).unwrap();