From 961775417ff9de2c03dab9f1bb013eb3dd045735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 2 Feb 2024 11:53:53 +0000 Subject: [PATCH] handle chunking on nym-api side --- .../nyxd/contract_traits/dkg_query_client.rs | 20 ++- .../coconut-dkg/src/dealing.rs | 100 +++++++++++++-- .../coconut-dkg/src/msg.rs | 7 +- .../contracts-common/src/dealings.rs | 8 +- contracts/coconut-dkg/src/contract.rs | 6 +- contracts/coconut-dkg/src/dealings/queries.rs | 58 ++++++--- contracts/coconut-dkg/src/dealings/storage.rs | 2 +- .../coconut-dkg/src/dealings/transactions.rs | 16 ++- contracts/coconut-dkg/src/error.rs | 12 +- nym-api/src/coconut/client.rs | 62 +++++++-- nym-api/src/coconut/dkg/client.rs | 66 ++++++---- nym-api/src/coconut/dkg/dealing.rs | 119 ++++++++++++++---- nym-api/src/coconut/dkg/helpers.rs | 5 +- nym-api/src/coconut/dkg/key_derivation.rs | 107 ++++++++++------ nym-api/src/coconut/dkg/key_validation.rs | 2 +- nym-api/src/coconut/error.rs | 13 +- nym-api/src/coconut/tests/mod.rs | 2 +- nym-api/src/support/nyxd/mod.rs | 60 +++++++-- 18 files changed, 521 insertions(+), 144 deletions(-) diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs index 5916718c03..5a3c019396 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs @@ -13,8 +13,8 @@ use serde::Deserialize; pub use nym_coconut_dkg_common::{ dealer::{DealerDetailsResponse, PagedDealerResponse}, dealing::{ - DealingChunkResponse, DealingChunkStatusResponse, DealingMetadataResponse, - DealingStatusResponse, + DealerDealingsStatusResponse, DealingChunkResponse, DealingChunkStatusResponse, + DealingMetadataResponse, DealingStatusResponse, }, msg::QueryMsg as DkgQueryMsg, types::{ @@ -93,6 +93,16 @@ pub trait DkgQueryClient { self.query_dkg_contract(request).await } + async fn get_dealer_dealings_status( + &self, + epoch_id: EpochId, + dealer: String, + ) -> Result { + let request = DkgQueryMsg::GetDealerDealingsStatus { epoch_id, dealer }; + + self.query_dkg_contract(request).await + } + async fn get_dealing_status( &self, epoch_id: EpochId, @@ -114,7 +124,7 @@ pub trait DkgQueryClient { dealer: String, dealing_index: DealingIndex, chunk_index: ChunkIndex, - ) -> Result { + ) -> Result { let request = DkgQueryMsg::GetDealingChunkStatus { epoch_id, dealer, @@ -217,6 +227,7 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::IgnoreValue; + use nym_coconut_dkg_common::msg::QueryMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] @@ -254,6 +265,9 @@ mod tests { } => client .get_dealings_metadata(epoch_id, dealer, dealing_index) .ignore(), + QueryMsg::GetDealerDealingsStatus { epoch_id, dealer } => { + client.get_dealer_dealings_status(epoch_id, dealer).ignore() + } DkgQueryMsg::GetDealingChunkStatus { epoch_id, dealer, diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealing.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealing.rs index 849c093d7f..ceff14bb84 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealing.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealing.rs @@ -2,9 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::types::{ChunkIndex, DealingIndex, EpochId, PartialContractDealingData}; +use contracts_common::dealings::ContractSafeBytes; use cosmwasm_schema::cw_serde; use cosmwasm_std::Addr; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; /// Defines the maximum size of a dealing chunk. Currently set to 2kB pub const MAX_DEALING_CHUNK_SIZE: usize = 2048; @@ -18,7 +19,26 @@ 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; +pub fn chunk_dealing( + dealing_index: DealingIndex, + dealing_bytes: Vec, + chunk_size: usize, +) -> HashMap { + let mut chunks = HashMap::new(); + for (chunk_index, chunk) in dealing_bytes.chunks(chunk_size).enumerate() { + let chunk = PartialContractDealing { + dealing_index, + chunk_index: chunk_index as ChunkIndex, + data: ContractSafeBytes(chunk.to_vec()), + }; + chunks.insert(chunk_index as ChunkIndex, chunk); + } + + chunks +} + #[cw_serde] +#[derive(Copy)] pub struct DealingChunkInfo { pub size: usize, } @@ -45,13 +65,26 @@ impl DealingChunkInfo { } #[cw_serde] +#[derive(Copy)] pub struct SubmittedChunk { pub info: DealingChunkInfo, + pub status: ChunkSubmissionStatus, +} + +#[cw_serde] +#[derive(Default, Copy)] +pub struct ChunkSubmissionStatus { // this field is updated by the contract itself to indicate when this particular chunk has been received pub submission_height: Option, } +impl ChunkSubmissionStatus { + pub fn submitted(&self) -> bool { + self.submission_height.is_some() + } +} + impl From for SubmittedChunk { fn from(value: DealingChunkInfo) -> Self { SubmittedChunk::new(value) @@ -62,9 +95,13 @@ impl SubmittedChunk { pub fn new(info: DealingChunkInfo) -> Self { SubmittedChunk { info, - submission_height: None, + status: Default::default(), } } + + pub fn submitted(&self) -> bool { + self.status.submitted() + } } #[cw_serde] @@ -87,14 +124,19 @@ impl DealingMetadata { } pub fn is_complete(&self) -> bool { - self.submitted_chunks - .values() - .all(|c| c.submission_height.is_some()) + self.submitted_chunks.values().all(|c| c.submitted()) } pub fn total_size(&self) -> usize { self.submitted_chunks.values().map(|c| c.info.size).sum() } + + pub fn submission_statuses(&self) -> BTreeMap { + self.submitted_chunks + .iter() + .map(|(id, c)| (*id, c.status)) + .collect() + } } #[cw_serde] @@ -152,7 +194,7 @@ pub struct DealingChunkStatusResponse { pub chunk_index: ChunkIndex, - pub submission_height: Option, + pub status: ChunkSubmissionStatus, } #[cw_serde] @@ -163,7 +205,51 @@ pub struct DealingStatusResponse { pub dealing_index: DealingIndex, - pub full_dealing_submitted: bool, + pub status: DealingStatus, +} + +#[cw_serde] +pub struct DealingStatus { + pub has_metadata: bool, + + pub fully_submitted: bool, + + pub chunk_submission_status: BTreeMap, +} + +impl From> for DealingStatus { + fn from(metadata: Option) -> Self { + DealingStatus { + has_metadata: metadata.is_some(), + fully_submitted: metadata + .as_ref() + .map(|m| m.is_complete()) + .unwrap_or_default(), + chunk_submission_status: metadata + .map(|m| m.submission_statuses()) + .unwrap_or_default(), + } + } +} + +#[cw_serde] +pub struct DealerDealingsStatusResponse { + pub epoch_id: EpochId, + + pub dealer: Addr, + + pub all_dealings_fully_submitted: bool, + + pub dealing_submission_status: BTreeMap, +} + +impl DealerDealingsStatusResponse { + pub fn full_dealings(&self) -> usize { + self.dealing_submission_status + .values() + .filter(|s| s.fully_submitted) + .count() + } } #[cfg(test)] diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs index 95a7ae8445..e677efd80f 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -13,8 +13,8 @@ use cosmwasm_schema::cw_serde; use crate::{ dealer::{DealerDetailsResponse, PagedDealerResponse}, dealing::{ - DealingChunkResponse, DealingChunkStatusResponse, DealingMetadataResponse, - DealingStatusResponse, + DealerDealingsStatusResponse, DealingChunkResponse, DealingChunkStatusResponse, + DealingMetadataResponse, DealingStatusResponse, }, types::{Epoch, InitialReplacementData, State}, verification_key::{PagedVKSharesResponse, VkShareResponse}, @@ -108,6 +108,9 @@ pub enum QueryMsg { dealing_index: DealingIndex, }, + #[cfg_attr(feature = "schema", returns(DealerDealingsStatusResponse))] + GetDealerDealingsStatus { epoch_id: EpochId, dealer: String }, + #[cfg_attr(feature = "schema", returns(DealingStatusResponse))] GetDealingStatus { epoch_id: EpochId, diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs b/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs index ef29c337d5..32a32d055a 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs @@ -4,7 +4,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt::{Display, Formatter}; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; // some sane upper-bound size on byte sizes // currently set to 128 bytes @@ -23,6 +23,12 @@ impl Deref for ContractSafeBytes { } } +impl DerefMut for ContractSafeBytes { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + impl From> for ContractSafeBytes { fn from(value: Vec) -> Self { ContractSafeBytes(value) diff --git a/contracts/coconut-dkg/src/contract.rs b/contracts/coconut-dkg/src/contract.rs index 4680abc0f3..1ab567d59c 100644 --- a/contracts/coconut-dkg/src/contract.rs +++ b/contracts/coconut-dkg/src/contract.rs @@ -6,7 +6,8 @@ use crate::dealers::queries::{ }; use crate::dealers::transactions::try_add_dealer; use crate::dealings::queries::{ - query_dealing_chunk, query_dealing_chunk_status, query_dealing_metadata, query_dealing_status, + query_dealer_dealings_status, 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::{ @@ -149,6 +150,9 @@ pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result { + to_binary(&query_dealer_dealings_status(deps, epoch_id, dealer)?)? + } QueryMsg::GetDealingStatus { epoch_id, dealer, diff --git a/contracts/coconut-dkg/src/dealings/queries.rs b/contracts/coconut-dkg/src/dealings/queries.rs index a83fcfb284..c31eda5527 100644 --- a/contracts/coconut-dkg/src/dealings/queries.rs +++ b/contracts/coconut-dkg/src/dealings/queries.rs @@ -2,12 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 use crate::dealings::storage::{StoredDealing, DEALINGS_METADATA}; +use crate::state::storage::STATE; use cosmwasm_std::{Deps, StdResult}; use nym_coconut_dkg_common::dealing::{ - DealingChunkResponse, DealingChunkStatusResponse, DealingMetadataResponse, - DealingStatusResponse, + DealerDealingsStatusResponse, DealingChunkResponse, DealingChunkStatusResponse, + DealingMetadataResponse, DealingStatus, DealingStatusResponse, }; use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId}; +use std::collections::BTreeMap; /// Get the metadata associated with the particular dealing pub fn query_dealing_metadata( @@ -27,6 +29,34 @@ pub fn query_dealing_metadata( }) } +/// Get the status of all dealings of particular dealer for given epoch. +pub fn query_dealer_dealings_status( + deps: Deps<'_>, + epoch_id: EpochId, + dealer: String, +) -> StdResult { + let dealer = deps.api.addr_validate(&dealer)?; + let state = STATE.load(deps.storage)?; + + let mut dealing_submission_status: BTreeMap = BTreeMap::new(); + + // Since our key size is in single digit range, querying all of this at once on chain is fine + for dealing_index in 0..state.key_size { + let metadata = + DEALINGS_METADATA.may_load(deps.storage, (epoch_id, &dealer, dealing_index))?; + dealing_submission_status.insert(dealing_index, metadata.into()); + } + + Ok(DealerDealingsStatusResponse { + epoch_id, + dealer, + all_dealings_fully_submitted: dealing_submission_status + .values() + .all(|d| d.fully_submitted), + dealing_submission_status, + }) +} + /// Get the status of particular dealing, i.e. whether it has been fully submitted. pub fn query_dealing_status( deps: Deps<'_>, @@ -37,17 +67,11 @@ pub fn query_dealing_status( let dealer = deps.api.addr_validate(&dealer)?; 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, - full_dealing_submitted, + status: metadata.into(), }) } @@ -62,22 +86,18 @@ pub fn query_dealing_chunk_status( 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 - }; + let status = metadata + .as_ref() + .and_then(|m| m.submitted_chunks.get(&chunk_index)) + .map(|&c| c.status) + .unwrap_or_default(); Ok(DealingChunkStatusResponse { epoch_id, dealer, dealing_index, chunk_index, - submission_height, + status, }) } diff --git a/contracts/coconut-dkg/src/dealings/storage.rs b/contracts/coconut-dkg/src/dealings/storage.rs index ee3eda01ac..dc63bb3a38 100644 --- a/contracts/coconut-dkg/src/dealings/storage.rs +++ b/contracts/coconut-dkg/src/dealings/storage.rs @@ -127,7 +127,7 @@ impl StoredDealing { let Some(chunk_info) = metadata.submitted_chunks.get(&chunk_index) else { return Ok(false); }; - Ok(chunk_info.submission_height.is_some()) + Ok(chunk_info.status.submitted()) // StoredDealing::storage_key(epoch_id, dealer, dealing_index).has(storage) } diff --git a/contracts/coconut-dkg/src/dealings/transactions.rs b/contracts/coconut-dkg/src/dealings/transactions.rs index cc80cda31e..6bad968573 100644 --- a/contracts/coconut-dkg/src/dealings/transactions.rs +++ b/contracts/coconut-dkg/src/dealings/transactions.rs @@ -164,7 +164,7 @@ pub fn try_commit_dealings_chunk( }; // check if this dealer has already committed this particular dealing chunk - if let Some(submission_height) = submission_status.submission_height { + if let Some(submission_height) = submission_status.status.submission_height { return Err(ContractError::DealingChunkAlreadyCommitted { epoch_id: epoch.epoch_id, dealer: info.sender, @@ -174,8 +174,20 @@ pub fn try_commit_dealings_chunk( }); } + // check if the received chunk has the specified size + if submission_status.info.size != chunk.data.len() { + return Err(ContractError::InconsistentChunkLength { + epoch_id: epoch.epoch_id, + dealer: info.sender, + dealing_index: chunk.dealing_index, + chunk_index: chunk.chunk_index, + metadata_length: submission_status.info.size, + received: chunk.data.len(), + }); + } + // update the metadata - submission_status.submission_height = Some(env.block.height); + submission_status.status.submission_height = Some(env.block.height); store_metadata( deps.storage, epoch.epoch_id, diff --git a/contracts/coconut-dkg/src/error.rs b/contracts/coconut-dkg/src/error.rs index 6f58d98d6f..ca8edfe97f 100644 --- a/contracts/coconut-dkg/src/error.rs +++ b/contracts/coconut-dkg/src/error.rs @@ -84,7 +84,7 @@ pub enum ContractError { chunks: usize, }, - #[error("")] + #[error("the declared chunk split for epoch {epoch_id} from dealer {dealer} for dealing index {dealing_index} is uneven. first chunk has size of {first_chunk_size} while chunk at index {chunk_index} has {size}")] UnevenChunkSplit { epoch_id: EpochId, dealer: Addr, @@ -94,6 +94,16 @@ pub enum ContractError { size: usize, }, + #[error("the received chunk for epoch {epoch_id} from dealer {dealer} at dealing index {dealing_index} at chunk index {chunk_index} has inconsistent length. the metadata contains length of {metadata_length} while the received data is {received} bytes long")] + InconsistentChunkLength { + epoch_id: EpochId, + dealer: Addr, + dealing_index: DealingIndex, + chunk_index: ChunkIndex, + metadata_length: usize, + received: 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, diff --git a/nym-api/src/coconut/client.rs b/nym-api/src/coconut/client.rs index d27d5defe5..c4ef30ebef 100644 --- a/nym-api/src/coconut/client.rs +++ b/nym-api/src/coconut/client.rs @@ -1,14 +1,18 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::coconut::error::Result; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; -use nym_coconut_dkg_common::dealer::{DealerDetails, DealerDetailsResponse, DealingStatusResponse}; +use nym_coconut_dkg_common::dealer::{DealerDetails, DealerDetailsResponse}; +use nym_coconut_dkg_common::dealing::{ + DealerDealingsStatusResponse, DealingChunkInfo, DealingMetadata, DealingStatusResponse, + PartialContractDealing, +}; use nym_coconut_dkg_common::types::{ - DealingIndex, EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, - PartialContractDealing, State, + ChunkIndex, DealingIndex, EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, + PartialContractDealingData, State, }; use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use nym_contracts_common::IdentityKey; @@ -21,44 +25,77 @@ pub trait Client { async fn address(&self) -> AccountId; async fn dkg_contract_address(&self) -> Result; + async fn get_tx(&self, tx_hash: Hash) -> Result; + async fn get_proposal(&self, proposal_id: u64) -> Result; + async fn list_proposals(&self) -> Result>; + async fn get_vote(&self, proposal_id: u64, voter: String) -> Result; + async fn get_spent_credential( &self, blinded_serial_number: String, ) -> Result; async fn contract_state(&self) -> Result; + async fn get_current_epoch(&self) -> Result; + async fn group_member(&self, addr: String) -> Result; + async fn get_current_epoch_threshold(&self) -> Result>; + async fn get_initial_dealers(&self) -> Result>; + async fn get_self_registered_dealer_details(&self) -> Result; + + async fn get_dealer_dealings_status( + &self, + epoch_id: EpochId, + dealer: String, + ) -> Result; + async fn get_dealing_status( &self, epoch_id: EpochId, dealer: String, dealing_index: DealingIndex, ) -> Result; + async fn get_current_dealers(&self) -> Result>; - async fn get_dealings( + + async fn get_dealing_metadata( + &self, + epoch_id: EpochId, + dealer: String, + dealing_index: DealingIndex, + ) -> Result>; + + async fn get_dealing_chunk( &self, epoch_id: EpochId, dealer: &str, - ) -> Result>; + dealing_index: DealingIndex, + chunk_index: ChunkIndex, + ) -> Result>; async fn get_verification_key_share( &self, epoch_id: EpochId, dealer: String, ) -> Result>; + async fn get_verification_key_shares(&self, epoch_id: EpochId) -> Result>; + async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option) -> Result<()>; + async fn execute_proposal(&self, proposal_id: u64) -> Result<()>; + async fn advance_epoch_state(&self) -> Result<()>; + async fn register_dealer( &self, bte_key: EncodedBTEPublicKeyWithProof, @@ -66,11 +103,20 @@ pub trait Client { announce_address: String, resharing: bool, ) -> Result; - async fn submit_dealing( + + async fn submit_dealing_metadata( &self, - dealing: PartialContractDealing, + dealing_index: DealingIndex, + chunks: Vec, resharing: bool, ) -> Result; + + async fn submit_dealing_chunk( + &self, + chunk: PartialContractDealing, + resharing: bool, + ) -> Result; + async fn submit_verification_key_share( &self, share: VerificationKeyShare, diff --git a/nym-api/src/coconut/dkg/client.rs b/nym-api/src/coconut/dkg/client.rs index 0a2c3590e6..9397d31a11 100644 --- a/nym-api/src/coconut/dkg/client.rs +++ b/nym-api/src/coconut/dkg/client.rs @@ -1,4 +1,4 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::coconut::client::Client; @@ -6,9 +6,12 @@ use crate::coconut::error::CoconutError; use cw3::{ProposalResponse, Status, VoteResponse}; use cw4::MemberResponse; use nym_coconut_dkg_common::dealer::{DealerDetails, DealerDetailsResponse}; +use nym_coconut_dkg_common::dealing::{ + DealerDealingsStatusResponse, DealingChunkInfo, PartialContractDealing, +}; use nym_coconut_dkg_common::types::{ - DealingIndex, EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, NodeIndex, - PartialContractDealing, State as ContractState, + ChunkIndex, DealingIndex, EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, + NodeIndex, PartialContractDealingData, State as ContractState, }; use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use nym_contracts_common::IdentityKey; @@ -75,25 +78,32 @@ impl DkgClient { self.inner.get_current_dealers().await } - pub(crate) async fn get_dealing_status( - &self, - epoch_id: EpochId, - dealing_index: DealingIndex, - ) -> Result { - let address = self.inner.address().await.to_string(); - - self.inner - .get_dealing_status(epoch_id, address, dealing_index) - .await - .map(|r| r.dealing_submitted) - } - - pub(crate) async fn get_dealings( + pub(crate) async fn get_dealings_statuses( &self, epoch_id: EpochId, dealer: String, - ) -> Result, CoconutError> { - self.inner.get_dealings(epoch_id, &dealer).await + ) -> Result { + self.inner + .get_dealer_dealings_status(epoch_id, dealer) + .await + } + + pub(crate) async fn get_dealing_chunk( + &self, + epoch_id: EpochId, + dealer: &str, + dealing_index: DealingIndex, + chunk_index: ChunkIndex, + ) -> Result { + self.inner + .get_dealing_chunk(epoch_id, dealer, dealing_index, chunk_index) + .await? + .ok_or(CoconutError::MissingDealingChunk { + epoch_id, + dealer: dealer.to_string(), + dealing_index, + chunk_index, + }) } pub(crate) async fn get_verification_key_share>( @@ -165,12 +175,24 @@ impl DkgClient { Ok(node_index) } - pub(crate) async fn submit_dealing( + pub(crate) async fn submit_dealing_metadata( &self, - dealing: PartialContractDealing, + dealing_index: DealingIndex, + chunks: Vec, resharing: bool, ) -> Result<(), CoconutError> { - self.inner.submit_dealing(dealing, resharing).await?; + self.inner + .submit_dealing_metadata(dealing_index, chunks, resharing) + .await?; + Ok(()) + } + + pub(crate) async fn submit_dealing_chunk( + &self, + chunk: PartialContractDealing, + resharing: bool, + ) -> Result<(), CoconutError> { + self.inner.submit_dealing_chunk(chunk, resharing).await?; Ok(()) } diff --git a/nym-api/src/coconut/dkg/dealing.rs b/nym-api/src/coconut/dkg/dealing.rs index 50295428bd..ab2b850f99 100644 --- a/nym-api/src/coconut/dkg/dealing.rs +++ b/nym-api/src/coconut/dkg/dealing.rs @@ -7,12 +7,11 @@ use crate::coconut::dkg::controller::DkgController; use crate::coconut::error::CoconutError; use crate::coconut::keys::KeyPairWithEpoch; use log::debug; -use nym_coconut_dkg_common::types::{ - ContractDealing, DealingIndex, EpochId, PartialContractDealing, -}; +use nym_coconut_dkg_common::dealing::{chunk_dealing, DealingChunkInfo, MAX_DEALING_CHUNK_SIZE}; +use nym_coconut_dkg_common::types::{DealingIndex, EpochId}; use nym_dkg::{Dealing, Scalar}; use rand::{CryptoRng, RngCore}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Formatter}; use std::path::PathBuf; use thiserror::Error; @@ -63,6 +62,8 @@ pub enum DealingGenerationError { } impl DkgController { + const DEALING_CHUNK_SIZE: usize = MAX_DEALING_CHUNK_SIZE; + async fn generate_dealings( &mut self, epoch_id: EpochId, @@ -153,28 +154,100 @@ impl DkgController { resharing: bool, ) -> Result<(), DealingGenerationError> { let dealing_state = self.state.dealing_exchange_state(epoch_id)?; + let address = self.dkg_client.get_address().await.to_string(); + let status = self + .dkg_client + .get_dealings_statuses(epoch_id, address) + .await?; + + if status.all_dealings_fully_submitted { + warn!("we have actually submitted all dealings for epoch {epoch_id}, but somehow haven't finalized the state!"); + return Ok(()); + } + + // check which dealing is actually present on the chain (some might have gotten stuck in the mempool for quite a while) for (dealing_index, dealing) in &dealing_state.generated_dealings { - // check which dealing is actually present on the chain (some might have gotten stuck in the mempool for quite a while) - let dealing_submitted = self - .dkg_client - .get_dealing_status(epoch_id, *dealing_index) - .await?; + // check the dealing + let Some(dealing_status) = status.dealing_submission_status.get(dealing_index) else { + // we should NEVER see this error + error!("we have generated a dealing for index {dealing_index} but the contract does not require its submission!"); + continue; + }; - if dealing_submitted { - warn!("we have already submitted dealing {dealing_index} before - we probably crashed or the chain timed out!"); + if !dealing_status.has_metadata { + // if the metadata doesn't exist on the chain, we haven't submitted anything - treat it as fresh submission + self.submit_fresh_dealing(*dealing_index, dealing, resharing) + .await?; continue; } - warn!( - "we have already generated dealing {dealing_index} before, but failed to submit it" - ); - let contract_dealing = - PartialContractDealing::new(*dealing_index, ContractDealing::from(dealing)); + + if dealing_status.fully_submitted { + warn!("we have already submitted the full dealing {dealing_index} before - we probably crashed or the chain timed out!"); + continue; + } + + let mut needs_resubmission = HashSet::new(); + for (chunk_id, chunk_status) in &dealing_status.chunk_submission_status { + if !chunk_status.submitted() { + needs_resubmission.insert(chunk_id); + } else { + warn!("[dealing {dealing_index}]: we have already submitted chunk at index {chunk_id} before - we probably crashed or the chain timed out!"); + } + } + + warn!("[dealing {dealing_index}]: the following chunks need to be resubmitted: {needs_resubmission:?}"); + + // perform the chunking (again) + let mut chunks = + chunk_dealing(*dealing_index, dealing.to_bytes(), Self::DEALING_CHUNK_SIZE); + for chunk_index in needs_resubmission { + // this is a hard failure, panic level, actually. + // because we have already committed to dealings of particular size + // yet we don't have relevant chunks after chunking + let chunk = chunks + .remove(&chunk_index) + .expect("chunking specification has changed mid-exchange!"); + debug!("[dealing {dealing_index}]: resubmitting chunk index {chunk_index}"); + self.dkg_client + .submit_dealing_chunk(chunk, resharing) + .await?; + } + } + Ok(()) + } + + async fn submit_fresh_dealing( + &self, + dealing_index: DealingIndex, + dealing: &Dealing, + resharing: bool, + ) -> Result<(), DealingGenerationError> { + let bytes = dealing.to_bytes(); + + // construct metadata + let chunk_info = DealingChunkInfo::construct(bytes.len(), Self::DEALING_CHUNK_SIZE); + + let total_chunks = chunk_info.len(); + debug!("dealing at index {dealing_index} has been chunked into {total_chunks} pieces",); + + // submit the metadata + self.dkg_client + .submit_dealing_metadata(dealing_index, chunk_info, resharing) + .await?; + + // actually chunk the dealing and submit the chunks + let chunks = chunk_dealing(dealing_index, bytes, Self::DEALING_CHUNK_SIZE); + + for (chunk_index, chunk) in chunks { + let human_index = chunk_index + 1; + debug!("[dealing {dealing_index}]: submitting chunk index {chunk_index} ({human_index}/{total_chunks})"); self.dkg_client - .submit_dealing(contract_dealing, resharing) + .submit_dealing_chunk(chunk, resharing) .await?; } + Ok(()) } @@ -376,15 +449,11 @@ impl DkgController { self.state.persist()?; } - for (i, (dealing_index, dealing)) in dealings.iter().enumerate() { + for (i, (&dealing_index, dealing)) in dealings.iter().enumerate() { let i = i + 1; debug!("submitting dealing index {dealing_index} ({i}/{total})"); - let contract_dealing = - PartialContractDealing::new(*dealing_index, ContractDealing::from(dealing)); - - self.dkg_client - .submit_dealing(contract_dealing, resharing) + self.submit_fresh_dealing(dealing_index, dealing, resharing) .await?; } @@ -481,7 +550,9 @@ pub(crate) mod tests { for submitted_dealing in submitted_dealings { let dealing = Dealing::try_from_bytes(submitted_dealing.data.as_slice())?; assert_eq!( - generated_dealings.get(&submitted_dealing.index).unwrap(), + generated_dealings + .get(&submitted_dealing.dealing_index) + .unwrap(), &dealing ) } diff --git a/nym-api/src/coconut/dkg/helpers.rs b/nym-api/src/coconut/dkg/helpers.rs index 0bb6f5caed..e36c1c04fc 100644 --- a/nym-api/src/coconut/dkg/helpers.rs +++ b/nym-api/src/coconut/dkg/helpers.rs @@ -3,7 +3,6 @@ use crate::coconut::dkg::controller::DkgController; use crate::coconut::error::CoconutError; -use cosmwasm_std::Addr; use cw3::{ProposalResponse, Status}; use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs; use nym_validator_client::nyxd::AccountId; @@ -15,7 +14,7 @@ impl DkgController { &self, dkg_contract: &AccountId, proposal: &ProposalResponse, - ) -> Option<(Addr, u64)> { + ) -> Option<(String, u64)> { // make sure the proposal we're checking is: // - still open (not point in voting for anything that has already expired) // - was proposed by the DKG contract - so that we'd ignore anything from malicious dealers @@ -30,7 +29,7 @@ impl DkgController { pub(crate) async fn get_validation_proposals( &self, - ) -> Result, CoconutError> { + ) -> Result, CoconutError> { let dkg_contract = self.dkg_client.dkg_contract_address().await?; // FUTURE OPTIMIZATION: don't query for ALL proposals. say if we're in epoch 50, diff --git a/nym-api/src/coconut/dkg/key_derivation.rs b/nym-api/src/coconut/dkg/key_derivation.rs index e7765f2e77..16dbc5ae81 100644 --- a/nym-api/src/coconut/dkg/key_derivation.rs +++ b/nym-api/src/coconut/dkg/key_derivation.rs @@ -12,7 +12,7 @@ use cosmwasm_std::Addr; use log::debug; use nym_coconut::{check_vk_pairing, Base58, SecretKey, VerificationKey}; use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID; -use nym_coconut_dkg_common::types::{DealingIndex, EpochId, NodeIndex, PartialContractDealing}; +use nym_coconut_dkg_common::types::{DealingIndex, EpochId, NodeIndex}; use nym_coconut_interface::KeyPair as CoconutKeyPair; use nym_dkg::{ bte::{self, decrypt_share}, @@ -21,7 +21,6 @@ use nym_dkg::{ use nym_validator_client::nyxd::cosmwasm_client::logs::{find_attribute, Log}; use nym_validator_client::nyxd::Hash; use rand::{CryptoRng, RngCore}; -use rocket::form::validate::Contains; use std::collections::{BTreeMap, HashMap}; use std::ops::Deref; use thiserror::Error; @@ -75,7 +74,7 @@ impl DkgController { epoch_id: EpochId, dealer: &Addr, epoch_receivers: &BTreeMap, - raw_dealings: Vec, + raw_dealings: HashMap>, prior_public_key: Option, ) -> Result, DealerRejectionReason>, KeyDerivationError> { @@ -105,11 +104,9 @@ impl DkgController { let mut temp_verified = Vec::with_capacity(raw_dealings.len()); // make sure ALL of them verify correctly, we can't have a situation where dealing 2 is valid but dealing 3 is not - for raw_dealing in raw_dealings { - let index = raw_dealing.index; - + for (index, data) in raw_dealings { // recover the actual dealing from its submitted bytes representation - let dealing = match Dealing::try_from_bytes(&raw_dealing.data) { + let dealing = match Dealing::try_from_bytes(&data) { Ok(dealing) => dealing, Err(err) => { warn!("failed to recover dealing {index} from {dealer}: {err}"); @@ -184,6 +181,63 @@ impl DkgController { )) } + async fn get_raw_dealings( + &self, + epoch_id: EpochId, + dealer: &Addr, + resharing: bool, + initial_dealers: &[Addr], + ) -> Result>, DealerRejectionReason>, KeyDerivationError> + { + let dealing_statuses = self + .dkg_client + .get_dealings_statuses(epoch_id, dealer.to_string()) + .await?; + + let submitted = dealing_statuses.full_dealings(); + + // no point in making any queries if the dealer hasn't submitted ALL expected dealings + if !dealing_statuses.all_dealings_fully_submitted { + // the dealer only submitted some subset of dealings + if submitted > 0 { + return Ok(Err( + DealerRejectionReason::InsufficientNumberOfDealingsProvided { + got: submitted, + expected: dealing_statuses.dealing_submission_status.len(), + }, + )); + } + + // we might be in resharing mode and this dealer was not in "initial" set. + // in that case we don't expect any dealings + if resharing && !initial_dealers.contains(&dealer) { + return Ok(Ok(HashMap::new())); + } + return Ok(Err(DealerRejectionReason::NoDealingsProvided)); + } + + // TODO: introduce caching here in case we crash or chain times out because those queries are EXPENSIVE + + // rebuild the dealings + let mut raw_dealings = HashMap::new(); + for (dealing_index, info) in dealing_statuses.dealing_submission_status { + let mut raw_dealing = Vec::new(); + + // note: we're iterating over a BTreeMap and so all chunk indices are guaranteed to be ordered + for chunk_index in info.chunk_submission_status.into_keys() { + let mut chunk_data = self + .dkg_client + .get_dealing_chunk(epoch_id, dealer.as_str(), dealing_index, chunk_index) + .await?; + raw_dealing.append(&mut chunk_data); + } + + raw_dealings.insert(dealing_index, raw_dealing); + } + + Ok(Ok(raw_dealings)) + } + /// Attempt to retrieve valid dealings submitted this epoch. /// /// For each dealer that submitted a valid public key, query its dealings. @@ -194,8 +248,6 @@ impl DkgController { epoch_id: EpochId, resharing: bool, ) -> Result>, KeyDerivationError> { - let expected_key_size = self.dkg_client.get_contract_state().await?.key_size; - let mut valid_dealings: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::new(); // given at MOST we'll have like 50 entries here, iterating over entire vector for lookup is fine @@ -211,32 +263,19 @@ impl DkgController { // note: if we're in resharing mode, the contract itself will forbid submission of dealings from // parties that were not "initial" dealers, so we don't have to worry about it - // TODO: introduce caching here in case we crash because those queries are EXPENSIVE - let raw_dealings = self - .dkg_client - .get_dealings(epoch_id, dealer.to_string()) - .await?; - - if raw_dealings.is_empty() { - // we might be in resharing mode and this dealer was not in "initial" set. - // in that case we don't expect any dealings - if resharing && !initial_dealers.contains(&dealer) { + let raw_dealings = match self + .get_raw_dealings(epoch_id, &dealer, resharing, &initial_dealers) + .await? + { + Ok(dealings) => dealings, + Err(rejection) => { + self.blacklist_dealer(epoch_id, dealer, rejection)?; continue; } - self.blacklist_dealer(epoch_id, dealer, DealerRejectionReason::NoDealingsProvided)?; - continue; - } + }; - // no point in verifying any dealings if we don't have all of them - if raw_dealings.len() != expected_key_size as usize { - self.blacklist_dealer( - epoch_id, - dealer, - DealerRejectionReason::InsufficientNumberOfDealingsProvided { - got: raw_dealings.len(), - expected: expected_key_size as usize, - }, - )?; + // nothing to do + if raw_dealings.is_empty() { continue; } @@ -449,9 +488,7 @@ impl DkgController { // submitted proposals and find the one with our address self.get_validation_proposals() .await? - .get(&Addr::unchecked( - self.dkg_client.get_address().await.as_ref(), - )) + .get(self.dkg_client.get_address().await.as_ref()) .copied() .ok_or(KeyDerivationError::UnrecoverableProposalId) } diff --git a/nym-api/src/coconut/dkg/key_validation.rs b/nym-api/src/coconut/dkg/key_validation.rs index 6b79b144b1..a957db8f23 100644 --- a/nym-api/src/coconut/dkg/key_validation.rs +++ b/nym-api/src/coconut/dkg/key_validation.rs @@ -147,7 +147,7 @@ impl DkgController { debug!("verifying vk share from {owner}"); // there's no point in checking anything if there doesn't exist an associated multisig proposal - let Some(proposal_id) = proposals.get(&owner) else { + let Some(proposal_id) = proposals.get(owner.as_ref()) else { warn!("there does not seem to exist proposal for share validation from {owner}"); continue; }; diff --git a/nym-api/src/coconut/error.rs b/nym-api/src/coconut/error.rs index 40359a4eb3..423574db8d 100644 --- a/nym-api/src/coconut/error.rs +++ b/nym-api/src/coconut/error.rs @@ -1,8 +1,8 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::NymApiStorageError; -use nym_coconut_dkg_common::types::EpochId; +use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId}; use nym_crypto::asymmetric::{ encryption::KeyRecoveryError, identity::{Ed25519RecoveryError, SignatureError}, @@ -141,9 +141,12 @@ pub enum CoconutError { #[error("the proposal id value for epoch {epoch_id} is not available")] UnavailableProposalId { epoch_id: EpochId }, - #[error("insufficient number of dealings provided to derive the key")] - InsufficientDealings { - // TODO: details + #[error("could not find dealing chunk {chunk_index} for dealing {dealing_index} from dealer {dealer} for epoch {epoch_id} on the chain!")] + MissingDealingChunk { + epoch_id: EpochId, + dealer: String, + dealing_index: DealingIndex, + chunk_index: ChunkIndex, }, } diff --git a/nym-api/src/coconut/tests/mod.rs b/nym-api/src/coconut/tests/mod.rs index 5ea0e90962..772552af65 100644 --- a/nym-api/src/coconut/tests/mod.rs +++ b/nym-api/src/coconut/tests/mod.rs @@ -653,7 +653,7 @@ impl super::client::Client for DummyClient { epoch_id, dealer: Addr::unchecked(dealer), dealing_index, - dealing_submitted: dealings.get(dealing_index as usize).is_some(), + full_dealing_submitted: dealings.get(dealing_index as usize).is_some(), }) } diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index e73edd8cf7..e4a713315e 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -9,10 +9,13 @@ use async_trait::async_trait; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; -use nym_coconut_dkg_common::dealer::DealingStatusResponse; +use nym_coconut_dkg_common::dealing::{ + DealerDealingsStatusResponse, DealingChunkInfo, DealingMetadata, DealingStatusResponse, + PartialContractDealing, +}; use nym_coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; use nym_coconut_dkg_common::types::{ - DealingIndex, InitialReplacementData, PartialContractDealing, State, + ChunkIndex, DealingIndex, InitialReplacementData, PartialContractDealingData, State, }; use nym_coconut_dkg_common::{ dealer::{DealerDetails, DealerDetailsResponse}, @@ -424,6 +427,17 @@ impl crate::coconut::client::Client for Client { Ok(nyxd_query!(self, get_dealer_details(self_address).await?)) } + async fn get_dealer_dealings_status( + &self, + epoch_id: EpochId, + dealer: String, + ) -> crate::coconut::error::Result { + Ok(nyxd_query!( + self, + get_dealer_dealings_status(epoch_id, dealer).await? + )) + } + async fn get_dealing_status( &self, epoch_id: EpochId, @@ -440,14 +454,32 @@ impl crate::coconut::client::Client for Client { Ok(nyxd_query!(self, get_all_current_dealers().await?)) } - async fn get_dealings( + async fn get_dealing_metadata( + &self, + epoch_id: EpochId, + dealer: String, + dealing_index: DealingIndex, + ) -> crate::coconut::error::Result> { + Ok(nyxd_query!( + self, + get_dealings_metadata(epoch_id, dealer, dealing_index) + .await? + .metadata + )) + } + + async fn get_dealing_chunk( &self, epoch_id: EpochId, dealer: &str, - ) -> crate::coconut::error::Result> { + dealing_index: DealingIndex, + chunk_index: ChunkIndex, + ) -> crate::coconut::error::Result> { Ok(nyxd_query!( self, - get_all_dealer_dealings(epoch_id, dealer).await? + get_dealing_chunk(epoch_id, dealer.to_string(), dealing_index, chunk_index) + .await? + .chunk )) } @@ -502,14 +534,26 @@ impl crate::coconut::client::Client for Client { )) } - async fn submit_dealing( + async fn submit_dealing_metadata( &self, - dealing: PartialContractDealing, + dealing_index: DealingIndex, + chunks: Vec, + resharing: bool, + ) -> crate::coconut::error::Result { + Ok(nyxd_signing!( + self, + submit_dealing_metadata(dealing_index, chunks, resharing, None).await? + )) + } + + async fn submit_dealing_chunk( + &self, + chunk: PartialContractDealing, resharing: bool, ) -> Result { Ok(nyxd_signing!( self, - submit_dealing_bytes(dealing, resharing, None).await? + submit_dealing_chunk(chunk, resharing, None).await? )) }