handle chunking on nym-api side

This commit is contained in:
Jędrzej Stuczyński
2024-02-02 11:53:53 +00:00
parent 044d94ad02
commit 961775417f
18 changed files with 521 additions and 144 deletions
@@ -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<DealerDealingsStatusResponse, NyxdError> {
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<DealingStatusResponse, NyxdError> {
) -> Result<DealingChunkStatusResponse, NyxdError> {
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,
@@ -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<u8>,
chunk_size: usize,
) -> HashMap<ChunkIndex, PartialContractDealing> {
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<u64>,
}
impl ChunkSubmissionStatus {
pub fn submitted(&self) -> bool {
self.submission_height.is_some()
}
}
impl From<DealingChunkInfo> 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<ChunkIndex, ChunkSubmissionStatus> {
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<u64>,
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<ChunkIndex, ChunkSubmissionStatus>,
}
impl From<Option<DealingMetadata>> for DealingStatus {
fn from(metadata: Option<DealingMetadata>) -> 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<DealingIndex, DealingStatus>,
}
impl DealerDealingsStatusResponse {
pub fn full_dealings(&self) -> usize {
self.dealing_submission_status
.values()
.filter(|s| s.fully_submitted)
.count()
}
}
#[cfg(test)]
@@ -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,
@@ -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<Vec<u8>> for ContractSafeBytes {
fn from(value: Vec<u8>) -> Self {
ContractSafeBytes(value)
+5 -1
View File
@@ -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<QueryResponse,
dealer,
dealing_index,
)?)?,
QueryMsg::GetDealerDealingsStatus { epoch_id, dealer } => {
to_binary(&query_dealer_dealings_status(deps, epoch_id, dealer)?)?
}
QueryMsg::GetDealingStatus {
epoch_id,
dealer,
+39 -19
View File
@@ -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<DealerDealingsStatusResponse> {
let dealer = deps.api.addr_validate(&dealer)?;
let state = STATE.load(deps.storage)?;
let mut dealing_submission_status: BTreeMap<DealingIndex, DealingStatus> = 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,
})
}
@@ -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)
}
@@ -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,
+11 -1
View File
@@ -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,
+54 -8
View File
@@ -1,14 +1,18 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// 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<AccountId>;
async fn get_tx(&self, tx_hash: Hash) -> Result<TxResponse>;
async fn get_proposal(&self, proposal_id: u64) -> Result<ProposalResponse>;
async fn list_proposals(&self) -> Result<Vec<ProposalResponse>>;
async fn get_vote(&self, proposal_id: u64, voter: String) -> Result<VoteResponse>;
async fn get_spent_credential(
&self,
blinded_serial_number: String,
) -> Result<SpendCredentialResponse>;
async fn contract_state(&self) -> Result<State>;
async fn get_current_epoch(&self) -> Result<Epoch>;
async fn group_member(&self, addr: String) -> Result<MemberResponse>;
async fn get_current_epoch_threshold(&self) -> Result<Option<Threshold>>;
async fn get_initial_dealers(&self) -> Result<Option<InitialReplacementData>>;
async fn get_self_registered_dealer_details(&self) -> Result<DealerDetailsResponse>;
async fn get_dealer_dealings_status(
&self,
epoch_id: EpochId,
dealer: String,
) -> Result<DealerDealingsStatusResponse>;
async fn get_dealing_status(
&self,
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
) -> Result<DealingStatusResponse>;
async fn get_current_dealers(&self) -> Result<Vec<DealerDetails>>;
async fn get_dealings(
async fn get_dealing_metadata(
&self,
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
) -> Result<Option<DealingMetadata>>;
async fn get_dealing_chunk(
&self,
epoch_id: EpochId,
dealer: &str,
) -> Result<Vec<PartialContractDealing>>;
dealing_index: DealingIndex,
chunk_index: ChunkIndex,
) -> Result<Option<PartialContractDealingData>>;
async fn get_verification_key_share(
&self,
epoch_id: EpochId,
dealer: String,
) -> Result<Option<ContractVKShare>>;
async fn get_verification_key_shares(&self, epoch_id: EpochId) -> Result<Vec<ContractVKShare>>;
async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option<Fee>)
-> 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<ExecuteResult>;
async fn submit_dealing(
async fn submit_dealing_metadata(
&self,
dealing: PartialContractDealing,
dealing_index: DealingIndex,
chunks: Vec<DealingChunkInfo>,
resharing: bool,
) -> Result<ExecuteResult>;
async fn submit_dealing_chunk(
&self,
chunk: PartialContractDealing,
resharing: bool,
) -> Result<ExecuteResult>;
async fn submit_verification_key_share(
&self,
share: VerificationKeyShare,
+44 -22
View File
@@ -1,4 +1,4 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// 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<bool, CoconutError> {
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<Vec<PartialContractDealing>, CoconutError> {
self.inner.get_dealings(epoch_id, &dealer).await
) -> Result<DealerDealingsStatusResponse, CoconutError> {
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<PartialContractDealingData, CoconutError> {
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<S: Into<String>>(
@@ -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<DealingChunkInfo>,
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(())
}
+95 -24
View File
@@ -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<R: RngCore + CryptoRng> DkgController<R> {
const DEALING_CHUNK_SIZE: usize = MAX_DEALING_CHUNK_SIZE;
async fn generate_dealings(
&mut self,
epoch_id: EpochId,
@@ -153,28 +154,100 @@ impl<R: RngCore + CryptoRng> DkgController<R> {
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<R: RngCore + CryptoRng> DkgController<R> {
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
)
}
+2 -3
View File
@@ -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<R: RngCore + CryptoRng> DkgController<R> {
&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<R: RngCore + CryptoRng> DkgController<R> {
pub(crate) async fn get_validation_proposals(
&self,
) -> Result<HashMap<Addr, u64>, CoconutError> {
) -> Result<HashMap<String, u64>, 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,
+72 -35
View File
@@ -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<R: RngCore + CryptoRng> DkgController<R> {
epoch_id: EpochId,
dealer: &Addr,
epoch_receivers: &BTreeMap<NodeIndex, bte::PublicKey>,
raw_dealings: Vec<PartialContractDealing>,
raw_dealings: HashMap<DealingIndex, Vec<u8>>,
prior_public_key: Option<VerificationKey>,
) -> Result<Result<Vec<(DealingIndex, Dealing)>, DealerRejectionReason>, KeyDerivationError>
{
@@ -105,11 +104,9 @@ impl<R: RngCore + CryptoRng> DkgController<R> {
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<R: RngCore + CryptoRng> DkgController<R> {
))
}
async fn get_raw_dealings(
&self,
epoch_id: EpochId,
dealer: &Addr,
resharing: bool,
initial_dealers: &[Addr],
) -> Result<Result<HashMap<DealingIndex, Vec<u8>>, 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<R: RngCore + CryptoRng> DkgController<R> {
epoch_id: EpochId,
resharing: bool,
) -> Result<BTreeMap<DealingIndex, BTreeMap<NodeIndex, Dealing>>, 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<R: RngCore + CryptoRng> DkgController<R> {
// 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<R: RngCore + CryptoRng> DkgController<R> {
// 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)
}
+1 -1
View File
@@ -147,7 +147,7 @@ impl<R: RngCore + CryptoRng> DkgController<R> {
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;
};
+8 -5
View File
@@ -1,8 +1,8 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// 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,
},
}
+1 -1
View File
@@ -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(),
})
}
+52 -8
View File
@@ -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<DealerDealingsStatusResponse> {
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<Option<DealingMetadata>> {
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<Vec<PartialContractDealing>> {
dealing_index: DealingIndex,
chunk_index: ChunkIndex,
) -> crate::coconut::error::Result<Option<PartialContractDealingData>> {
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<DealingChunkInfo>,
resharing: bool,
) -> crate::coconut::error::Result<ExecuteResult> {
Ok(nyxd_signing!(
self,
submit_dealing_metadata(dealing_index, chunks, resharing, None).await?
))
}
async fn submit_dealing_chunk(
&self,
chunk: PartialContractDealing,
resharing: bool,
) -> Result<ExecuteResult, CoconutError> {
Ok(nyxd_signing!(
self,
submit_dealing_bytes(dealing, resharing, None).await?
submit_dealing_chunk(chunk, resharing, None).await?
))
}