diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs index 83f848e0d7..50152ba409 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::types::{ - ContractDealing, DealingIndex, EncodedBTEPublicKeyWithProof, EpochId, NodeIndex, - PartialContractDealing, + DealingIndex, EncodedBTEPublicKeyWithProof, EpochId, NodeIndex, PartialContractDealing, + PartialContractDealingData, }; use cosmwasm_schema::cw_serde; use cosmwasm_std::Addr; @@ -77,7 +77,7 @@ pub struct DealingResponse { pub dealing_index: DealingIndex, - pub dealing: Option, + pub dealing: Option, } #[cw_serde] diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs index e4d24f82f7..1f28eac719 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use cosmwasm_schema::cw_serde; +use std::collections::BTreeMap; use std::fmt::{Display, Formatter}; use std::str::FromStr; @@ -15,32 +16,108 @@ pub type EncodedBTEPublicKeyWithProofRef<'a> = &'a str; pub type NodeIndex = u64; pub type EpochId = u64; pub type DealingIndex = u32; -pub type ContractDealing = ContractSafeBytes; +// we really don't need to hold more data than that (even u8 would have been enough), +// but explicitly make it different type than `DealingIndex` so type system would detect any +// accidental misuses +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 PartialContractDealing { - pub index: DealingIndex, - pub data: ContractDealing, +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 PartialContractDealing { - pub fn new(index: DealingIndex, data: ContractDealing) -> Self { - PartialContractDealing { index, data } - } -} - -impl From<(DealingIndex, ContractDealing)> for PartialContractDealing { - fn from(value: (DealingIndex, ContractDealing)) -> Self { - PartialContractDealing { - index: value.0, - data: value.1, +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, @@ -277,3 +354,37 @@ impl EpochState { matches!(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/contracts-common/src/dealings.rs b/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs index c365d2691f..ef29c337d5 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs @@ -10,8 +10,8 @@ use std::ops::Deref; // currently set to 128 bytes 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 +// helps to transfer bytes between contract boundary to decrease amount of data sent accross +// after it's put to `Binary` #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, JsonSchema)] pub struct ContractSafeBytes(pub Vec); diff --git a/contracts/coconut-dkg/src/contract.rs b/contracts/coconut-dkg/src/contract.rs index 1bbfb72a70..84af041d0a 100644 --- a/contracts/coconut-dkg/src/contract.rs +++ b/contracts/coconut-dkg/src/contract.rs @@ -6,7 +6,7 @@ use crate::dealers::queries::{ }; 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; +use crate::dealings::transactions::try_commit_dealings_chunk; use crate::epoch_state::queries::{ query_current_epoch, query_current_epoch_threshold, query_initial_dealers, }; @@ -101,7 +101,7 @@ pub fn execute( resharing, ), ExecuteMsg::CommitDealing { dealing, resharing } => { - try_commit_dealings(deps, info, dealing, resharing) + try_commit_dealings_chunk(deps, info, dealing, resharing) } ExecuteMsg::CommitVerificationKeyShare { share, resharing } => { try_commit_verification_key_share(deps, env, info, share, resharing) diff --git a/contracts/coconut-dkg/src/dealings/queries.rs b/contracts/coconut-dkg/src/dealings/queries.rs index d78582aeee..921474473e 100644 --- a/contracts/coconut-dkg/src/dealings/queries.rs +++ b/contracts/coconut-dkg/src/dealings/queries.rs @@ -35,15 +35,16 @@ pub fn query_dealing( dealer: String, dealing_index: DealingIndex, ) -> StdResult { - 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, - }) + 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, + // }) } pub fn query_dealings_paged( @@ -53,254 +54,255 @@ pub fn query_dealings_paged( start_after: Option, limit: Option, ) -> StdResult { - 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.index); - - Ok(PagedDealingsResponse::new( - epoch_id, - dealer, - dealings, - start_next_after, - )) + 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, + // )) } -#[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, partial_dealing_fixture}; - use crate::support::tests::helpers::init_contract; - use cosmwasm_std::{Addr, DepsMut}; - use nym_coconut_dkg_common::types::PartialContractDealing; - - fn fill_dealings(deps: DepsMut<'_>, epoch: EpochId, dealers: usize, key_size: u32) { - for i in 0..dealers { - let dealer = Addr::unchecked(format!("dealer{i}")); - for dealing_index in 0..key_size { - StoredDealing::save( - deps.storage, - epoch, - &dealer, - PartialContractDealing { - index: dealing_index, - data: dealing_bytes_fixture(), - }, - ) - } - } - } - - #[test] - fn test_query_dealing() { - let mut deps = init_contract(); - - let bad_address = "FOOMP".to_string(); - assert!(query_dealing(deps.as_ref(), 0, bad_address, 0).is_err()); - - let empty = query_dealing(deps.as_ref(), 0, "foo".to_string(), 0).unwrap(); - assert_eq!(empty.epoch_id, 0); - assert_eq!(empty.dealing_index, 0); - assert_eq!(empty.dealer, Addr::unchecked("foo")); - assert!(empty.dealing.is_none()); - - // insert the dealing - let dealing = partial_dealing_fixture(); - StoredDealing::save( - deps.as_mut().storage, - 0, - &Addr::unchecked("foo"), - dealing.clone(), - ); - - let retrieved = query_dealing(deps.as_ref(), 0, "foo".to_string(), 0).unwrap(); - assert_eq!(retrieved.epoch_id, 0); - assert_eq!(retrieved.dealing_index, dealing.index); - assert_eq!(retrieved.dealer, Addr::unchecked("foo")); - assert_eq!(retrieved.dealing.unwrap(), dealing.data); - } - - #[test] - fn test_query_dealing_status() { - let mut deps = init_contract(); - - let bad_address = "FOOMP".to_string(); - assert!(query_dealing_status(deps.as_ref(), 0, bad_address, 0).is_err()); - - let empty = query_dealing_status(deps.as_ref(), 0, "foo".to_string(), 0).unwrap(); - assert_eq!(empty.epoch_id, 0); - assert_eq!(empty.dealing_index, 0); - assert_eq!(empty.dealer, Addr::unchecked("foo")); - assert!(!empty.dealing_submitted); - - // insert the dealing - let dealing = partial_dealing_fixture(); - StoredDealing::save( - deps.as_mut().storage, - 0, - &Addr::unchecked("foo"), - dealing.clone(), - ); - - let retrieved = query_dealing_status(deps.as_ref(), 0, "foo".to_string(), 0).unwrap(); - assert_eq!(retrieved.epoch_id, 0); - assert_eq!(retrieved.dealing_index, dealing.index); - assert_eq!(retrieved.dealer, Addr::unchecked("foo")); - assert!(retrieved.dealing_submitted) - } - - #[cfg(test)] - mod query_dealings { - use super::*; - use nym_coconut_dkg_common::types::DEFAULT_DEALINGS; - - #[test] - fn dealings_empty_on_init() { - let deps = init_contract(); - let all_dealings = StoredDealing::unchecked_all_entries(&deps.storage); - assert!(all_dealings.is_empty()) - } - - #[test] - fn dealings_paged_retrieval_obeys_limits() { - let mut deps = init_contract(); - let limit = 2; - fill_dealings(deps.as_mut(), 0, 10, DEFAULT_DEALINGS as u32); - - for dealer in 0..10 { - let dealer = format!("dealer{dealer}"); - let page1 = - query_dealings_paged(deps.as_ref(), 0, dealer, 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(); - fill_dealings(deps.as_mut(), 0, 10, DEFAULT_DEALINGS as u32); - - for dealer in 0..10 { - let dealer = format!("dealer{dealer}"); - // query without explicitly setting a limit - let page1 = query_dealings_paged(deps.as_ref(), 0, dealer, 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(); - fill_dealings(deps.as_mut(), 0, 10, DEFAULT_DEALINGS as u32); - - // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 1000 * DEALINGS_PAGE_MAX_LIMIT; - for dealer in 0..10 { - let dealer = format!("dealer{dealer}"); - let page1 = - query_dealings_paged(deps.as_ref(), 0, dealer, 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(); - - fill_dealings(deps.as_mut(), 0, 10, 1); - let per_page = 2; - - for dealer in 0..10 { - let dealer = format!("dealer{dealer}"); - let page1 = - query_dealings_paged(deps.as_ref(), 0, dealer, 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(), 1, 10, 2); - - for dealer in 0..10 { - let dealer = format!("dealer{dealer}"); - // page1 should have 2 results on it - let page1 = - query_dealings_paged(deps.as_ref(), 1, dealer, None, Option::from(per_page)) - .unwrap(); - assert_eq!(2, page1.dealings.len()); - } - - fill_dealings(deps.as_mut(), 3, 10, 3); - - for dealer in 0..10 { - let dealer = format!("dealer{dealer}"); - // page1 still has 2 results - let page1 = query_dealings_paged( - deps.as_ref(), - 3, - dealer.clone(), - 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(), - 3, - dealer, - Option::from(start_after), - Option::from(per_page), - ) - .unwrap(); - - assert_eq!(1, page2.dealings.len()); - } - - fill_dealings(deps.as_mut(), 4, 10, 4); - - for dealer in 0..10 { - let dealer = format!("dealer{dealer}"); - let page1 = query_dealings_paged( - deps.as_ref(), - 4, - dealer.clone(), - None, - Option::from(per_page), - ) - .unwrap(); - let start_after = page1.start_next_after.unwrap(); - let page2 = query_dealings_paged( - deps.as_ref(), - 4, - dealer, - Option::from(start_after), - Option::from(per_page), - ) - .unwrap(); - - // now we have 2 pages, with 2 results on the second page - assert_eq!(2, page2.dealings.len()); - } - } - } -} +// #[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, partial_dealing_fixture}; +// use crate::support::tests::helpers::init_contract; +// use cosmwasm_std::{Addr, DepsMut}; +// use nym_coconut_dkg_common::types::PartialContractDealing; +// +// fn fill_dealings(deps: DepsMut<'_>, epoch: EpochId, dealers: usize, key_size: u32) { +// for i in 0..dealers { +// let dealer = Addr::unchecked(format!("dealer{i}")); +// for dealing_index in 0..key_size { +// StoredDealing::save( +// deps.storage, +// epoch, +// &dealer, +// PartialContractDealing { +// dealing_index: dealing_index, +// data: dealing_bytes_fixture(), +// }, +// ) +// } +// } +// } +// +// #[test] +// fn test_query_dealing() { +// let mut deps = init_contract(); +// +// let bad_address = "FOOMP".to_string(); +// assert!(query_dealing(deps.as_ref(), 0, bad_address, 0).is_err()); +// +// let empty = query_dealing(deps.as_ref(), 0, "foo".to_string(), 0).unwrap(); +// assert_eq!(empty.epoch_id, 0); +// assert_eq!(empty.dealing_index, 0); +// assert_eq!(empty.dealer, Addr::unchecked("foo")); +// assert!(empty.dealing.is_none()); +// +// // insert the dealing +// let dealing = partial_dealing_fixture(); +// StoredDealing::save( +// deps.as_mut().storage, +// 0, +// &Addr::unchecked("foo"), +// dealing.clone(), +// ); +// +// let retrieved = query_dealing(deps.as_ref(), 0, "foo".to_string(), 0).unwrap(); +// assert_eq!(retrieved.epoch_id, 0); +// assert_eq!(retrieved.dealing_index, dealing.dealing_index); +// assert_eq!(retrieved.dealer, Addr::unchecked("foo")); +// assert_eq!(retrieved.dealing.unwrap(), dealing.data); +// } +// +// #[test] +// fn test_query_dealing_status() { +// let mut deps = init_contract(); +// +// let bad_address = "FOOMP".to_string(); +// assert!(query_dealing_status(deps.as_ref(), 0, bad_address, 0).is_err()); +// +// let empty = query_dealing_status(deps.as_ref(), 0, "foo".to_string(), 0).unwrap(); +// assert_eq!(empty.epoch_id, 0); +// assert_eq!(empty.dealing_index, 0); +// assert_eq!(empty.dealer, Addr::unchecked("foo")); +// assert!(!empty.dealing_submitted); +// +// // insert the dealing +// let dealing = partial_dealing_fixture(); +// StoredDealing::save( +// deps.as_mut().storage, +// 0, +// &Addr::unchecked("foo"), +// dealing.clone(), +// ); +// +// let retrieved = query_dealing_status(deps.as_ref(), 0, "foo".to_string(), 0).unwrap(); +// assert_eq!(retrieved.epoch_id, 0); +// assert_eq!(retrieved.dealing_index, dealing.dealing_index); +// assert_eq!(retrieved.dealer, Addr::unchecked("foo")); +// assert!(retrieved.dealing_submitted) +// } +// +// #[cfg(test)] +// mod query_dealings { +// use super::*; +// use nym_coconut_dkg_common::types::DEFAULT_DEALINGS; +// +// #[test] +// fn dealings_empty_on_init() { +// let deps = init_contract(); +// let all_dealings = StoredDealing::unchecked_all_entries(&deps.storage); +// assert!(all_dealings.is_empty()) +// } +// +// #[test] +// fn dealings_paged_retrieval_obeys_limits() { +// let mut deps = init_contract(); +// let limit = 2; +// fill_dealings(deps.as_mut(), 0, 10, DEFAULT_DEALINGS as u32); +// +// for dealer in 0..10 { +// let dealer = format!("dealer{dealer}"); +// let page1 = +// query_dealings_paged(deps.as_ref(), 0, dealer, 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(); +// fill_dealings(deps.as_mut(), 0, 10, DEFAULT_DEALINGS as u32); +// +// for dealer in 0..10 { +// let dealer = format!("dealer{dealer}"); +// // query without explicitly setting a limit +// let page1 = query_dealings_paged(deps.as_ref(), 0, dealer, 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(); +// fill_dealings(deps.as_mut(), 0, 10, DEFAULT_DEALINGS as u32); +// +// // query with a crazily high limit in an attempt to use too many resources +// let crazy_limit = 1000 * DEALINGS_PAGE_MAX_LIMIT; +// for dealer in 0..10 { +// let dealer = format!("dealer{dealer}"); +// let page1 = +// query_dealings_paged(deps.as_ref(), 0, dealer, 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(); +// +// fill_dealings(deps.as_mut(), 0, 10, 1); +// let per_page = 2; +// +// for dealer in 0..10 { +// let dealer = format!("dealer{dealer}"); +// let page1 = +// query_dealings_paged(deps.as_ref(), 0, dealer, 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(), 1, 10, 2); +// +// for dealer in 0..10 { +// let dealer = format!("dealer{dealer}"); +// // page1 should have 2 results on it +// let page1 = +// query_dealings_paged(deps.as_ref(), 1, dealer, None, Option::from(per_page)) +// .unwrap(); +// assert_eq!(2, page1.dealings.len()); +// } +// +// fill_dealings(deps.as_mut(), 3, 10, 3); +// +// for dealer in 0..10 { +// let dealer = format!("dealer{dealer}"); +// // page1 still has 2 results +// let page1 = query_dealings_paged( +// deps.as_ref(), +// 3, +// dealer.clone(), +// 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(), +// 3, +// dealer, +// Option::from(start_after), +// Option::from(per_page), +// ) +// .unwrap(); +// +// assert_eq!(1, page2.dealings.len()); +// } +// +// fill_dealings(deps.as_mut(), 4, 10, 4); +// +// for dealer in 0..10 { +// let dealer = format!("dealer{dealer}"); +// let page1 = query_dealings_paged( +// deps.as_ref(), +// 4, +// dealer.clone(), +// None, +// Option::from(per_page), +// ) +// .unwrap(); +// let start_after = page1.start_next_after.unwrap(); +// let page2 = query_dealings_paged( +// deps.as_ref(), +// 4, +// dealer, +// Option::from(start_after), +// Option::from(per_page), +// ) +// .unwrap(); +// +// // now we have 2 pages, with 2 results on the second page +// assert_eq!(2, page2.dealings.len()); +// } +// } +// } +// } diff --git a/contracts/coconut-dkg/src/dealings/storage.rs b/contracts/coconut-dkg/src/dealings/storage.rs index 49d8958897..8946a449f3 100644 --- a/contracts/coconut-dkg/src/dealings/storage.rs +++ b/contracts/coconut-dkg/src/dealings/storage.rs @@ -1,10 +1,12 @@ // Copyright 2022 - 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, Path, Prefix, Prefixer, PrimaryKey}; +use cw_storage_plus::{Bound, Key, KeyDeserialize, Map, Path, Prefix, Prefixer, PrimaryKey}; use nym_coconut_dkg_common::types::{ - ContractDealing, ContractSafeBytes, DealingIndex, EpochId, PartialContractDealing, + ChunkIndex, ContractSafeBytes, DealingIndex, DealingMetadata, EpochId, PartialContractDealing, + PartialContractDealingData, }; pub(crate) const DEALINGS_PAGE_MAX_LIMIT: u32 = 2; @@ -12,54 +14,105 @@ 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> = + Map::new("dealings_metadata"); + +pub(crate) fn metadata_exists( + storage: &dyn Storage, + epoch_id: EpochId, + dealer: Dealer, + dealing_index: DealingIndex, +) -> bool { + DEALINGS_METADATA.has(storage, (epoch_id, dealer, dealing_index)) +} + +pub(crate) fn may_read_metadata( + storage: &dyn Storage, + epoch_id: EpochId, + dealer: Dealer, + dealing_index: DealingIndex, +) -> Result, ContractError> { + Ok(DEALINGS_METADATA.may_load(storage, (epoch_id, dealer, dealing_index))?) +} + +pub(crate) fn must_read_metadata( + storage: &dyn Storage, + epoch_id: EpochId, + dealer: Dealer, + dealing_index: DealingIndex, +) -> Result { + DEALINGS_METADATA + .may_load(storage, (epoch_id, dealer, dealing_index))? + .ok_or_else(|| ContractError::UnavailableDealingMetadata { + epoch_id, + dealer: dealer.to_owned(), + dealing_index, + }) +} + +pub(crate) fn store_metadata( + storage: &mut dyn Storage, + epoch_id: EpochId, + dealer: Dealer, + dealing_index: DealingIndex, + metadata: &DealingMetadata, +) -> Result<(), ContractError> { + Ok(DEALINGS_METADATA.save(storage, (epoch_id, dealer, dealing_index), metadata)?) +} + // dealings are stored in a multilevel map with the following hierarchy: // - epoch-id: // - issuer-address: // - dealing id: -// - dealing content +// - chunk_id: +// - 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; -// part of `StoredDealing` to make existence lookup cheaper -// TODO: do it later since we need to chunk the dealings anyway -// pub(crate) struct UNIMPLEMENTED_DealingLookup; - impl StoredDealing { const NAMESPACE: &'static [u8] = b"dealing"; - fn deserialize_dealing_record(kv: Record) -> StdResult<(DealingIndex, ContractDealing)> { - let (k, v) = kv; - let index = ::from_vec(k)?; - let data = ContractSafeBytes(v); - - Ok((index, data)) + 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)) } fn storage_key( epoch_id: EpochId, dealer: Dealer, dealing_index: DealingIndex, + chunk_index: ChunkIndex, ) -> Path> { // just replicate the behaviour from `Map::key` - let key = (epoch_id, dealer, dealing_index); + // note: `PrimaryKey` trait is not implemented for tuple (T, U, V, W), only for up to (T, U, V) + // that's why we create a (T, U, (V, W)) tuple(s) instead + let key = (epoch_id, dealer, (dealing_index, chunk_index)); Path::new( Self::NAMESPACE, &key.key().iter().map(Key::as_ref).collect::>(), ) } - fn prefix(prefix: (EpochId, Dealer)) -> 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"), - ) - } + // 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"), + // // ) + // } pub(crate) fn exists( storage: &dyn Storage, @@ -67,7 +120,8 @@ impl StoredDealing { dealer: &Addr, dealing_index: DealingIndex, ) -> bool { - StoredDealing::storage_key(epoch_id, dealer, dealing_index).has(storage) + todo!() + // StoredDealing::storage_key(epoch_id, dealer, dealing_index).has(storage) } pub(crate) fn save( @@ -77,7 +131,8 @@ impl StoredDealing { dealing: PartialContractDealing, ) { // NOTE: we're storing bytes directly here! - let storage_key = StoredDealing::storage_key(epoch_id, dealer, dealing.index); + let storage_key = + Self::storage_key(epoch_id, dealer, dealing.dealing_index, dealing.chunk_index); storage.set(&storage_key, dealing.data.as_slice()); } @@ -86,212 +141,215 @@ impl StoredDealing { epoch_id: EpochId, dealer: Dealer, dealing_index: DealingIndex, - ) -> Option { - let storage_key = StoredDealing::storage_key(epoch_id, dealer, dealing_index); - let raw_dealing = storage.get(&storage_key); - raw_dealing.map(ContractSafeBytes) + chunk_index: ChunkIndex, + ) -> Option { + let storage_key = Self::storage_key(epoch_id, dealer, dealing_index, chunk_index); + storage.get(&storage_key).map(ContractSafeBytes) } - pub(crate) fn prefix_range<'a>( - storage: &'a dyn Storage, - prefix: (EpochId, Dealer), - start: Option>, - ) -> impl Iterator> + 'a { - 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), + // 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)) + // } // iterate over all values, only to be used in tests due to the amount of data being returned #[cfg(test)] pub(crate) fn unchecked_all_entries( storage: &dyn Storage, - ) -> Vec<((EpochId, Addr, DealingIndex), ContractDealing)> { - 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), 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() } } -#[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, - ) -> ContractDealing { - 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 { - 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 { - 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().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().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, +// ) -> 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); +// } +// } diff --git a/contracts/coconut-dkg/src/dealings/transactions.rs b/contracts/coconut-dkg/src/dealings/transactions.rs index c53afb47bb..cd2d32a758 100644 --- a/contracts/coconut-dkg/src/dealings/transactions.rs +++ b/contracts/coconut-dkg/src/dealings/transactions.rs @@ -2,60 +2,192 @@ // SPDX-License-Identifier: Apache-2.0 use crate::dealers::storage as dealers_storage; -use crate::dealings::storage::StoredDealing; +use crate::dealings::storage::{ + metadata_exists, must_read_metadata, store_metadata, StoredDealing, +}; use crate::epoch_state::storage::{CURRENT_EPOCH, INITIAL_REPLACEMENT_DATA}; use crate::epoch_state::utils::check_epoch_state; use crate::error::ContractError; use crate::state::storage::STATE; -use cosmwasm_std::{DepsMut, MessageInfo, Response}; -use nym_coconut_dkg_common::types::{EpochState, PartialContractDealing}; +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Storage}; +use nym_coconut_dkg_common::types::{ + DealingMetadata, EpochState, PartialContractDealing, MAX_DEALING_CHUNKS, +}; -pub fn try_commit_dealings( - deps: DepsMut<'_>, - info: MessageInfo, - dealing: PartialContractDealing, +// make sure the epoch is in the dealing exchange and the message sender is a valid dealer for this epoch +fn ensure_permission( + storage: &dyn Storage, + sender: &Addr, resharing: bool, -) -> Result { - check_epoch_state(deps.storage, EpochState::DealingExchange { resharing })?; +) -> Result<(), ContractError> { + check_epoch_state(storage, EpochState::DealingExchange { resharing })?; + // ensure the sender is a dealer if dealers_storage::current_dealers() - .may_load(deps.storage, &info.sender)? + .may_load(storage, sender)? .is_none() { return Err(ContractError::NotADealer); } if resharing && !INITIAL_REPLACEMENT_DATA - .load(deps.storage)? + .load(storage)? .initial_dealers - .contains(&info.sender) + .contains(sender) { return Err(ContractError::NotAnInitialDealer); } + Ok(()) +} + +pub fn try_submit_dealings_metadata( + deps: DepsMut, + info: MessageInfo, + metadata: DealingMetadata, + resharing: bool, +) -> Result { + ensure_permission(deps.storage, &info.sender, resharing)?; let state = STATE.load(deps.storage)?; let epoch = CURRENT_EPOCH.load(deps.storage)?; - // check if the index is in range without doing expensive storage reads + // don't allow overwriting existing metadata + if metadata_exists( + deps.storage, + epoch.epoch_id, + &info.sender, + metadata.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, + }); + } + + // make sure the dealing index is in the allowed range // note: dealing indexing starts from 0 - if dealing.index >= state.key_size { + if metadata.dealing_index >= state.key_size { return Err(ContractError::DealingOutOfRange { epoch_id: epoch.epoch_id, dealer: info.sender, - index: dealing.index, + index: metadata.dealing_index, key_size: state.key_size, }); } - // check if this dealer has already committed this particular dealing - if StoredDealing::exists(deps.storage, epoch.epoch_id, &info.sender, dealing.index) { - return Err(ContractError::DealingAlreadyCommitted { + let chunks = metadata.submitted_chunks.len(); + + // 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 { + return Err(ContractError::TooFragmentedMetadata { epoch_id: epoch.epoch_id, dealer: info.sender, - index: dealing.index, + dealing_index: metadata.dealing_index, + chunks, }); } + // 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 + #[allow(clippy::unwrap_used)] + let first_chunk_size = metadata.submitted_chunks.first_key_value().unwrap().1.size; + + for (&chunk_index, chunk_info) in metadata.submitted_chunks.iter().take(chunks - 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, + first_chunk_size, + size: chunk_info.size, + }); + } + } + + // finally, store the metadata + store_metadata( + deps.storage, + epoch.epoch_id, + &info.sender, + metadata.dealing_index, + &metadata, + )?; + + Ok(Response::new()) +} + +pub fn try_commit_dealings_chunk( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + dealing: PartialContractDealing, + resharing: bool, +) -> Result { + ensure_permission(deps.storage, &info.sender, resharing)?; + + let epoch = CURRENT_EPOCH.load(deps.storage)?; + + // read meta + let mut metadata = must_read_metadata( + deps.storage, + epoch.epoch_id, + &info.sender, + dealing.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 { + return Err(ContractError::DealingChunkNotInMetadata { + epoch_id: epoch.epoch_id, + dealer: info.sender, + dealing_index: dealing.dealing_index, + chunk_index: dealing.chunk_index, + }); + }; + + // check if this dealer has already committed this particular dealing chunk + if let Some(submission_height) = submission_status.submission_height { + return Err(ContractError::DealingChunkAlreadyCommitted { + epoch_id: epoch.epoch_id, + dealer: info.sender, + dealing_index: dealing.dealing_index, + chunk_index: dealing.chunk_index, + block_height: submission_height, + }); + } + + // update the metadata + submission_status.submission_height = Some(env.block.height); + store_metadata( + deps.storage, + epoch.epoch_id, + &info.sender, + dealing.dealing_index, + &metadata, + )?; + + // store the dealing StoredDealing::save(deps.storage, epoch.epoch_id, &info.sender, dealing); Ok(Response::new()) @@ -78,127 +210,128 @@ pub(crate) mod tests { #[test] fn invalid_commit_dealing() { - let mut deps = helpers::init_contract(); - let mut env = mock_env(); - try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); - - let owner = Addr::unchecked("owner1"); - let info = mock_info(owner.as_str(), &[]); - let dealing = partial_dealing_fixture(); - - let ret = - try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false).unwrap_err(); - assert_eq!( - ret, - ContractError::IncorrectEpochState { - current_state: EpochState::PublicKeySubmission { resharing: false }.to_string(), - expected_state: EpochState::DealingExchange { resharing: false }.to_string() - } - ); - - env.block.time = env - .block - .time - .plus_seconds(TimeConfiguration::default().public_key_submission_time_secs); - add_fixture_dealer(deps.as_mut()); - advance_epoch_state(deps.as_mut(), env).unwrap(); - - let ret = - try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false).unwrap_err(); - assert_eq!(ret, ContractError::NotADealer); - - let dealer_details = DealerDetails { - address: owner.clone(), - 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) - .unwrap(); - - // assume we're in resharing mode - CURRENT_EPOCH - .update::<_, ContractError>(deps.as_mut().storage, |mut epoch| { - epoch.state = EpochState::DealingExchange { resharing: true }; - Ok(epoch) - }) - .unwrap(); - INITIAL_REPLACEMENT_DATA - .save( - deps.as_mut().storage, - &InitialReplacementData { - initial_dealers: vec![], - initial_height: 1, - }, - ) - .unwrap(); - let ret = - try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), true).unwrap_err(); - assert_eq!(ret, ContractError::NotAnInitialDealer); - - INITIAL_REPLACEMENT_DATA - .update::<_, ContractError>(deps.as_mut().storage, |mut data| { - data.initial_dealers = vec![dealer_details_fixture(1).address]; - Ok(data) - }) - .unwrap(); - - // back to 'normal' mode - CURRENT_EPOCH - .update::<_, ContractError>(deps.as_mut().storage, |mut epoch| { - epoch.state = EpochState::DealingExchange { resharing: false }; - Ok(epoch) - }) - .unwrap(); - - // dealing out of range - let ret = try_commit_dealings( - deps.as_mut(), - info.clone(), - PartialContractDealing { - index: 42, - data: ContractSafeBytes(vec![1, 2, 3]), - }, - false, - ) - .unwrap_err(); - assert_eq!( - ret, - ContractError::DealingOutOfRange { - epoch_id: 0, - dealer: info.sender.clone(), - index: 42, - key_size: DEFAULT_DEALINGS as u32, - } - ); - - // 'good' dealing - let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false); - assert!(ret.is_ok()); - - // duplicate dealing - let ret = - try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false).unwrap_err(); - assert_eq!( - ret, - ContractError::DealingAlreadyCommitted { - epoch_id: 0, - dealer: info.sender.clone(), - index: 0, - } - ); - - // same index, but next epoch - CURRENT_EPOCH - .update::<_, ContractError>(deps.as_mut().storage, |mut epoch| { - epoch.epoch_id += 1; - Ok(epoch) - }) - .unwrap(); - - let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false); - assert!(ret.is_ok()); + todo!() + // let mut deps = helpers::init_contract(); + // let mut env = mock_env(); + // try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap(); + // + // let owner = Addr::unchecked("owner1"); + // let info = mock_info(owner.as_str(), &[]); + // let dealing = partial_dealing_fixture(); + // + // let ret = + // try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false).unwrap_err(); + // assert_eq!( + // ret, + // ContractError::IncorrectEpochState { + // current_state: EpochState::PublicKeySubmission { resharing: false }.to_string(), + // expected_state: EpochState::DealingExchange { resharing: false }.to_string() + // } + // ); + // + // env.block.time = env + // .block + // .time + // .plus_seconds(TimeConfiguration::default().public_key_submission_time_secs); + // add_fixture_dealer(deps.as_mut()); + // advance_epoch_state(deps.as_mut(), env).unwrap(); + // + // let ret = + // try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false).unwrap_err(); + // assert_eq!(ret, ContractError::NotADealer); + // + // let dealer_details = DealerDetails { + // address: owner.clone(), + // 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) + // .unwrap(); + // + // // assume we're in resharing mode + // CURRENT_EPOCH + // .update::<_, ContractError>(deps.as_mut().storage, |mut epoch| { + // epoch.state = EpochState::DealingExchange { resharing: true }; + // Ok(epoch) + // }) + // .unwrap(); + // INITIAL_REPLACEMENT_DATA + // .save( + // deps.as_mut().storage, + // &InitialReplacementData { + // initial_dealers: vec![], + // initial_height: 1, + // }, + // ) + // .unwrap(); + // let ret = + // try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), true).unwrap_err(); + // assert_eq!(ret, ContractError::NotAnInitialDealer); + // + // INITIAL_REPLACEMENT_DATA + // .update::<_, ContractError>(deps.as_mut().storage, |mut data| { + // data.initial_dealers = vec![dealer_details_fixture(1).address]; + // Ok(data) + // }) + // .unwrap(); + // + // // back to 'normal' mode + // CURRENT_EPOCH + // .update::<_, ContractError>(deps.as_mut().storage, |mut epoch| { + // epoch.state = EpochState::DealingExchange { resharing: false }; + // Ok(epoch) + // }) + // .unwrap(); + // + // // dealing out of range + // let ret = try_commit_dealings( + // deps.as_mut(), + // info.clone(), + // PartialContractDealing { + // dealing_index: 42, + // data: ContractSafeBytes(vec![1, 2, 3]), + // }, + // false, + // ) + // .unwrap_err(); + // assert_eq!( + // ret, + // ContractError::DealingOutOfRange { + // epoch_id: 0, + // dealer: info.sender.clone(), + // index: 42, + // key_size: DEFAULT_DEALINGS as u32, + // } + // ); + // + // // 'good' dealing + // let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false); + // assert!(ret.is_ok()); + // + // // duplicate dealing + // let ret = + // try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false).unwrap_err(); + // assert_eq!( + // ret, + // ContractError::DealingAlreadyCommitted { + // epoch_id: 0, + // dealer: info.sender.clone(), + // index: 0, + // } + // ); + // + // // same index, but next epoch + // CURRENT_EPOCH + // .update::<_, ContractError>(deps.as_mut().storage, |mut epoch| { + // epoch.epoch_id += 1; + // Ok(epoch) + // }) + // .unwrap(); + // + // let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false); + // assert!(ret.is_ok()); } } diff --git a/contracts/coconut-dkg/src/error.rs b/contracts/coconut-dkg/src/error.rs index c28e37a905..623e28cafe 100644 --- a/contracts/coconut-dkg/src/error.rs +++ b/contracts/coconut-dkg/src/error.rs @@ -3,7 +3,7 @@ use cosmwasm_std::{Addr, StdError}; use cw_controllers::AdminError; -use nym_coconut_dkg_common::types::{DealingIndex, EpochId}; +use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId, MAX_DEALING_CHUNKS}; use thiserror::Error; /// Custom errors for contract failure conditions. @@ -37,7 +37,7 @@ pub enum ContractError { EpochNotInitialised, #[error( - "Requested action needs state to be {expected_state}, currently in state {current_state}, " + "Requested action needs state to be {expected_state}, currently in state {current_state}" )] IncorrectEpochState { current_state: String, @@ -50,18 +50,24 @@ pub enum ContractError { #[error("This sender is not a dealer for the current resharing epoch")] NotAnInitialDealer, - #[error( - "Dealer {dealer} has already committed dealing for epoch {epoch_id} with index {index}" - )] - DealingAlreadyCommitted { + #[error("Dealer {dealer} has already committed dealing chunk for epoch {epoch_id} with dealing index {dealing_index} and chunk index {chunk_index} at height {block_height}")] + DealingChunkAlreadyCommitted { epoch_id: EpochId, dealer: Addr, - index: DealingIndex, + dealing_index: DealingIndex, + chunk_index: ChunkIndex, + block_height: u64, }, - #[error( - "Dealer {dealer} has attempted to commit dealing for epoch {epoch_id} with index {index} while the key size is set to {key_size}" - )] + #[error("dealer {dealer} tried to commit chunk {chunk_index} of dealing {dealing_index} for epoch {epoch_id}, but it hasn't been declared in the prior metadata")] + DealingChunkNotInMetadata { + epoch_id: EpochId, + dealer: Addr, + dealing_index: DealingIndex, + chunk_index: ChunkIndex, + }, + + #[error("dealer {dealer} has attempted to commit dealing chunk for epoch {epoch_id} with dealing index {index} while the key size is set to {key_size}")] DealingOutOfRange { epoch_id: EpochId, dealer: Addr, @@ -69,6 +75,45 @@ pub enum ContractError { key_size: u32, }, + #[error("dealer {dealer} has attempted to commit dealing metadata for epoch {epoch_id} for dealing index {dealing_index} with {chunks} chunks while at most {} chunks are allowed", MAX_DEALING_CHUNKS)] + TooFragmentedMetadata { + epoch_id: EpochId, + dealer: Addr, + dealing_index: DealingIndex, + chunks: usize, + }, + + #[error("")] + UnevenChunkSplit { + epoch_id: EpochId, + dealer: Addr, + dealing_index: DealingIndex, + chunk_index: ChunkIndex, + first_chunk_size: usize, + size: usize, + }, + + #[error("dealer {dealer} has attempted to commit dealing metadata for epoch {epoch_id} for dealing index {dealing_index} zero chunks")] + EmptyMetadata { + epoch_id: EpochId, + dealer: Addr, + dealing_index: DealingIndex, + }, + + #[error("metadata for dealing for epoch {epoch_id} from {dealer} at index {dealing_index} does not exist")] + UnavailableDealingMetadata { + epoch_id: EpochId, + dealer: Addr, + dealing_index: DealingIndex, + }, + + #[error("metadata for dealing for epoch {epoch_id} from {dealer} at index {dealing_index} already exists")] + MetadataAlreadyExists { + epoch_id: EpochId, + dealer: Addr, + dealing_index: DealingIndex, + }, + #[error("This dealer has already committed {commitment}")] AlreadyCommitted { commitment: String }, diff --git a/contracts/coconut-dkg/src/support/tests/fixtures.rs b/contracts/coconut-dkg/src/support/tests/fixtures.rs index 7a7f520bcc..153190d289 100644 --- a/contracts/coconut-dkg/src/support/tests/fixtures.rs +++ b/contracts/coconut-dkg/src/support/tests/fixtures.rs @@ -24,10 +24,11 @@ pub fn dealing_bytes_fixture() -> ContractSafeBytes { } pub fn partial_dealing_fixture() -> PartialContractDealing { - PartialContractDealing { - index: 0, - data: ContractSafeBytes(vec![1, 2, 3]), - } + todo!() + // PartialContractDealing { + // dealing_index: 0, + // data: ContractSafeBytes(vec![1, 2, 3]), + // } } pub fn dealer_details_fixture(assigned_index: u64) -> DealerDetails {