Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f9ac1286b | |||
| 961775417f | |||
| 044d94ad02 | |||
| 35d7525d1e | |||
| b393405db8 | |||
| 6974f3d785 | |||
| 85d96deded | |||
| 4fe7ce0f12 | |||
| 5886361dc5 | |||
| 40c26b1326 | |||
| 46a482cfcb | |||
| 761b6c2cac | |||
| 0afdd7bc82 | |||
| 7b9fe3dc09 | |||
| d734174f6e | |||
| 6187d94b68 | |||
| a7dca2f07c | |||
| 2ed4449e0b | |||
| 605176551b | |||
| ac3830e677 | |||
| c00e4655f4 | |||
| 8fa84a28e6 | |||
| 94d3bf087a | |||
| 7a9b989db9 | |||
| 9f1b89616a | |||
| 89315f0c2a | |||
| 858fafb1a5 | |||
| 169f8f2c1c | |||
| 8782fd7bb8 | |||
| 146c3bd358 | |||
| e68ebdc2b8 | |||
| 397b03267a | |||
| 2a021b46ac | |||
| c43cb657c6 | |||
| 6f66b377e2 | |||
| 5ea67a9376 | |||
| f566dffc5b | |||
| 05f8beedad | |||
| 2fff051e28 | |||
| 44bd70c546 | |||
| 702354d127 | |||
| 56b1010d16 | |||
| 0632517f5d | |||
| bfcc5e9b41 | |||
| 337aacd442 | |||
| da5b7302b5 | |||
| 7a53e86b40 | |||
| 654dd07d19 | |||
| ef0765face | |||
| 3685b4681c | |||
| 47e2af2caa | |||
| 5be555d79f | |||
| 8cc2b3167e | |||
| 4d95955961 | |||
| e36ae4091f | |||
| b566147f2f | |||
| 12242bb3c6 | |||
| 0a0b0e80f4 |
Generated
+5
@@ -5983,6 +5983,8 @@ dependencies = [
|
||||
"pin-project",
|
||||
"rand 0.7.3",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.2.2",
|
||||
"rand_chacha 0.3.1",
|
||||
"reqwest",
|
||||
"rocket",
|
||||
"rocket_cors",
|
||||
@@ -5991,6 +5993,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"sha2 0.9.9",
|
||||
"sqlx",
|
||||
"tap",
|
||||
"tempfile",
|
||||
@@ -6309,6 +6312,8 @@ dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"cw4",
|
||||
"nym-contracts-common",
|
||||
"nym-multisig-contract-common",
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::collect_paged;
|
||||
@@ -7,14 +7,22 @@ use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
use nym_coconut_dkg_common::{
|
||||
dealer::{ContractDealing, DealerDetailsResponse, PagedDealerResponse, PagedDealingsResponse},
|
||||
msg::QueryMsg as DkgQueryMsg,
|
||||
types::{DealerDetails, Epoch, EpochId, InitialReplacementData},
|
||||
verification_key::{ContractVKShare, PagedVKSharesResponse},
|
||||
};
|
||||
use nym_coconut_dkg_common::types::ChunkIndex;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub use nym_coconut_dkg_common::{
|
||||
dealer::{DealerDetailsResponse, PagedDealerResponse},
|
||||
dealing::{
|
||||
DealerDealingsStatusResponse, DealingChunkResponse, DealingChunkStatusResponse,
|
||||
DealingMetadataResponse, DealingStatusResponse,
|
||||
},
|
||||
msg::QueryMsg as DkgQueryMsg,
|
||||
types::{
|
||||
DealerDetails, DealingIndex, Epoch, EpochId, EpochState, InitialReplacementData, State,
|
||||
},
|
||||
verification_key::{ContractVKShare, PagedVKSharesResponse, VkShareResponse},
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait DkgQueryClient {
|
||||
@@ -22,10 +30,16 @@ pub trait DkgQueryClient {
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_state(&self) -> Result<State, NyxdError> {
|
||||
let request = DkgQueryMsg::GetState {};
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_current_epoch(&self) -> Result<Epoch, NyxdError> {
|
||||
let request = DkgQueryMsg::GetCurrentEpochState {};
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_current_epoch_threshold(&self) -> Result<Option<u64>, NyxdError> {
|
||||
let request = DkgQueryMsg::GetCurrentEpochThreshold {};
|
||||
self.query_dkg_contract(request).await
|
||||
@@ -64,17 +78,86 @@ pub trait DkgQueryClient {
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_dealings_paged(
|
||||
async fn get_dealings_metadata(
|
||||
&self,
|
||||
idx: u64,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PagedDealingsResponse, NyxdError> {
|
||||
let request = DkgQueryMsg::GetDealing {
|
||||
idx,
|
||||
limit,
|
||||
start_after,
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
) -> Result<DealingMetadataResponse, NyxdError> {
|
||||
let request = DkgQueryMsg::GetDealingsMetadata {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
};
|
||||
|
||||
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,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
) -> Result<DealingStatusResponse, NyxdError> {
|
||||
let request = DkgQueryMsg::GetDealingStatus {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
};
|
||||
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_dealing_chunk_status(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
chunk_index: ChunkIndex,
|
||||
) -> Result<DealingChunkStatusResponse, NyxdError> {
|
||||
let request = DkgQueryMsg::GetDealingChunkStatus {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
chunk_index,
|
||||
};
|
||||
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_dealing_chunk(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
chunk_index: ChunkIndex,
|
||||
) -> Result<DealingChunkResponse, NyxdError> {
|
||||
let request = DkgQueryMsg::GetDealingChunk {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
chunk_index,
|
||||
};
|
||||
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_vk_share(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
owner: String,
|
||||
) -> Result<VkShareResponse, NyxdError> {
|
||||
let request = DkgQueryMsg::GetVerificationKey { epoch_id, owner };
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
@@ -91,6 +174,11 @@ pub trait DkgQueryClient {
|
||||
};
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_contract_cw2_version(&self) -> Result<cw2::ContractVersion, NyxdError> {
|
||||
self.query_dkg_contract(DkgQueryMsg::GetCW2ContractVersion {})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
// extension trait to the query client to deal with the paged queries
|
||||
@@ -106,10 +194,6 @@ pub trait PagedDkgQueryClient: DkgQueryClient {
|
||||
collect_paged!(self, get_past_dealers_paged, dealers)
|
||||
}
|
||||
|
||||
async fn get_all_epoch_dealings(&self, idx: u64) -> Result<Vec<ContractDealing>, NyxdError> {
|
||||
collect_paged!(self, get_dealings_paged, dealings, idx)
|
||||
}
|
||||
|
||||
async fn get_all_verification_key_shares(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
@@ -143,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)]
|
||||
@@ -151,6 +236,7 @@ mod tests {
|
||||
msg: DkgQueryMsg,
|
||||
) {
|
||||
match msg {
|
||||
DkgQueryMsg::GetState {} => client.get_state().ignore(),
|
||||
DkgQueryMsg::GetCurrentEpochState {} => client.get_current_epoch().ignore(),
|
||||
DkgQueryMsg::GetCurrentEpochThreshold {} => {
|
||||
client.get_current_epoch_threshold().ignore()
|
||||
@@ -165,11 +251,42 @@ mod tests {
|
||||
DkgQueryMsg::GetPastDealers { limit, start_after } => {
|
||||
client.get_past_dealers_paged(start_after, limit).ignore()
|
||||
}
|
||||
DkgQueryMsg::GetDealing {
|
||||
idx,
|
||||
limit,
|
||||
start_after,
|
||||
} => client.get_dealings_paged(idx, start_after, limit).ignore(),
|
||||
DkgQueryMsg::GetDealingStatus {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
} => client
|
||||
.get_dealing_status(epoch_id, dealer, dealing_index)
|
||||
.ignore(),
|
||||
DkgQueryMsg::GetDealingsMetadata {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
} => 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,
|
||||
dealing_index,
|
||||
chunk_index,
|
||||
} => client
|
||||
.get_dealing_chunk_status(epoch_id, dealer, dealing_index, chunk_index)
|
||||
.ignore(),
|
||||
DkgQueryMsg::GetDealingChunk {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
chunk_index,
|
||||
} => client
|
||||
.get_dealing_chunk(epoch_id, dealer, dealing_index, chunk_index)
|
||||
.ignore(),
|
||||
DkgQueryMsg::GetVerificationKey { epoch_id, owner } => {
|
||||
client.get_vk_share(epoch_id, owner).ignore()
|
||||
}
|
||||
DkgQueryMsg::GetVerificationKeys {
|
||||
epoch_id,
|
||||
limit,
|
||||
@@ -177,6 +294,7 @@ mod tests {
|
||||
} => client
|
||||
.get_vk_shares_paged(epoch_id, start_after, limit)
|
||||
.ignore(),
|
||||
DkgQueryMsg::GetCW2ContractVersion {} => client.get_contract_cw2_version().ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+52
-20
@@ -8,11 +8,11 @@ use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
|
||||
use crate::signing::signer::OfflineSigner;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut_dkg_common::dealing::{DealingChunkInfo, PartialContractDealing};
|
||||
use nym_coconut_dkg_common::msg::ExecuteMsg as DkgExecuteMsg;
|
||||
use nym_coconut_dkg_common::types::EncodedBTEPublicKeyWithProof;
|
||||
use nym_coconut_dkg_common::types::{DealingIndex, EncodedBTEPublicKeyWithProof};
|
||||
use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
|
||||
use nym_contracts_common::dealings::ContractSafeBytes;
|
||||
use nym_contracts_common::IdentityKey;
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
@@ -25,6 +25,13 @@ pub trait DkgSigningClient {
|
||||
funds: Vec<Coin>,
|
||||
) -> Result<ExecuteResult, NyxdError>;
|
||||
|
||||
async fn initiate_dkg(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError> {
|
||||
let req = DkgExecuteMsg::InitiateDkg {};
|
||||
|
||||
self.execute_dkg_contract(fee, req, "initiating the DKG".to_string(), vec![])
|
||||
.await
|
||||
}
|
||||
|
||||
async fn advance_dkg_epoch_state(&self, fee: Option<Fee>) -> Result<ExecuteResult, NyxdError> {
|
||||
let req = DkgExecuteMsg::AdvanceEpochState {};
|
||||
|
||||
@@ -42,12 +49,14 @@ pub trait DkgSigningClient {
|
||||
async fn register_dealer(
|
||||
&self,
|
||||
bte_key: EncodedBTEPublicKeyWithProof,
|
||||
identity_key: IdentityKey,
|
||||
announce_address: String,
|
||||
resharing: bool,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let req = DkgExecuteMsg::RegisterDealer {
|
||||
bte_key_with_proof: bte_key,
|
||||
identity_key,
|
||||
announce_address,
|
||||
resharing,
|
||||
};
|
||||
@@ -56,18 +65,32 @@ pub trait DkgSigningClient {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn submit_dealing_bytes(
|
||||
async fn submit_dealing_metadata(
|
||||
&self,
|
||||
dealing_bytes: ContractSafeBytes,
|
||||
dealing_index: DealingIndex,
|
||||
chunks: Vec<DealingChunkInfo>,
|
||||
resharing: bool,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let req = DkgExecuteMsg::CommitDealing {
|
||||
dealing_bytes,
|
||||
let req = DkgExecuteMsg::CommitDealingsMetadata {
|
||||
dealing_index,
|
||||
chunks,
|
||||
resharing,
|
||||
};
|
||||
|
||||
self.execute_dkg_contract(fee, req, "dealing commitment".to_string(), vec![])
|
||||
self.execute_dkg_contract(fee, req, "dealing metadata commitment".to_string(), vec![])
|
||||
.await
|
||||
}
|
||||
|
||||
async fn submit_dealing_chunk(
|
||||
&self,
|
||||
chunk: PartialContractDealing,
|
||||
resharing: bool,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let req = DkgExecuteMsg::CommitDealingsChunk { chunk, resharing };
|
||||
|
||||
self.execute_dkg_contract(fee, req, "dealing chunk commitment".to_string(), vec![])
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -94,9 +117,10 @@ pub trait DkgSigningClient {
|
||||
resharing: bool,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
// the call to unchecked is fine as we're converting from pre-validated `AccountId`
|
||||
let owner = Addr::unchecked(owner.to_string());
|
||||
let req = DkgExecuteMsg::VerifyVerificationKeyShare { owner, resharing };
|
||||
let req = DkgExecuteMsg::VerifyVerificationKeyShare {
|
||||
owner: owner.to_string(),
|
||||
resharing,
|
||||
};
|
||||
|
||||
self.execute_dkg_contract(
|
||||
fee,
|
||||
@@ -146,28 +170,36 @@ mod tests {
|
||||
msg: DkgExecuteMsg,
|
||||
) {
|
||||
match msg {
|
||||
DkgExecuteMsg::InitiateDkg {} => client.initiate_dkg(None).ignore(),
|
||||
DkgExecuteMsg::RegisterDealer {
|
||||
bte_key_with_proof,
|
||||
identity_key,
|
||||
announce_address,
|
||||
resharing,
|
||||
} => client
|
||||
.register_dealer(bte_key_with_proof, announce_address, resharing, None)
|
||||
.register_dealer(
|
||||
bte_key_with_proof,
|
||||
identity_key,
|
||||
announce_address,
|
||||
resharing,
|
||||
None,
|
||||
)
|
||||
.ignore(),
|
||||
DkgExecuteMsg::CommitDealing {
|
||||
dealing_bytes,
|
||||
DkgExecuteMsg::CommitDealingsMetadata {
|
||||
dealing_index,
|
||||
chunks,
|
||||
resharing,
|
||||
} => client
|
||||
.submit_dealing_bytes(dealing_bytes, resharing, None)
|
||||
.submit_dealing_metadata(dealing_index, chunks, resharing, None)
|
||||
.ignore(),
|
||||
DkgExecuteMsg::CommitDealingsChunk { chunk, resharing } => {
|
||||
client.submit_dealing_chunk(chunk, resharing, None).ignore()
|
||||
}
|
||||
DkgExecuteMsg::CommitVerificationKeyShare { share, resharing } => client
|
||||
.submit_verification_key_share(share, resharing, None)
|
||||
.ignore(),
|
||||
DkgExecuteMsg::VerifyVerificationKeyShare { owner, resharing } => client
|
||||
.verify_verification_key_share(
|
||||
&owner.into_string().parse().unwrap(),
|
||||
resharing,
|
||||
None,
|
||||
)
|
||||
.verify_verification_key_share(&owner.parse().unwrap(), resharing, None)
|
||||
.ignore(),
|
||||
DkgExecuteMsg::SurpassedThreshold {} => client.surpass_threshold(None).ignore(),
|
||||
DkgExecuteMsg::AdvanceEpochState {} => client.advance_dkg_epoch_state(None).ignore(),
|
||||
|
||||
@@ -8,26 +8,26 @@ use std::str::FromStr;
|
||||
// TODO: all of those could/should be derived via a macro
|
||||
|
||||
// query clients
|
||||
mod coconut_bandwidth_query_client;
|
||||
mod dkg_query_client;
|
||||
mod ephemera_query_client;
|
||||
mod group_query_client;
|
||||
mod mixnet_query_client;
|
||||
mod multisig_query_client;
|
||||
mod name_service_query_client;
|
||||
mod sp_directory_query_client;
|
||||
mod vesting_query_client;
|
||||
pub mod coconut_bandwidth_query_client;
|
||||
pub mod dkg_query_client;
|
||||
pub mod ephemera_query_client;
|
||||
pub mod group_query_client;
|
||||
pub mod mixnet_query_client;
|
||||
pub mod multisig_query_client;
|
||||
pub mod name_service_query_client;
|
||||
pub mod sp_directory_query_client;
|
||||
pub mod vesting_query_client;
|
||||
|
||||
// signing clients
|
||||
mod coconut_bandwidth_signing_client;
|
||||
mod dkg_signing_client;
|
||||
mod ephemera_signing_client;
|
||||
mod group_signing_client;
|
||||
mod mixnet_signing_client;
|
||||
mod multisig_signing_client;
|
||||
mod name_service_signing_client;
|
||||
mod sp_directory_signing_client;
|
||||
mod vesting_signing_client;
|
||||
pub mod coconut_bandwidth_signing_client;
|
||||
pub mod dkg_signing_client;
|
||||
pub mod ephemera_signing_client;
|
||||
pub mod group_signing_client;
|
||||
pub mod mixnet_signing_client;
|
||||
pub mod multisig_signing_client;
|
||||
pub mod name_service_signing_client;
|
||||
pub mod sp_directory_signing_client;
|
||||
pub mod vesting_signing_client;
|
||||
|
||||
// re-export query traits
|
||||
pub use coconut_bandwidth_query_client::{
|
||||
|
||||
@@ -14,7 +14,7 @@ pub use nym_coconut::{
|
||||
aggregate_signature_shares, aggregate_verification_keys, blind_sign, hash_to_scalar,
|
||||
prepare_blind_sign, prove_bandwidth_credential, Attribute, Base58, BlindSignRequest,
|
||||
BlindedSignature, Bytable, CoconutError, KeyPair, Parameters, PrivateAttribute,
|
||||
PublicAttribute, Signature, SignatureShare, Theta, VerificationKey,
|
||||
PublicAttribute, SecretKey, Signature, SignatureShare, Theta, VerificationKey,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Getters, CopyGetters, Clone, PartialEq, Eq)]
|
||||
|
||||
@@ -6,7 +6,7 @@ use log::{debug, info};
|
||||
use std::str::FromStr;
|
||||
|
||||
use nym_coconut_dkg_common::msg::InstantiateMsg;
|
||||
use nym_coconut_dkg_common::types::TimeConfiguration;
|
||||
use nym_coconut_dkg_common::types::{TimeConfiguration, DEFAULT_DEALINGS};
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -93,6 +93,7 @@ pub async fn generate(args: Args) {
|
||||
multisig_addr: multisig_addr.to_string(),
|
||||
time_configuration: Some(time_configuration),
|
||||
mix_denom,
|
||||
key_size: DEFAULT_DEALINGS as u32,
|
||||
};
|
||||
|
||||
debug!("instantiate_msg: {:?}", instantiate_msg);
|
||||
|
||||
@@ -10,6 +10,8 @@ license.workspace = true
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
cw-utils = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw4 = { workspace = true }
|
||||
|
||||
contracts-common = { path = "../contracts-common", package = "nym-contracts-common" }
|
||||
nym-multisig-contract-common = { path = "../multisig-contract" }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof, NodeIndex};
|
||||
use crate::types::{EncodedBTEPublicKeyWithProof, NodeIndex};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::Addr;
|
||||
|
||||
@@ -9,6 +9,7 @@ use cosmwasm_std::Addr;
|
||||
pub struct DealerDetails {
|
||||
pub address: Addr,
|
||||
pub bte_public_key_with_proof: EncodedBTEPublicKeyWithProof,
|
||||
pub ed25519_identity: String,
|
||||
pub announce_address: String,
|
||||
pub assigned_index: NodeIndex,
|
||||
}
|
||||
@@ -64,38 +65,3 @@ impl PagedDealerResponse {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct ContractDealing {
|
||||
pub dealing: ContractSafeBytes,
|
||||
pub dealer: Addr,
|
||||
}
|
||||
|
||||
impl ContractDealing {
|
||||
pub fn new(dealing: ContractSafeBytes, dealer: Addr) -> Self {
|
||||
ContractDealing { dealing, dealer }
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct PagedDealingsResponse {
|
||||
pub dealings: Vec<ContractDealing>,
|
||||
pub per_page: usize,
|
||||
|
||||
/// Field indicating paging information for the following queries if the caller wishes to get further entries.
|
||||
pub start_next_after: Option<Addr>,
|
||||
}
|
||||
|
||||
impl PagedDealingsResponse {
|
||||
pub fn new(
|
||||
dealings: Vec<ContractDealing>,
|
||||
per_page: usize,
|
||||
start_next_after: Option<Addr>,
|
||||
) -> Self {
|
||||
PagedDealingsResponse {
|
||||
dealings,
|
||||
per_page,
|
||||
start_next_after,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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, HashMap};
|
||||
|
||||
/// 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;
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
impl DealingChunkInfo {
|
||||
pub fn new(size: usize) -> Self {
|
||||
DealingChunkInfo { size }
|
||||
}
|
||||
|
||||
pub fn construct(dealing_len: usize, chunk_size: usize) -> Vec<Self> {
|
||||
let (full_chunks, overflow) = (dealing_len / chunk_size, dealing_len % chunk_size);
|
||||
|
||||
let mut chunks = Vec::new();
|
||||
for _ in 0..full_chunks {
|
||||
chunks.push(DealingChunkInfo::new(chunk_size));
|
||||
}
|
||||
|
||||
if overflow != 0 {
|
||||
chunks.push(DealingChunkInfo::new(overflow));
|
||||
}
|
||||
|
||||
chunks
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
||||
impl SubmittedChunk {
|
||||
pub fn new(info: DealingChunkInfo) -> Self {
|
||||
SubmittedChunk {
|
||||
info,
|
||||
status: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn submitted(&self) -> bool {
|
||||
self.status.submitted()
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct DealingMetadata {
|
||||
pub dealing_index: DealingIndex,
|
||||
|
||||
pub submitted_chunks: BTreeMap<ChunkIndex, SubmittedChunk>,
|
||||
}
|
||||
|
||||
impl DealingMetadata {
|
||||
pub fn new(dealing_index: DealingIndex, chunks: Vec<DealingChunkInfo>) -> Self {
|
||||
DealingMetadata {
|
||||
dealing_index,
|
||||
submitted_chunks: chunks
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(id, chunk)| (id as ChunkIndex, chunk.into()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.submitted_chunks.values().all(|c| c.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]
|
||||
pub struct PartialContractDealing {
|
||||
pub dealing_index: DealingIndex,
|
||||
pub chunk_index: ChunkIndex,
|
||||
pub data: PartialContractDealingData,
|
||||
}
|
||||
|
||||
impl PartialContractDealing {
|
||||
pub fn new(
|
||||
dealing_index: DealingIndex,
|
||||
chunk_index: ChunkIndex,
|
||||
data: PartialContractDealingData,
|
||||
) -> Self {
|
||||
PartialContractDealing {
|
||||
dealing_index,
|
||||
chunk_index,
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct DealingMetadataResponse {
|
||||
pub epoch_id: EpochId,
|
||||
|
||||
pub dealer: Addr,
|
||||
|
||||
pub dealing_index: DealingIndex,
|
||||
|
||||
pub metadata: Option<DealingMetadata>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct DealingChunkResponse {
|
||||
pub epoch_id: EpochId,
|
||||
|
||||
pub dealer: Addr,
|
||||
|
||||
pub dealing_index: DealingIndex,
|
||||
|
||||
pub chunk_index: ChunkIndex,
|
||||
|
||||
pub chunk: Option<PartialContractDealingData>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct DealingChunkStatusResponse {
|
||||
pub epoch_id: EpochId,
|
||||
|
||||
pub dealer: Addr,
|
||||
|
||||
pub dealing_index: DealingIndex,
|
||||
|
||||
pub chunk_index: ChunkIndex,
|
||||
|
||||
pub status: ChunkSubmissionStatus,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct DealingStatusResponse {
|
||||
pub epoch_id: EpochId,
|
||||
|
||||
pub dealer: Addr,
|
||||
|
||||
pub dealing_index: DealingIndex,
|
||||
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn chunking_dealings() {
|
||||
const CHUNK_SIZE: usize = 512;
|
||||
|
||||
let test_cases = [
|
||||
(CHUNK_SIZE - 10, CHUNK_SIZE, 1),
|
||||
(CHUNK_SIZE, CHUNK_SIZE, 1),
|
||||
(CHUNK_SIZE + 10, CHUNK_SIZE, 2),
|
||||
(CHUNK_SIZE * 2, CHUNK_SIZE, 2),
|
||||
(CHUNK_SIZE * 2 + 1, CHUNK_SIZE, 3),
|
||||
(CHUNK_SIZE * 10 + 42, CHUNK_SIZE, 11),
|
||||
];
|
||||
|
||||
for (dealing_len, chunk_size, expected_chunks) in test_cases {
|
||||
let chunks = DealingChunkInfo::construct(dealing_len, chunk_size);
|
||||
assert_eq!(expected_chunks, chunks.len());
|
||||
assert_eq!(dealing_len, chunks.iter().map(|c| c.size).sum::<usize>());
|
||||
|
||||
let mut expected_last = dealing_len % chunk_size;
|
||||
if expected_last == 0 {
|
||||
expected_last = chunk_size;
|
||||
}
|
||||
assert_eq!(chunks.last().unwrap().size, expected_last);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod dealer;
|
||||
pub mod dealing;
|
||||
pub mod event_attributes;
|
||||
pub mod msg;
|
||||
pub mod types;
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof, EpochId, TimeConfiguration};
|
||||
use crate::dealing::{DealingChunkInfo, PartialContractDealing};
|
||||
use crate::types::{
|
||||
ChunkIndex, DealingIndex, EncodedBTEPublicKeyWithProof, EpochId, TimeConfiguration,
|
||||
};
|
||||
use crate::verification_key::VerificationKeyShare;
|
||||
use contracts_common::IdentityKey;
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::Addr;
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::{
|
||||
dealer::{DealerDetailsResponse, PagedDealerResponse, PagedDealingsResponse},
|
||||
types::{Epoch, InitialReplacementData},
|
||||
verification_key::PagedVKSharesResponse,
|
||||
dealer::{DealerDetailsResponse, PagedDealerResponse},
|
||||
dealing::{
|
||||
DealerDealingsStatusResponse, DealingChunkResponse, DealingChunkStatusResponse,
|
||||
DealingMetadataResponse, DealingStatusResponse,
|
||||
},
|
||||
types::{Epoch, InitialReplacementData, State},
|
||||
verification_key::{PagedVKSharesResponse, VkShareResponse},
|
||||
};
|
||||
#[cfg(feature = "schema")]
|
||||
use cosmwasm_schema::QueryResponses;
|
||||
@@ -21,18 +28,31 @@ pub struct InstantiateMsg {
|
||||
pub multisig_addr: String,
|
||||
pub time_configuration: Option<TimeConfiguration>,
|
||||
pub mix_denom: String,
|
||||
|
||||
/// Specifies the number of elements in the derived keys
|
||||
pub key_size: u32,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub enum ExecuteMsg {
|
||||
// we could have just re-used AdvanceEpochState, but imo an explicit message is better
|
||||
InitiateDkg {},
|
||||
|
||||
RegisterDealer {
|
||||
bte_key_with_proof: EncodedBTEPublicKeyWithProof,
|
||||
identity_key: IdentityKey,
|
||||
announce_address: String,
|
||||
resharing: bool,
|
||||
},
|
||||
|
||||
CommitDealing {
|
||||
dealing_bytes: ContractSafeBytes,
|
||||
CommitDealingsMetadata {
|
||||
dealing_index: DealingIndex,
|
||||
chunks: Vec<DealingChunkInfo>,
|
||||
resharing: bool,
|
||||
},
|
||||
|
||||
CommitDealingsChunk {
|
||||
chunk: PartialContractDealing,
|
||||
resharing: bool,
|
||||
},
|
||||
|
||||
@@ -42,8 +62,7 @@ pub enum ExecuteMsg {
|
||||
},
|
||||
|
||||
VerifyVerificationKeyShare {
|
||||
// TODO: this should be using a String...
|
||||
owner: Addr,
|
||||
owner: String,
|
||||
resharing: bool,
|
||||
},
|
||||
|
||||
@@ -55,6 +74,9 @@ pub enum ExecuteMsg {
|
||||
#[cw_serde]
|
||||
#[cfg_attr(feature = "schema", derive(QueryResponses))]
|
||||
pub enum QueryMsg {
|
||||
#[cfg_attr(feature = "schema", returns(State))]
|
||||
GetState {},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(Epoch))]
|
||||
GetCurrentEpochState {},
|
||||
|
||||
@@ -79,19 +101,53 @@ pub enum QueryMsg {
|
||||
start_after: Option<String>,
|
||||
},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(PagedDealingsResponse))]
|
||||
GetDealing {
|
||||
idx: u64,
|
||||
limit: Option<u32>,
|
||||
start_after: Option<String>,
|
||||
#[cfg_attr(feature = "schema", returns(DealingMetadataResponse))]
|
||||
GetDealingsMetadata {
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
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,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(DealingChunkStatusResponse))]
|
||||
GetDealingChunkStatus {
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
chunk_index: ChunkIndex,
|
||||
},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(DealingChunkResponse))]
|
||||
GetDealingChunk {
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
chunk_index: ChunkIndex,
|
||||
},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(VkShareResponse))]
|
||||
GetVerificationKey { epoch_id: EpochId, owner: String },
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(PagedVKSharesResponse))]
|
||||
GetVerificationKeys {
|
||||
epoch_id: EpochId,
|
||||
limit: Option<u32>,
|
||||
start_after: Option<String>,
|
||||
},
|
||||
|
||||
/// Gets the stored contract version information that's required by the CW2 spec interface for migrations.
|
||||
#[serde(rename = "get_cw2_contract_version")]
|
||||
#[cfg_attr(feature = "schema", returns(cw2::ContractVersion))]
|
||||
GetCW2ContractVersion {},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
|
||||
@@ -8,14 +8,18 @@ use std::str::FromStr;
|
||||
pub use crate::dealer::{DealerDetails, PagedDealerResponse};
|
||||
pub use contracts_common::dealings::ContractSafeBytes;
|
||||
pub use cosmwasm_std::{Addr, Coin, Timestamp};
|
||||
pub use cw4::Cw4Contract;
|
||||
|
||||
pub type EncodedBTEPublicKeyWithProof = String;
|
||||
pub type EncodedBTEPublicKeyWithProofRef<'a> = &'a str;
|
||||
pub type NodeIndex = u64;
|
||||
pub type EpochId = u64;
|
||||
|
||||
// 2 public attributes, 2 private attributes, 1 fixed for coconut credential
|
||||
pub const TOTAL_DEALINGS: usize = 2 + 2 + 1;
|
||||
pub type DealingIndex = u32;
|
||||
// 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;
|
||||
|
||||
#[cw_serde]
|
||||
pub struct InitialReplacementData {
|
||||
@@ -73,13 +77,23 @@ impl Default for TimeConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct State {
|
||||
pub mix_denom: String,
|
||||
pub multisig_addr: Addr,
|
||||
pub group_addr: Cw4Contract,
|
||||
|
||||
/// Specifies the number of elements in the derived keys
|
||||
pub key_size: u32,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[derive(Copy, Default)]
|
||||
pub struct Epoch {
|
||||
pub state: EpochState,
|
||||
pub epoch_id: EpochId,
|
||||
pub time_configuration: TimeConfiguration,
|
||||
pub finish_timestamp: Timestamp,
|
||||
pub finish_timestamp: Option<Timestamp>,
|
||||
}
|
||||
|
||||
impl Epoch {
|
||||
@@ -90,36 +104,40 @@ impl Epoch {
|
||||
current_timestamp: Timestamp,
|
||||
) -> Self {
|
||||
let duration = match state {
|
||||
EpochState::WaitingInitialisation => None,
|
||||
EpochState::PublicKeySubmission { .. } => {
|
||||
time_configuration.public_key_submission_time_secs
|
||||
Some(time_configuration.public_key_submission_time_secs)
|
||||
}
|
||||
EpochState::DealingExchange { .. } => {
|
||||
Some(time_configuration.dealing_exchange_time_secs)
|
||||
}
|
||||
EpochState::DealingExchange { .. } => time_configuration.dealing_exchange_time_secs,
|
||||
EpochState::VerificationKeySubmission { .. } => {
|
||||
time_configuration.verification_key_submission_time_secs
|
||||
Some(time_configuration.verification_key_submission_time_secs)
|
||||
}
|
||||
EpochState::VerificationKeyValidation { .. } => {
|
||||
time_configuration.verification_key_validation_time_secs
|
||||
Some(time_configuration.verification_key_validation_time_secs)
|
||||
}
|
||||
EpochState::VerificationKeyFinalization { .. } => {
|
||||
time_configuration.verification_key_finalization_time_secs
|
||||
Some(time_configuration.verification_key_finalization_time_secs)
|
||||
}
|
||||
EpochState::InProgress => time_configuration.in_progress_time_secs,
|
||||
EpochState::InProgress => Some(time_configuration.in_progress_time_secs),
|
||||
};
|
||||
Epoch {
|
||||
state,
|
||||
epoch_id,
|
||||
time_configuration,
|
||||
finish_timestamp: current_timestamp.plus_seconds(duration),
|
||||
finish_timestamp: duration.map(|d| current_timestamp.plus_seconds(d)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn final_timestamp_secs(&self) -> u64 {
|
||||
let mut finish = self.finish_timestamp.seconds();
|
||||
pub fn final_timestamp_secs(&self) -> Option<u64> {
|
||||
let mut finish = self.finish_timestamp?.seconds();
|
||||
let time_configuration = self.time_configuration;
|
||||
let mut curr_epoch_state = self.state;
|
||||
while let Some(state) = curr_epoch_state.next() {
|
||||
curr_epoch_state = state;
|
||||
let adding = match curr_epoch_state {
|
||||
EpochState::WaitingInitialisation => return None,
|
||||
EpochState::PublicKeySubmission { .. } => {
|
||||
time_configuration.public_key_submission_time_secs
|
||||
}
|
||||
@@ -137,12 +155,13 @@ impl Epoch {
|
||||
};
|
||||
finish += adding;
|
||||
}
|
||||
finish
|
||||
Some(finish)
|
||||
}
|
||||
}
|
||||
|
||||
// currently (it is still extremely likely to change, we might be able to get rid of verification key-related complaints),
|
||||
// the epoch can be in the following states (in order):
|
||||
// 0. WaitingInitialisation -> the contract has been instantiated, but awaits for the admin to kick off the process (group members might still be getting added)
|
||||
// 1. PublicKeySubmission -> potential dealers are submitting their BTE and ed25519 public keys to participate in dealing exchange
|
||||
// 2. DealingExchange -> the actual (off-chain) dealing exchange is happening
|
||||
// 3. ComplaintSubmission -> receivers submitting evidence of other dealers sending malformed data
|
||||
@@ -156,6 +175,7 @@ impl Epoch {
|
||||
#[cw_serde]
|
||||
#[derive(Copy)]
|
||||
pub enum EpochState {
|
||||
WaitingInitialisation,
|
||||
PublicKeySubmission { resharing: bool },
|
||||
DealingExchange { resharing: bool },
|
||||
VerificationKeySubmission { resharing: bool },
|
||||
@@ -166,13 +186,14 @@ pub enum EpochState {
|
||||
|
||||
impl Default for EpochState {
|
||||
fn default() -> Self {
|
||||
Self::PublicKeySubmission { resharing: false }
|
||||
Self::WaitingInitialisation
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EpochState {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
EpochState::WaitingInitialisation => write!(f, "Waiting for initialisation"),
|
||||
EpochState::PublicKeySubmission { resharing } => {
|
||||
write!(f, "PublicKeySubmission (resharing: {resharing})")
|
||||
}
|
||||
@@ -194,8 +215,13 @@ impl Display for EpochState {
|
||||
}
|
||||
|
||||
impl EpochState {
|
||||
pub fn first() -> Self {
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
}
|
||||
|
||||
pub fn next(self) -> Option<Self> {
|
||||
match self {
|
||||
EpochState::WaitingInitialisation => None,
|
||||
EpochState::PublicKeySubmission { resharing } => {
|
||||
Some(EpochState::DealingExchange { resharing })
|
||||
}
|
||||
|
||||
@@ -20,6 +20,13 @@ pub struct ContractVKShare {
|
||||
pub verified: bool,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct VkShareResponse {
|
||||
pub owner: Addr,
|
||||
pub epoch_id: EpochId,
|
||||
pub share: Option<ContractVKShare>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct PagedVKSharesResponse {
|
||||
pub shares: Vec<ContractVKShare>,
|
||||
@@ -36,7 +43,10 @@ pub fn to_cosmos_msg(
|
||||
multisig_addr: String,
|
||||
expiration_time: Timestamp,
|
||||
) -> StdResult<CosmosMsg> {
|
||||
let verify_vk_share_req = ExecuteMsg::VerifyVerificationKeyShare { owner, resharing };
|
||||
let verify_vk_share_req = ExecuteMsg::VerifyVerificationKeyShare {
|
||||
owner: owner.to_string(),
|
||||
resharing,
|
||||
};
|
||||
let verify_vk_share_msg = CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr: coconut_dkg_addr,
|
||||
msg: to_binary(&verify_vk_share_req)?,
|
||||
@@ -57,7 +67,14 @@ pub fn to_cosmos_msg(
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
pub fn owner_from_cosmos_msgs(msgs: &[CosmosMsg]) -> Option<Addr> {
|
||||
// DKG SAFETY:
|
||||
// each legit verification proposal will only contain a single execute msg,
|
||||
// if they have more than one, we can safely ignore it
|
||||
pub fn owner_from_cosmos_msgs(msgs: &[CosmosMsg]) -> Option<String> {
|
||||
if msgs.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr: _,
|
||||
msg,
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
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
|
||||
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<u8>);
|
||||
|
||||
@@ -23,6 +23,18 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContractSafeBytes {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
if !self.0.is_empty() {
|
||||
|
||||
@@ -5,7 +5,9 @@ use nym_bandwidth_controller::acquire::state::State;
|
||||
use nym_client_core::config::disk_persistence::CommonClientPaths;
|
||||
use nym_config::DEFAULT_DATA_DIR;
|
||||
use nym_credential_storage::persistent_storage::PersistentStorage;
|
||||
use nym_validator_client::nyxd::contract_traits::{CoconutBandwidthSigningClient, DkgQueryClient};
|
||||
use nym_validator_client::nyxd::contract_traits::{
|
||||
dkg_query_client::EpochState, CoconutBandwidthSigningClient, DkgQueryClient,
|
||||
};
|
||||
use nym_validator_client::nyxd::Coin;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
@@ -87,21 +89,29 @@ where
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("the system clock is set to 01/01/1970 (or earlier)")
|
||||
.as_secs();
|
||||
|
||||
if epoch.state.is_final() {
|
||||
if current_timestamp_secs + SAFETY_BUFFER_SECS >= epoch.finish_timestamp.seconds() {
|
||||
info!("In the next {} minute(s), a transition will take place in the coconut system. Deposits should be halted in this time for safety reasons.", SAFETY_BUFFER_SECS / 60);
|
||||
exit(0);
|
||||
if let Some(finish_timestamp) = epoch.finish_timestamp {
|
||||
if current_timestamp_secs + SAFETY_BUFFER_SECS >= finish_timestamp.seconds() {
|
||||
info!("In the next {} minute(s), a transition will take place in the coconut system. Deposits should be halted in this time for safety reasons.", SAFETY_BUFFER_SECS / 60);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
} else {
|
||||
} else if let Some(final_timestamp) = epoch.final_timestamp_secs() {
|
||||
// Use 1 additional second to not start the next iteration immediately and spam get_current_epoch queries
|
||||
let secs_until_final = epoch
|
||||
.final_timestamp_secs()
|
||||
.saturating_sub(current_timestamp_secs)
|
||||
+ 1;
|
||||
let secs_until_final = final_timestamp.saturating_sub(current_timestamp_secs) + 1;
|
||||
info!("Approximately {} seconds until coconut is available. Sleeping until then. You can safely kill the process at any moment.", secs_until_final);
|
||||
tokio::time::sleep(Duration::from_secs(secs_until_final)).await;
|
||||
} else if matches!(epoch.state, EpochState::WaitingInitialisation) {
|
||||
info!("dkg hasn't been initialised yet and it is not known when it will be. Going to check again later");
|
||||
tokio::time::sleep(Duration::from_secs(60 * 5)).await;
|
||||
} else {
|
||||
// this should never be the case since the only case where final timestamp is unknown is when it's waiting for initialisation,
|
||||
// but let's guard ourselves against future changes
|
||||
info!("it is unknown when coconut will be come available. Going to check again later");
|
||||
tokio::time::sleep(Duration::from_secs(60 * 5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ use std::collections::HashMap;
|
||||
use std::ops::Neg;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(Clone, PartialEq, Eq))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Ciphertexts {
|
||||
pub rr: [G1Projective; NUM_CHUNKS],
|
||||
pub ss: [G1Projective; NUM_CHUNKS],
|
||||
|
||||
@@ -53,8 +53,7 @@ impl PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PublicKeyWithProof {
|
||||
pub(crate) key: PublicKey,
|
||||
pub(crate) proof: ProofOfDiscreteLog,
|
||||
|
||||
@@ -67,8 +67,7 @@ impl<'a> Instance<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(Clone, PartialEq, Eq))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ProofOfChunking {
|
||||
y0: G1Projective,
|
||||
bb: Vec<G1Projective>,
|
||||
|
||||
@@ -13,8 +13,7 @@ use zeroize::Zeroize;
|
||||
const DISCRETE_LOG_DOMAIN: &[u8] =
|
||||
b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_DISCRETE_LOG";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ProofOfDiscreteLog {
|
||||
pub(crate) rand_commitment: G1Projective,
|
||||
pub(crate) response: Scalar,
|
||||
|
||||
@@ -76,8 +76,7 @@ impl<'a> Instance<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(Clone, PartialEq, Eq))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ProofOfSecretSharing {
|
||||
ff: G1Projective,
|
||||
aa: G2Projective,
|
||||
|
||||
+100
-20
@@ -82,8 +82,7 @@ impl RecoveredVerificationKeys {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Dealing {
|
||||
pub public_coefficients: PublicCoefficients,
|
||||
pub ciphertexts: Ciphertexts,
|
||||
@@ -321,9 +320,17 @@ impl<'a> TryFrom<&'a nym_contracts_common::dealings::ContractSafeBytes> for Deal
|
||||
}
|
||||
}
|
||||
|
||||
// this assumes all dealings have been verified
|
||||
/// Attempt to run the `VkCombine` algorithm to obtain the public master verification key, `VK`
|
||||
/// alongside shares of the verification key, `shvk_{1}`, `shvk_{2}`, ... `svhk_{n}`, where n is the number of receivers.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `dealings`: map of dealer indices to dealings they generated
|
||||
/// * `threshold`: explicit threshold value of the associated dealings
|
||||
/// * `receivers`:map of receiver indices to their public keys
|
||||
// note: this function assumes all dealings have already been verified
|
||||
pub fn try_recover_verification_keys(
|
||||
dealings: &[Dealing],
|
||||
dealings: &BTreeMap<NodeIndex, Dealing>,
|
||||
threshold: Threshold,
|
||||
receivers: &BTreeMap<NodeIndex, PublicKey>,
|
||||
) -> Result<RecoveredVerificationKeys, DkgError> {
|
||||
@@ -331,24 +338,31 @@ pub fn try_recover_verification_keys(
|
||||
return Err(DkgError::NoDealingsAvailable);
|
||||
}
|
||||
|
||||
let threshold_usize = threshold as usize;
|
||||
let threshold = threshold as usize;
|
||||
|
||||
if dealings.len() < threshold {
|
||||
return Err(DkgError::NotEnoughDealingsAvailable {
|
||||
available: dealings.len(),
|
||||
required: threshold,
|
||||
});
|
||||
}
|
||||
|
||||
if !dealings
|
||||
.iter()
|
||||
.all(|dealing| dealing.public_coefficients.size() == threshold_usize)
|
||||
.values()
|
||||
.all(|dealing| dealing.public_coefficients.size() == threshold)
|
||||
{
|
||||
return Err(DkgError::MismatchedDealings);
|
||||
}
|
||||
|
||||
let indices = receivers.keys().collect::<Vec<_>>();
|
||||
let dealer_indices = dealings.keys().collect::<Vec<_>>();
|
||||
|
||||
// Compute A0, ..., A_{t-1}
|
||||
let mut interpolated_coefficients = Vec::with_capacity(threshold_usize);
|
||||
for k in 0..threshold_usize {
|
||||
let mut samples = Vec::with_capacity(indices.len());
|
||||
for (j, dealing) in dealings.iter().enumerate() {
|
||||
let mut interpolated_coefficients = Vec::with_capacity(threshold);
|
||||
for k in 0..threshold {
|
||||
let mut samples = Vec::with_capacity(dealer_indices.len());
|
||||
for (dealer_index, dealing) in dealings.iter() {
|
||||
samples.push((
|
||||
Scalar::from(*indices[j]),
|
||||
Scalar::from(*dealer_index),
|
||||
*dealing.public_coefficients.nth(k),
|
||||
))
|
||||
}
|
||||
@@ -365,7 +379,7 @@ pub fn try_recover_verification_keys(
|
||||
// shvk_j = A0^{j^0} * A1^{j^1} * ... * A_{t-1}^{j^{t-1}}
|
||||
let verification_key_shares = receivers
|
||||
.keys()
|
||||
.map(|index| interpolated_coefficients.evaluate_at(&Scalar::from(*index)))
|
||||
.map(|receiver_index| interpolated_coefficients.evaluate_at(&Scalar::from(*receiver_index)))
|
||||
.collect();
|
||||
|
||||
Ok(RecoveredVerificationKeys {
|
||||
@@ -457,14 +471,17 @@ mod tests {
|
||||
let dealings = node_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0
|
||||
(
|
||||
dealer_index,
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let mut derived_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.iter()
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
derived_secrets.push(
|
||||
@@ -513,9 +530,12 @@ mod tests {
|
||||
let dealings = node_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0
|
||||
(
|
||||
dealer_index,
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let RecoveredVerificationKeys {
|
||||
recovered_master,
|
||||
@@ -531,6 +551,66 @@ mod tests {
|
||||
.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn verifying_partial_verification_keys_with_different_dealers_and_receivers() {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
|
||||
let dealer_indices = [1, 2, 3, 8];
|
||||
let receiver_indices = [3, 4, 5, 6, 7];
|
||||
let threshold = 3;
|
||||
|
||||
let mut receivers = BTreeMap::new();
|
||||
let mut full_keys = Vec::new();
|
||||
for index in &receiver_indices {
|
||||
let (dk, pk) = keygen(¶ms, &mut rng);
|
||||
receivers.insert(*index, *pk.public_key());
|
||||
full_keys.push((dk, pk))
|
||||
}
|
||||
|
||||
let dealings = dealer_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
(
|
||||
dealer_index,
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0,
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let RecoveredVerificationKeys {
|
||||
recovered_master,
|
||||
recovered_partials,
|
||||
} = try_recover_verification_keys(&dealings, threshold, &receivers).unwrap();
|
||||
|
||||
let g2 = G2Projective::generator();
|
||||
|
||||
let mut derived_secrets = Vec::new();
|
||||
for (i, (dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret = combine_shares(shares, &dealer_indices).unwrap();
|
||||
|
||||
// make sure it matches the associated vk
|
||||
assert_eq!(recovered_partials[i], g2 * recovered_secret);
|
||||
|
||||
derived_secrets.push(recovered_secret)
|
||||
}
|
||||
|
||||
assert!(verify_verification_keys(
|
||||
&recovered_master,
|
||||
&recovered_partials,
|
||||
&receivers,
|
||||
threshold
|
||||
)
|
||||
.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn dealing_roundtrip() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[derive(Debug, Error, Clone)]
|
||||
pub enum DkgError {
|
||||
#[error("Provided set of values contained duplicate coordinate")]
|
||||
DuplicateCoordinate,
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod dealing;
|
||||
pub(crate) mod share;
|
||||
pub(crate) mod utils;
|
||||
|
||||
pub use bls12_381::{G2Projective, Scalar};
|
||||
pub use dealing::*;
|
||||
pub use share::*;
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ fn single_sender() {
|
||||
.unwrap();
|
||||
|
||||
// make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check)
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap();
|
||||
}
|
||||
|
||||
@@ -91,10 +91,13 @@ fn full_threshold_secret_sharing() {
|
||||
let dealings = node_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0
|
||||
(
|
||||
dealer_index,
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for dealing in dealings.iter() {
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
for dealing in dealings.values() {
|
||||
dealing
|
||||
.verify(¶ms, threshold, &receivers, None)
|
||||
.unwrap();
|
||||
@@ -109,9 +112,9 @@ fn full_threshold_secret_sharing() {
|
||||
let g2 = G2Projective::generator();
|
||||
|
||||
let mut derived_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.iter()
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
@@ -169,9 +172,12 @@ fn full_threshold_secret_resharing() {
|
||||
let first_dealings = node_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0
|
||||
(
|
||||
dealer_index,
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// recover verification keys
|
||||
let RecoveredVerificationKeys {
|
||||
@@ -180,9 +186,9 @@ fn full_threshold_secret_resharing() {
|
||||
} = try_recover_verification_keys(&first_dealings, threshold, &receivers).unwrap();
|
||||
|
||||
let mut derived_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = first_dealings
|
||||
.iter()
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
@@ -203,19 +209,22 @@ fn full_threshold_secret_resharing() {
|
||||
.iter()
|
||||
.zip(derived_secrets.iter())
|
||||
.map(|(&dealer_index, prior_secret)| {
|
||||
Dealing::create(
|
||||
&mut rng,
|
||||
¶ms,
|
||||
(
|
||||
dealer_index,
|
||||
threshold,
|
||||
&receivers,
|
||||
Some(*prior_secret),
|
||||
Dealing::create(
|
||||
&mut rng,
|
||||
¶ms,
|
||||
dealer_index,
|
||||
threshold,
|
||||
&receivers,
|
||||
Some(*prior_secret),
|
||||
)
|
||||
.0,
|
||||
)
|
||||
.0
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
for (reshared_dealing, prior_vk) in resharing_dealings.iter().zip(recovered_partials.iter()) {
|
||||
for (reshared_dealing, prior_vk) in resharing_dealings.values().zip(recovered_partials.iter()) {
|
||||
reshared_dealing
|
||||
.verify(¶ms, threshold, &receivers, Some(*prior_vk))
|
||||
.unwrap();
|
||||
@@ -228,9 +237,9 @@ fn full_threshold_secret_resharing() {
|
||||
} = try_recover_verification_keys(&resharing_dealings, threshold, &receivers).unwrap();
|
||||
|
||||
let mut reshared_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = resharing_dealings
|
||||
.iter()
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
@@ -279,9 +288,12 @@ fn full_threshold_secret_resharing_left_party() {
|
||||
let first_dealings = node_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0
|
||||
(
|
||||
dealer_index,
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// recover verification keys
|
||||
let RecoveredVerificationKeys {
|
||||
@@ -290,9 +302,9 @@ fn full_threshold_secret_resharing_left_party() {
|
||||
} = try_recover_verification_keys(&first_dealings, threshold, &receivers).unwrap();
|
||||
|
||||
let mut derived_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = first_dealings
|
||||
.iter()
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
@@ -323,20 +335,23 @@ fn full_threshold_secret_resharing_left_party() {
|
||||
.iter()
|
||||
.zip(derived_secrets.iter().take(2))
|
||||
.map(|(&dealer_index, prior_secret)| {
|
||||
Dealing::create(
|
||||
&mut rng,
|
||||
¶ms,
|
||||
(
|
||||
dealer_index,
|
||||
threshold,
|
||||
&receivers,
|
||||
Some(*prior_secret),
|
||||
Dealing::create(
|
||||
&mut rng,
|
||||
¶ms,
|
||||
dealer_index,
|
||||
threshold,
|
||||
&receivers,
|
||||
Some(*prior_secret),
|
||||
)
|
||||
.0,
|
||||
)
|
||||
.0
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
for (reshared_dealing, prior_vk) in resharing_dealings
|
||||
.iter()
|
||||
.values()
|
||||
.zip(recovered_partials.iter().take(2))
|
||||
{
|
||||
reshared_dealing
|
||||
@@ -351,9 +366,9 @@ fn full_threshold_secret_resharing_left_party() {
|
||||
} = try_recover_verification_keys(&resharing_dealings, threshold, &receivers).unwrap();
|
||||
|
||||
let mut reshared_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = resharing_dealings
|
||||
.iter()
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -99,7 +99,9 @@ impl SecretKey {
|
||||
Self { x, ys }
|
||||
}
|
||||
|
||||
pub fn into_raw(&self) -> (Scalar, Vec<Scalar>) {
|
||||
/// Extract the Scalar copy of the underlying secrets.
|
||||
/// The caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized
|
||||
pub fn hazmat_to_raw(&self) -> (Scalar, Vec<Scalar>) {
|
||||
(self.x, self.ys.clone())
|
||||
}
|
||||
|
||||
|
||||
@@ -228,10 +228,11 @@ pub fn check_vk_pairing(
|
||||
|
||||
// safety: we made an explicit check for if the length of the slice is 0, thus unwrap here is fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
if &vk.alpha != *dkg_values.last().as_ref().unwrap() {
|
||||
if &vk.alpha != *dkg_values.first().as_ref().unwrap() {
|
||||
return false;
|
||||
}
|
||||
if dkg_values
|
||||
let dkg_betas = &dkg_values[1..];
|
||||
if dkg_betas
|
||||
.iter()
|
||||
.zip(vk.beta_g2.iter())
|
||||
.any(|(dkg_beta, vk_beta)| dkg_beta != vk_beta)
|
||||
@@ -329,8 +330,9 @@ mod tests {
|
||||
let params = setup(2).unwrap();
|
||||
let keypair = keygen(¶ms);
|
||||
let vk = keypair.verification_key();
|
||||
let mut dkg_values = vk.beta_g2.clone();
|
||||
dkg_values.push(vk.alpha);
|
||||
|
||||
let mut dkg_values = vec![vk.alpha];
|
||||
dkg_values.append(&mut vk.beta_g2.clone());
|
||||
assert!(check_vk_pairing(¶ms, &dkg_values, vk));
|
||||
}
|
||||
|
||||
|
||||
Generated
+6
-3
@@ -1220,12 +1220,13 @@ dependencies = [
|
||||
"cw-controllers",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"cw4",
|
||||
"cw4-group",
|
||||
"lazy_static",
|
||||
"nym-coconut-dkg-common",
|
||||
"nym-group-contract-common",
|
||||
"rusty-fork",
|
||||
"semver",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
@@ -1237,6 +1238,8 @@ dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"cw4",
|
||||
"nym-contracts-common",
|
||||
"nym-multisig-contract-common",
|
||||
]
|
||||
@@ -1879,9 +1882,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.17"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
|
||||
checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
|
||||
@@ -48,5 +48,6 @@ cw3 = "=1.1.0"
|
||||
cw3-fixed-multisig = "=1.1.0"
|
||||
cw4 = "=1.1.0"
|
||||
cw20 = "=1.1.0"
|
||||
semver = "1.0.21"
|
||||
|
||||
thiserror = "1.0.48"
|
||||
|
||||
@@ -20,15 +20,16 @@ cosmwasm-std = { workspace = true }
|
||||
cosmwasm-storage = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw4 = { workspace = true }
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
semver = { workspace = true, default-features = false }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
cw-multi-test = { workspace = true }
|
||||
cw4-group = { path = "../multisig/cw4-group" }
|
||||
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract" }
|
||||
lazy_static = "1.4"
|
||||
rusty-fork = "0.3"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"group_addr",
|
||||
"key_size",
|
||||
"mix_denom",
|
||||
"multisig_addr"
|
||||
],
|
||||
@@ -15,6 +16,12 @@
|
||||
"group_addr": {
|
||||
"type": "string"
|
||||
},
|
||||
"key_size": {
|
||||
"description": "Specifies the number of elements in the derived keys",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"mix_denom": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -84,6 +91,19 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ExecuteMsg",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"initiate_dkg"
|
||||
],
|
||||
"properties": {
|
||||
"initiate_dkg": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -95,6 +115,7 @@
|
||||
"required": [
|
||||
"announce_address",
|
||||
"bte_key_with_proof",
|
||||
"identity_key",
|
||||
"resharing"
|
||||
],
|
||||
"properties": {
|
||||
@@ -104,6 +125,9 @@
|
||||
"bte_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"identity_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"resharing": {
|
||||
"type": "boolean"
|
||||
}
|
||||
@@ -122,12 +146,12 @@
|
||||
"commit_dealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealing_bytes",
|
||||
"dealing",
|
||||
"resharing"
|
||||
],
|
||||
"properties": {
|
||||
"dealing_bytes": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
"dealing": {
|
||||
"$ref": "#/definitions/PartialContractDealing"
|
||||
},
|
||||
"resharing": {
|
||||
"type": "boolean"
|
||||
@@ -227,6 +251,24 @@
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"PartialContractDealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data",
|
||||
"index"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
},
|
||||
"index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -234,6 +276,19 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryMsg",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_state"
|
||||
],
|
||||
"properties": {
|
||||
"get_state": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -352,6 +407,39 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_dealing_status"
|
||||
],
|
||||
"properties": {
|
||||
"get_dealing_status": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"type": "string"
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -361,10 +449,47 @@
|
||||
"get_dealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"idx"
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"idx": {
|
||||
"dealer": {
|
||||
"type": "string"
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_dealings"
|
||||
],
|
||||
"properties": {
|
||||
"get_dealings": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"type": "string"
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
@@ -379,9 +504,38 @@
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"string",
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_verification_key"
|
||||
],
|
||||
"properties": {
|
||||
"get_verification_key": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"epoch_id",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -425,6 +579,20 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Gets the stored contract version information that's required by the CW2 spec interface for migrations.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_cw2_contract_version"
|
||||
],
|
||||
"properties": {
|
||||
"get_cw2_contract_version": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -436,6 +604,26 @@
|
||||
},
|
||||
"sudo": null,
|
||||
"responses": {
|
||||
"get_c_w2_contract_version": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ContractVersion",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"contract",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"contract": {
|
||||
"description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"get_current_dealers": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PagedDealerResponse",
|
||||
@@ -480,7 +668,8 @@
|
||||
"address",
|
||||
"announce_address",
|
||||
"assigned_index",
|
||||
"bte_public_key_with_proof"
|
||||
"bte_public_key_with_proof",
|
||||
"ed25519_identity"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
@@ -496,6 +685,9 @@
|
||||
},
|
||||
"bte_public_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"ed25519_identity": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -508,7 +700,6 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"epoch_id",
|
||||
"finish_timestamp",
|
||||
"state",
|
||||
"time_configuration"
|
||||
],
|
||||
@@ -519,7 +710,14 @@
|
||||
"minimum": 0.0
|
||||
},
|
||||
"finish_timestamp": {
|
||||
"$ref": "#/definitions/Timestamp"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Timestamp"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/definitions/EpochState"
|
||||
@@ -535,6 +733,7 @@
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"waiting_initialisation",
|
||||
"in_progress"
|
||||
]
|
||||
},
|
||||
@@ -744,7 +943,8 @@
|
||||
"address",
|
||||
"announce_address",
|
||||
"assigned_index",
|
||||
"bte_public_key_with_proof"
|
||||
"bte_public_key_with_proof",
|
||||
"ed25519_identity"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
@@ -760,6 +960,9 @@
|
||||
},
|
||||
"bte_public_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"ed25519_identity": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -776,34 +979,36 @@
|
||||
},
|
||||
"get_dealing": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PagedDealingsResponse",
|
||||
"title": "DealingResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealings",
|
||||
"per_page"
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ContractDealing"
|
||||
}
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
|
||||
"dealing": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -812,21 +1017,91 @@
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"ContractDealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealing"
|
||||
"ContractSafeBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_dealing_status": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "DealingStatusResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"dealing_submitted",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"dealing_submitted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_dealings": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PagedDealingsResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealings",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"dealings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PartialContractDealing"
|
||||
}
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"dealing": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"ContractSafeBytes": {
|
||||
"type": "array",
|
||||
@@ -835,6 +1110,24 @@
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"PartialContractDealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data",
|
||||
"index"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
},
|
||||
"index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -921,7 +1214,8 @@
|
||||
"address",
|
||||
"announce_address",
|
||||
"assigned_index",
|
||||
"bte_public_key_with_proof"
|
||||
"bte_public_key_with_proof",
|
||||
"ed25519_identity"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
@@ -937,6 +1231,124 @@
|
||||
},
|
||||
"bte_public_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"ed25519_identity": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_state": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "State",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"group_addr",
|
||||
"key_size",
|
||||
"mix_denom",
|
||||
"multisig_addr"
|
||||
],
|
||||
"properties": {
|
||||
"group_addr": {
|
||||
"$ref": "#/definitions/Cw4Contract"
|
||||
},
|
||||
"key_size": {
|
||||
"description": "Specifies the number of elements in the derived keys",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"mix_denom": {
|
||||
"type": "string"
|
||||
},
|
||||
"multisig_addr": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"Cw4Contract": {
|
||||
"description": "Cw4Contract is a wrapper around Addr that provides a lot of helpers for working with cw4 contracts\n\nIf you wish to persist this, convert to Cw4CanonicalContract via .canonical()",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_verification_key": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "VkShareResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"epoch_id",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"share": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ContractVKShare"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"ContractVKShare": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"announce_address",
|
||||
"epoch_id",
|
||||
"node_index",
|
||||
"owner",
|
||||
"share",
|
||||
"verified"
|
||||
],
|
||||
"properties": {
|
||||
"announce_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_index": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"share": {
|
||||
"type": "string"
|
||||
},
|
||||
"verified": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ExecuteMsg",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"initiate_dkg"
|
||||
],
|
||||
"properties": {
|
||||
"initiate_dkg": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -13,6 +26,7 @@
|
||||
"required": [
|
||||
"announce_address",
|
||||
"bte_key_with_proof",
|
||||
"identity_key",
|
||||
"resharing"
|
||||
],
|
||||
"properties": {
|
||||
@@ -22,6 +36,9 @@
|
||||
"bte_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"identity_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"resharing": {
|
||||
"type": "boolean"
|
||||
}
|
||||
@@ -40,12 +57,12 @@
|
||||
"commit_dealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealing_bytes",
|
||||
"dealing",
|
||||
"resharing"
|
||||
],
|
||||
"properties": {
|
||||
"dealing_bytes": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
"dealing": {
|
||||
"$ref": "#/definitions/PartialContractDealing"
|
||||
},
|
||||
"resharing": {
|
||||
"type": "boolean"
|
||||
@@ -145,6 +162,24 @@
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"PartialContractDealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data",
|
||||
"index"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
},
|
||||
"index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"group_addr",
|
||||
"key_size",
|
||||
"mix_denom",
|
||||
"multisig_addr"
|
||||
],
|
||||
@@ -11,6 +12,12 @@
|
||||
"group_addr": {
|
||||
"type": "string"
|
||||
},
|
||||
"key_size": {
|
||||
"description": "Specifies the number of elements in the derived keys",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"mix_denom": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryMsg",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_state"
|
||||
],
|
||||
"properties": {
|
||||
"get_state": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -120,6 +133,39 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_dealing_status"
|
||||
],
|
||||
"properties": {
|
||||
"get_dealing_status": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"type": "string"
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -129,10 +175,47 @@
|
||||
"get_dealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"idx"
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"idx": {
|
||||
"dealer": {
|
||||
"type": "string"
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_dealings"
|
||||
],
|
||||
"properties": {
|
||||
"get_dealings": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"type": "string"
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
@@ -147,9 +230,38 @@
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"string",
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_verification_key"
|
||||
],
|
||||
"properties": {
|
||||
"get_verification_key": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"epoch_id",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -193,6 +305,20 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Gets the stored contract version information that's required by the CW2 spec interface for migrations.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_cw2_contract_version"
|
||||
],
|
||||
"properties": {
|
||||
"get_cw2_contract_version": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ContractVersion",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"contract",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"contract": {
|
||||
"description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -42,7 +42,8 @@
|
||||
"address",
|
||||
"announce_address",
|
||||
"assigned_index",
|
||||
"bte_public_key_with_proof"
|
||||
"bte_public_key_with_proof",
|
||||
"ed25519_identity"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
@@ -58,6 +59,9 @@
|
||||
},
|
||||
"bte_public_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"ed25519_identity": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"epoch_id",
|
||||
"finish_timestamp",
|
||||
"state",
|
||||
"time_configuration"
|
||||
],
|
||||
@@ -15,7 +14,14 @@
|
||||
"minimum": 0.0
|
||||
},
|
||||
"finish_timestamp": {
|
||||
"$ref": "#/definitions/Timestamp"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Timestamp"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/definitions/EpochState"
|
||||
@@ -31,6 +37,7 @@
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"waiting_initialisation",
|
||||
"in_progress"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"address",
|
||||
"announce_address",
|
||||
"assigned_index",
|
||||
"bte_public_key_with_proof"
|
||||
"bte_public_key_with_proof",
|
||||
"ed25519_identity"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
@@ -48,6 +49,9 @@
|
||||
},
|
||||
"bte_public_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"ed25519_identity": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PagedDealingsResponse",
|
||||
"title": "DealingResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealings",
|
||||
"per_page"
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ContractDealing"
|
||||
}
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
|
||||
"dealing": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -36,22 +38,6 @@
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"ContractDealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealing"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"dealing": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ContractSafeBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "DealingStatusResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"dealing_submitted",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"dealing_submitted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PagedDealingsResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealings",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"dealings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PartialContractDealing"
|
||||
}
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"ContractSafeBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"PartialContractDealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data",
|
||||
"index"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
},
|
||||
"index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,8 @@
|
||||
"address",
|
||||
"announce_address",
|
||||
"assigned_index",
|
||||
"bte_public_key_with_proof"
|
||||
"bte_public_key_with_proof",
|
||||
"ed25519_identity"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
@@ -58,6 +59,9 @@
|
||||
},
|
||||
"bte_public_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"ed25519_identity": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "State",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"group_addr",
|
||||
"key_size",
|
||||
"mix_denom",
|
||||
"multisig_addr"
|
||||
],
|
||||
"properties": {
|
||||
"group_addr": {
|
||||
"$ref": "#/definitions/Cw4Contract"
|
||||
},
|
||||
"key_size": {
|
||||
"description": "Specifies the number of elements in the derived keys",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"mix_denom": {
|
||||
"type": "string"
|
||||
},
|
||||
"multisig_addr": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"Cw4Contract": {
|
||||
"description": "Cw4Contract is a wrapper around Addr that provides a lot of helpers for working with cw4 contracts\n\nIf you wish to persist this, convert to Cw4CanonicalContract via .canonical()",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "VkShareResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"epoch_id",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"share": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ContractVKShare"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"ContractVKShare": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"announce_address",
|
||||
"epoch_id",
|
||||
"node_index",
|
||||
"owner",
|
||||
"share",
|
||||
"verified"
|
||||
],
|
||||
"properties": {
|
||||
"announce_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_index": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"share": {
|
||||
"type": "string"
|
||||
},
|
||||
"verified": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,26 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::dealers::queries::{
|
||||
query_current_dealers_paged, query_dealer_details, query_past_dealers_paged,
|
||||
};
|
||||
use crate::dealers::transactions::try_add_dealer;
|
||||
use crate::dealings::queries::query_dealings_paged;
|
||||
use crate::dealings::transactions::try_commit_dealings;
|
||||
use crate::dealings::queries::{
|
||||
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::{
|
||||
query_current_epoch, query_current_epoch_threshold, query_initial_dealers,
|
||||
};
|
||||
use crate::epoch_state::storage::CURRENT_EPOCH;
|
||||
use crate::epoch_state::transactions::{advance_epoch_state, try_surpassed_threshold};
|
||||
use crate::epoch_state::transactions::{
|
||||
advance_epoch_state, try_initiate_dkg, try_surpassed_threshold,
|
||||
};
|
||||
use crate::error::ContractError;
|
||||
use crate::state::{State, MULTISIG, STATE};
|
||||
use crate::verification_key_shares::queries::query_vk_shares_paged;
|
||||
use crate::state::queries::query_state;
|
||||
use crate::state::storage::{DKG_ADMIN, MULTISIG, STATE};
|
||||
use crate::verification_key_shares::queries::{query_vk_share, query_vk_shares_paged};
|
||||
use crate::verification_key_shares::transactions::try_commit_verification_key_share;
|
||||
use crate::verification_key_shares::transactions::try_verify_verification_key_share;
|
||||
use cosmwasm_std::{
|
||||
@@ -22,7 +28,11 @@ use cosmwasm_std::{
|
||||
};
|
||||
use cw4::Cw4Contract;
|
||||
use nym_coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
|
||||
use nym_coconut_dkg_common::types::{Epoch, EpochState};
|
||||
use nym_coconut_dkg_common::types::{Epoch, EpochState, State};
|
||||
use semver::Version;
|
||||
|
||||
const CONTRACT_NAME: &str = "crate:nym-coconut-dkg";
|
||||
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// Instantiate the contract.
|
||||
///
|
||||
@@ -33,13 +43,15 @@ use nym_coconut_dkg_common::types::{Epoch, EpochState};
|
||||
pub fn instantiate(
|
||||
mut deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
_info: MessageInfo,
|
||||
info: MessageInfo,
|
||||
msg: InstantiateMsg,
|
||||
) -> Result<Response, ContractError> {
|
||||
let multisig_addr = deps.api.addr_validate(&msg.multisig_addr)?;
|
||||
MULTISIG.set(deps.branch(), Some(multisig_addr.clone()))?;
|
||||
|
||||
let group_addr = Cw4Contract(deps.api.addr_validate(&msg.group_addr).map_err(|_| {
|
||||
DKG_ADMIN.set(deps.branch(), Some(info.sender))?;
|
||||
|
||||
let group_addr = Cw4Contract::new(deps.api.addr_validate(&msg.group_addr).map_err(|_| {
|
||||
ContractError::InvalidGroup {
|
||||
addr: msg.group_addr.clone(),
|
||||
}
|
||||
@@ -49,19 +61,22 @@ pub fn instantiate(
|
||||
group_addr,
|
||||
multisig_addr,
|
||||
mix_denom: msg.mix_denom,
|
||||
key_size: msg.key_size,
|
||||
};
|
||||
STATE.save(deps.storage, &state)?;
|
||||
|
||||
CURRENT_EPOCH.save(
|
||||
deps.storage,
|
||||
&Epoch::new(
|
||||
EpochState::default(),
|
||||
EpochState::WaitingInitialisation,
|
||||
0,
|
||||
msg.time_configuration.unwrap_or_default(),
|
||||
env.block.time,
|
||||
),
|
||||
)?;
|
||||
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
@@ -74,15 +89,28 @@ pub fn execute(
|
||||
msg: ExecuteMsg,
|
||||
) -> Result<Response, ContractError> {
|
||||
match msg {
|
||||
ExecuteMsg::InitiateDkg {} => try_initiate_dkg(deps, env, info),
|
||||
ExecuteMsg::RegisterDealer {
|
||||
bte_key_with_proof,
|
||||
identity_key,
|
||||
announce_address,
|
||||
resharing,
|
||||
} => try_add_dealer(deps, info, bte_key_with_proof, announce_address, resharing),
|
||||
ExecuteMsg::CommitDealing {
|
||||
dealing_bytes,
|
||||
} => try_add_dealer(
|
||||
deps,
|
||||
info,
|
||||
bte_key_with_proof,
|
||||
identity_key,
|
||||
announce_address,
|
||||
resharing,
|
||||
} => try_commit_dealings(deps, info, dealing_bytes, resharing),
|
||||
),
|
||||
ExecuteMsg::CommitDealingsMetadata {
|
||||
dealing_index,
|
||||
chunks,
|
||||
resharing,
|
||||
} => try_submit_dealings_metadata(deps, info, dealing_index, chunks, resharing),
|
||||
ExecuteMsg::CommitDealingsChunk { chunk, resharing } => {
|
||||
try_commit_dealings_chunk(deps, env, info, chunk, resharing)
|
||||
}
|
||||
ExecuteMsg::CommitVerificationKeyShare { share, resharing } => {
|
||||
try_commit_verification_key_share(deps, env, info, share, resharing)
|
||||
}
|
||||
@@ -97,6 +125,7 @@ pub fn execute(
|
||||
#[entry_point]
|
||||
pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
|
||||
let response = match msg {
|
||||
QueryMsg::GetState {} => to_binary(&query_state(deps.storage)?)?,
|
||||
QueryMsg::GetCurrentEpochState {} => to_binary(&query_current_epoch(deps.storage)?)?,
|
||||
QueryMsg::GetCurrentEpochThreshold {} => {
|
||||
to_binary(&query_current_epoch_threshold(deps.storage)?)?
|
||||
@@ -111,24 +140,90 @@ pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result<QueryResponse,
|
||||
QueryMsg::GetPastDealers { limit, start_after } => {
|
||||
to_binary(&query_past_dealers_paged(deps, start_after, limit)?)?
|
||||
}
|
||||
QueryMsg::GetDealing {
|
||||
idx,
|
||||
limit,
|
||||
start_after,
|
||||
} => to_binary(&query_dealings_paged(deps, idx, start_after, limit)?)?,
|
||||
QueryMsg::GetDealingsMetadata {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
} => to_binary(&query_dealing_metadata(
|
||||
deps,
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
)?)?,
|
||||
QueryMsg::GetDealerDealingsStatus { epoch_id, dealer } => {
|
||||
to_binary(&query_dealer_dealings_status(deps, epoch_id, dealer)?)?
|
||||
}
|
||||
QueryMsg::GetDealingStatus {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
} => to_binary(&query_dealing_status(
|
||||
deps,
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
)?)?,
|
||||
QueryMsg::GetDealingChunkStatus {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
chunk_index,
|
||||
} => to_binary(&query_dealing_chunk_status(
|
||||
deps,
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
chunk_index,
|
||||
)?)?,
|
||||
QueryMsg::GetDealingChunk {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
chunk_index,
|
||||
} => to_binary(&query_dealing_chunk(
|
||||
deps,
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
chunk_index,
|
||||
)?)?,
|
||||
QueryMsg::GetVerificationKey { owner, epoch_id } => {
|
||||
to_binary(&query_vk_share(deps, owner, epoch_id)?)?
|
||||
}
|
||||
QueryMsg::GetVerificationKeys {
|
||||
epoch_id,
|
||||
limit,
|
||||
start_after,
|
||||
} => to_binary(&query_vk_shares_paged(deps, epoch_id, start_after, limit)?)?,
|
||||
QueryMsg::GetCW2ContractVersion {} => to_binary(&cw2::get_contract_version(deps.storage)?)?,
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[entry_point]
|
||||
pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
|
||||
Ok(Default::default())
|
||||
pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
|
||||
fn parse_semver(raw: &str) -> Result<Version, ContractError> {
|
||||
raw.parse()
|
||||
.map_err(|error: semver::Error| ContractError::SemVerFailure {
|
||||
value: CONTRACT_VERSION.to_string(),
|
||||
error_message: error.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
// Note: don't remove this particular bit of code as we have to ALWAYS check whether we have to
|
||||
// update the stored version
|
||||
let build_version: Version = parse_semver(CONTRACT_VERSION)?;
|
||||
let stored_version: Version = parse_semver(&cw2::get_contract_version(deps.storage)?.version)?;
|
||||
|
||||
if stored_version < build_version {
|
||||
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
// If state structure changed in any contract version in the way migration is needed, it
|
||||
// should occur here, for example anything from `crate::queued_migrations::`
|
||||
}
|
||||
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -140,7 +235,8 @@ mod tests {
|
||||
use cosmwasm_std::{coins, Addr};
|
||||
use cw4::Member;
|
||||
use cw_multi_test::{App, AppBuilder, AppResponse, ContractWrapper, Executor};
|
||||
use nym_coconut_dkg_common::msg::ExecuteMsg::RegisterDealer;
|
||||
use nym_coconut_dkg_common::dealing::DEFAULT_DEALINGS;
|
||||
use nym_coconut_dkg_common::msg::ExecuteMsg::{InitiateDkg, RegisterDealer};
|
||||
use nym_coconut_dkg_common::types::NodeIndex;
|
||||
use nym_group_contract_common::msg::InstantiateMsg as GroupInstantiateMsg;
|
||||
|
||||
@@ -178,6 +274,7 @@ mod tests {
|
||||
multisig_addr: MULTISIG_CONTRACT.to_string(),
|
||||
time_configuration: None,
|
||||
mix_denom: TEST_MIX_DENOM.to_string(),
|
||||
key_size: DEFAULT_DEALINGS as u32,
|
||||
};
|
||||
app.instantiate_contract(
|
||||
coconut_dkg_code_id,
|
||||
@@ -213,6 +310,7 @@ mod tests {
|
||||
multisig_addr: "multisig_addr".to_string(),
|
||||
time_configuration: None,
|
||||
mix_denom: "nym".to_string(),
|
||||
key_size: 5,
|
||||
};
|
||||
let info = mock_info("creator", &[]);
|
||||
|
||||
@@ -235,6 +333,14 @@ mod tests {
|
||||
});
|
||||
let coconut_dkg_contract_addr = instantiate_with_group(&mut app, &members);
|
||||
|
||||
app.execute_contract(
|
||||
Addr::unchecked(ADMIN_ADDRESS),
|
||||
coconut_dkg_contract_addr.clone(),
|
||||
&InitiateDkg {},
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for (idx, member) in members.iter().enumerate() {
|
||||
let res = app
|
||||
.execute_contract(
|
||||
@@ -242,6 +348,7 @@ mod tests {
|
||||
coconut_dkg_contract_addr.clone(),
|
||||
&RegisterDealer {
|
||||
bte_key_with_proof: "bte_key_with_proof".to_string(),
|
||||
identity_key: "identity".to_string(),
|
||||
announce_address: "127.0.0.1:8000".to_string(),
|
||||
resharing: false,
|
||||
},
|
||||
@@ -256,6 +363,7 @@ mod tests {
|
||||
coconut_dkg_contract_addr.clone(),
|
||||
&RegisterDealer {
|
||||
bte_key_with_proof: "bte_key_with_proof".to_string(),
|
||||
identity_key: "identity".to_string(),
|
||||
announce_address: "127.0.0.1:8000".to_string(),
|
||||
resharing: false,
|
||||
},
|
||||
@@ -272,6 +380,7 @@ mod tests {
|
||||
coconut_dkg_contract_addr,
|
||||
&RegisterDealer {
|
||||
bte_key_with_proof: "bte_key_with_proof".to_string(),
|
||||
identity_key: "identity".to_string(),
|
||||
announce_address: "127.0.0.1:8000".to_string(),
|
||||
resharing: false,
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::dealers::storage as dealers_storage;
|
||||
use crate::epoch_state::storage::INITIAL_REPLACEMENT_DATA;
|
||||
use crate::epoch_state::utils::check_epoch_state;
|
||||
use crate::error::ContractError;
|
||||
use crate::state::STATE;
|
||||
use crate::state::storage::STATE;
|
||||
use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response};
|
||||
use nym_coconut_dkg_common::types::{DealerDetails, EncodedBTEPublicKeyWithProof, EpochState};
|
||||
|
||||
@@ -34,10 +34,13 @@ fn verify_dealer(deps: DepsMut<'_>, dealer: &Addr, resharing: bool) -> Result<()
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// future optimisation:
|
||||
// for a recurring dealer just let it refresh the keys without having to do all the storage operations
|
||||
pub fn try_add_dealer(
|
||||
mut deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
bte_key_with_proof: EncodedBTEPublicKeyWithProof,
|
||||
identity_key: String,
|
||||
announce_address: String,
|
||||
resharing: bool,
|
||||
) -> Result<Response, ContractError> {
|
||||
@@ -65,6 +68,7 @@ pub fn try_add_dealer(
|
||||
let dealer_details = DealerDetails {
|
||||
address: info.sender.clone(),
|
||||
bte_public_key_with_proof: bte_key_with_proof,
|
||||
ed25519_identity: identity_key,
|
||||
announce_address,
|
||||
assigned_index: node_index,
|
||||
};
|
||||
@@ -77,10 +81,10 @@ pub fn try_add_dealer(
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::dealers::storage::current_dealers;
|
||||
use crate::epoch_state::transactions::advance_epoch_state;
|
||||
use crate::epoch_state::transactions::{advance_epoch_state, try_initiate_dkg};
|
||||
use crate::support::tests::fixtures::dealer_details_fixture;
|
||||
use crate::support::tests::helpers;
|
||||
use crate::support::tests::helpers::{add_fixture_dealer, GROUP_MEMBERS};
|
||||
use crate::support::tests::helpers::{add_fixture_dealer, ADMIN_ADDRESS, GROUP_MEMBERS};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cw4::Member;
|
||||
use nym_coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration};
|
||||
@@ -137,10 +141,13 @@ pub(crate) mod tests {
|
||||
#[test]
|
||||
fn invalid_state() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let owner = Addr::unchecked("owner");
|
||||
let mut env = mock_env();
|
||||
try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap();
|
||||
|
||||
let owner = Addr::unchecked("owner");
|
||||
let info = mock_info(owner.as_str(), &[]);
|
||||
let bte_key_with_proof = String::from("bte_key_with_proof");
|
||||
let identity = String::from("identity");
|
||||
let announce_address = String::from("localhost:8000");
|
||||
|
||||
env.block.time = env
|
||||
@@ -155,6 +162,7 @@ pub(crate) mod tests {
|
||||
deps.as_mut(),
|
||||
info,
|
||||
bte_key_with_proof,
|
||||
identity,
|
||||
announce_address,
|
||||
false,
|
||||
)
|
||||
@@ -163,7 +171,7 @@ pub(crate) mod tests {
|
||||
ret,
|
||||
ContractError::IncorrectEpochState {
|
||||
current_state: EpochState::DealingExchange { resharing: false }.to_string(),
|
||||
expected_state: EpochState::default().to_string(),
|
||||
expected_state: EpochState::PublicKeySubmission { resharing: false }.to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,195 +1,353 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::dealings::storage;
|
||||
use crate::dealings::storage::DEALINGS_BYTES;
|
||||
use cosmwasm_std::{Deps, Order, StdResult};
|
||||
use cw_storage_plus::Bound;
|
||||
use nym_coconut_dkg_common::dealer::{ContractDealing, PagedDealingsResponse};
|
||||
use nym_coconut_dkg_common::types::TOTAL_DEALINGS;
|
||||
use crate::dealings::storage::{StoredDealing, DEALINGS_METADATA};
|
||||
use crate::state::storage::STATE;
|
||||
use cosmwasm_std::{Deps, StdResult};
|
||||
use nym_coconut_dkg_common::dealing::{
|
||||
DealerDealingsStatusResponse, DealingChunkResponse, DealingChunkStatusResponse,
|
||||
DealingMetadataResponse, DealingStatus, DealingStatusResponse,
|
||||
};
|
||||
use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub fn query_dealings_paged(
|
||||
/// Get the metadata associated with the particular dealing
|
||||
pub fn query_dealing_metadata(
|
||||
deps: Deps<'_>,
|
||||
idx: u64,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
) -> StdResult<PagedDealingsResponse> {
|
||||
let limit = limit
|
||||
.unwrap_or(storage::DEALINGS_PAGE_DEFAULT_LIMIT)
|
||||
.min(storage::DEALINGS_PAGE_MAX_LIMIT) as usize;
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
) -> StdResult<DealingMetadataResponse> {
|
||||
let dealer = deps.api.addr_validate(&dealer)?;
|
||||
let metadata = DEALINGS_METADATA.may_load(deps.storage, (epoch_id, &dealer, dealing_index))?;
|
||||
|
||||
let idx = idx as usize;
|
||||
if idx >= TOTAL_DEALINGS {
|
||||
return Ok(PagedDealingsResponse::new(vec![], limit, None));
|
||||
}
|
||||
|
||||
let addr = start_after
|
||||
.map(|addr| deps.api.addr_validate(&addr))
|
||||
.transpose()?;
|
||||
|
||||
let start = addr.as_ref().map(Bound::exclusive);
|
||||
|
||||
let dealings = DEALINGS_BYTES[idx]
|
||||
.range(deps.storage, start, None, Order::Ascending)
|
||||
.take(limit)
|
||||
.map(|res| res.map(|(dealer, dealing)| ContractDealing::new(dealing, dealer)))
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
let start_next_after = dealings.last().map(|dealing| dealing.dealer.clone());
|
||||
|
||||
Ok(PagedDealingsResponse::new(
|
||||
dealings,
|
||||
limit,
|
||||
start_next_after,
|
||||
))
|
||||
Ok(DealingMetadataResponse {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::dealings::storage::{DEALINGS_PAGE_DEFAULT_LIMIT, DEALINGS_PAGE_MAX_LIMIT};
|
||||
use crate::support::tests::fixtures::dealing_bytes_fixture;
|
||||
use crate::support::tests::helpers::init_contract;
|
||||
use cosmwasm_std::{Addr, DepsMut};
|
||||
/// 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)?;
|
||||
|
||||
fn fill_dealings(deps: DepsMut<'_>, size: usize) {
|
||||
for n in 0..size {
|
||||
let dealing_share = dealing_bytes_fixture();
|
||||
let sender = Addr::unchecked(format!("owner{}", n));
|
||||
(0..TOTAL_DEALINGS).for_each(|idx| {
|
||||
DEALINGS_BYTES[idx]
|
||||
.save(deps.storage, &sender, &dealing_share)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_on_bad_idx() {
|
||||
let mut deps = init_contract();
|
||||
fill_dealings(deps.as_mut(), 1000);
|
||||
|
||||
for idx in TOTAL_DEALINGS as u64..100 * TOTAL_DEALINGS as u64 {
|
||||
let page1 = query_dealings_paged(deps.as_ref(), idx, None, None).unwrap();
|
||||
assert_eq!(0, page1.dealings.len() as u32);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dealings_empty_on_init() {
|
||||
let deps = init_contract();
|
||||
for idx in 0..TOTAL_DEALINGS as u64 {
|
||||
let response = query_dealings_paged(deps.as_ref(), idx, None, Option::from(2)).unwrap();
|
||||
assert_eq!(0, response.dealings.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dealings_paged_retrieval_obeys_limits() {
|
||||
let mut deps = init_contract();
|
||||
let limit = 2;
|
||||
fill_dealings(deps.as_mut(), 1000);
|
||||
|
||||
for idx in 0..TOTAL_DEALINGS as u64 {
|
||||
let page1 =
|
||||
query_dealings_paged(deps.as_ref(), idx, None, Option::from(limit)).unwrap();
|
||||
assert_eq!(limit, page1.dealings.len() as u32);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dealings_paged_retrieval_has_default_limit() {
|
||||
let mut deps = init_contract();
|
||||
fill_dealings(deps.as_mut(), 1000);
|
||||
|
||||
for idx in 0..TOTAL_DEALINGS as u64 {
|
||||
// query without explicitly setting a limit
|
||||
let page1 = query_dealings_paged(deps.as_ref(), idx, None, None).unwrap();
|
||||
|
||||
assert_eq!(DEALINGS_PAGE_DEFAULT_LIMIT, page1.dealings.len() as u32);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dealings_paged_retrieval_has_max_limit() {
|
||||
let mut deps = init_contract();
|
||||
fill_dealings(deps.as_mut(), 1000);
|
||||
|
||||
// query with a crazily high limit in an attempt to use too many resources
|
||||
let crazy_limit = 1000 * DEALINGS_PAGE_MAX_LIMIT;
|
||||
for idx in 0..TOTAL_DEALINGS as u64 {
|
||||
let page1 =
|
||||
query_dealings_paged(deps.as_ref(), idx, None, Option::from(crazy_limit)).unwrap();
|
||||
|
||||
// we default to a decent sized upper bound instead
|
||||
let expected_limit = DEALINGS_PAGE_MAX_LIMIT;
|
||||
assert_eq!(expected_limit, page1.dealings.len() as u32);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dealings_pagination_works() {
|
||||
let mut deps = init_contract();
|
||||
|
||||
fill_dealings(deps.as_mut(), 1);
|
||||
|
||||
let per_page = 2;
|
||||
|
||||
for idx in 0..TOTAL_DEALINGS as u64 {
|
||||
let page1 =
|
||||
query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap();
|
||||
|
||||
// page should have 1 result on it
|
||||
assert_eq!(1, page1.dealings.len());
|
||||
}
|
||||
|
||||
// save another
|
||||
fill_dealings(deps.as_mut(), 2);
|
||||
|
||||
for idx in 0..TOTAL_DEALINGS as u64 {
|
||||
// page1 should have 2 results on it
|
||||
let page1 =
|
||||
query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap();
|
||||
assert_eq!(2, page1.dealings.len());
|
||||
}
|
||||
|
||||
fill_dealings(deps.as_mut(), 3);
|
||||
|
||||
for idx in 0..TOTAL_DEALINGS as u64 {
|
||||
// page1 still has 2 results
|
||||
let page1 =
|
||||
query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap();
|
||||
assert_eq!(2, page1.dealings.len());
|
||||
|
||||
// retrieving the next page should start after the last key on this page
|
||||
let start_after = page1.start_next_after.unwrap();
|
||||
let page2 = query_dealings_paged(
|
||||
deps.as_ref(),
|
||||
idx,
|
||||
Option::from(start_after.to_string()),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, page2.dealings.len());
|
||||
}
|
||||
|
||||
fill_dealings(deps.as_mut(), 4);
|
||||
|
||||
for idx in 0..TOTAL_DEALINGS as u64 {
|
||||
let page1 =
|
||||
query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap();
|
||||
let start_after = page1.start_next_after.unwrap();
|
||||
let page2 = query_dealings_paged(
|
||||
deps.as_ref(),
|
||||
idx,
|
||||
Option::from(start_after.to_string()),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now we have 2 pages, with 2 results on the second page
|
||||
assert_eq!(2, page2.dealings.len());
|
||||
}
|
||||
}
|
||||
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<'_>,
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
) -> StdResult<DealingStatusResponse> {
|
||||
let dealer = deps.api.addr_validate(&dealer)?;
|
||||
let metadata = DEALINGS_METADATA.may_load(deps.storage, (epoch_id, &dealer, dealing_index))?;
|
||||
|
||||
Ok(DealingStatusResponse {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
status: metadata.into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the status of particular chunk, i.e. whether (and when) it has been fully submitted.
|
||||
pub fn query_dealing_chunk_status(
|
||||
deps: Deps<'_>,
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
chunk_index: ChunkIndex,
|
||||
) -> StdResult<DealingChunkStatusResponse> {
|
||||
let dealer = deps.api.addr_validate(&dealer)?;
|
||||
let metadata = DEALINGS_METADATA.may_load(deps.storage, (epoch_id, &dealer, dealing_index))?;
|
||||
|
||||
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,
|
||||
status,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the particular chunk of the dealing.
|
||||
pub fn query_dealing_chunk(
|
||||
deps: Deps<'_>,
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
chunk_index: ChunkIndex,
|
||||
) -> StdResult<DealingChunkResponse> {
|
||||
let dealer = deps.api.addr_validate(&dealer)?;
|
||||
let chunk = StoredDealing::read(deps.storage, epoch_id, &dealer, dealing_index, chunk_index);
|
||||
|
||||
Ok(DealingChunkResponse {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
chunk_index,
|
||||
chunk,
|
||||
})
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// 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());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,33 +1,421 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::Addr;
|
||||
use cw_storage_plus::Map;
|
||||
use nym_coconut_dkg_common::types::{ContractSafeBytes, TOTAL_DEALINGS};
|
||||
use crate::error::ContractError;
|
||||
use cosmwasm_std::{Addr, Order, Record, StdResult, Storage};
|
||||
use cw_storage_plus::{Bound, Key, KeyDeserialize, Map, Path, Prefix, Prefixer, PrimaryKey};
|
||||
use nym_coconut_dkg_common::dealing::{DealingMetadata, PartialContractDealing};
|
||||
use nym_coconut_dkg_common::types::{
|
||||
ChunkIndex, ContractSafeBytes, DealingIndex, EpochId, PartialContractDealingData,
|
||||
};
|
||||
|
||||
pub(crate) const DEALINGS_PAGE_MAX_LIMIT: u32 = 2;
|
||||
pub(crate) const DEALINGS_PAGE_DEFAULT_LIMIT: u32 = 1;
|
||||
type Dealer<'a> = &'a Addr;
|
||||
|
||||
type DealingKey<'a> = &'a Addr;
|
||||
/// Metadata for a dealing for given `EpochId`, submitted by particular `Dealer` for given `DealingIndex`.
|
||||
pub(crate) const DEALINGS_METADATA: Map<(EpochId, Dealer, DealingIndex), DealingMetadata> =
|
||||
Map::new("dealings_metadata");
|
||||
|
||||
// Note to whoever is looking at this implementation and is thinking of using something similar
|
||||
// for storing small commitments/hashes of data on chain:
|
||||
// If there's a lot of entries you want to store thinking, "oh, this digest is only 32 bytes, it's not that much",
|
||||
// the default cosmwasm' serializer will bloat it to around ~100B. So you really don't want to be using
|
||||
// Buckets/Maps, etc. for that purpose. Instead you want to use `storage` directly (look into the actual implementation of
|
||||
// `Map` or `Bucket` to see what I mean. Instead of using the `to_vec` method on serde_json_wasm, you'd
|
||||
// provide your data directly yourself.
|
||||
// but you must be extremely careful when doing so, as you might end up overwriting some existing data
|
||||
// if you don't choose your prefixes wisely.
|
||||
// I didn't have to do it here as I'm storing relatively little data and after just base58-encoding
|
||||
// my bytes, I was fine with the json overhead.
|
||||
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))
|
||||
}
|
||||
|
||||
// if TOTAL_DEALINGS is modified to anything other then current value (5), this part will also need
|
||||
// to be modified
|
||||
pub(crate) const DEALINGS_BYTES: [Map<'_, DealingKey<'_>, ContractSafeBytes>; TOTAL_DEALINGS] = [
|
||||
Map::new("dbyt1"),
|
||||
Map::new("dbyt2"),
|
||||
Map::new("dbyt3"),
|
||||
Map::new("dbyt4"),
|
||||
Map::new("dbyt5"),
|
||||
];
|
||||
pub(crate) fn may_read_metadata(
|
||||
storage: &dyn Storage,
|
||||
epoch_id: EpochId,
|
||||
dealer: Dealer,
|
||||
dealing_index: DealingIndex,
|
||||
) -> Result<Option<DealingMetadata>, 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<DealingMetadata, ContractError> {
|
||||
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 data is stored in a multilevel map with the following hierarchy:
|
||||
// - epoch-id:
|
||||
// - issuer-address:
|
||||
// - dealing id:
|
||||
// - 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;
|
||||
|
||||
impl StoredDealing {
|
||||
const NAMESPACE: &'static [u8] = b"dealing";
|
||||
|
||||
fn deserialize_dealing_record(
|
||||
kv: Record,
|
||||
) -> StdResult<(ChunkIndex, PartialContractDealingData)> {
|
||||
let (k, v) = kv;
|
||||
let index = <ChunkIndex as KeyDeserialize>::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<Vec<u8>> {
|
||||
// just replicate the behaviour from `Map::key`
|
||||
// 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::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn prefix(
|
||||
prefix: (EpochId, Dealer, DealingIndex),
|
||||
) -> Prefix<ChunkIndex, PartialContractDealingData, ChunkIndex> {
|
||||
Prefix::with_deserialization_functions(
|
||||
Self::NAMESPACE,
|
||||
&prefix.prefix(),
|
||||
&[],
|
||||
// explicitly panic to make sure we're never attempting to call an unexpected deserializer on our data
|
||||
|_, _, kv| Self::deserialize_dealing_record(kv),
|
||||
|_, _, _| panic!("attempted to call custom de_fn_v"),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn exists(
|
||||
storage: &dyn Storage,
|
||||
epoch_id: EpochId,
|
||||
dealer: &Addr,
|
||||
dealing_index: DealingIndex,
|
||||
chunk_index: ChunkIndex,
|
||||
) -> StdResult<bool> {
|
||||
// whenever the dealing is saved, the metadata is appropriately updated
|
||||
// reading metadata is way cheaper than the dealing chunk itself
|
||||
let Some(metadata) =
|
||||
DEALINGS_METADATA.may_load(storage, (epoch_id, dealer, dealing_index))?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let Some(chunk_info) = metadata.submitted_chunks.get(&chunk_index) else {
|
||||
return Ok(false);
|
||||
};
|
||||
Ok(chunk_info.status.submitted())
|
||||
// StoredDealing::storage_key(epoch_id, dealer, dealing_index).has(storage)
|
||||
}
|
||||
|
||||
pub(crate) fn save(
|
||||
storage: &mut dyn Storage,
|
||||
epoch_id: EpochId,
|
||||
dealer: Dealer,
|
||||
dealng_chunk: PartialContractDealing,
|
||||
) {
|
||||
// NOTE: we're storing bytes directly here!
|
||||
let storage_key = Self::storage_key(
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealng_chunk.dealing_index,
|
||||
dealng_chunk.chunk_index,
|
||||
);
|
||||
storage.set(&storage_key, dealng_chunk.data.as_slice());
|
||||
}
|
||||
|
||||
pub(crate) fn read(
|
||||
storage: &dyn Storage,
|
||||
epoch_id: EpochId,
|
||||
dealer: Dealer,
|
||||
dealing_index: DealingIndex,
|
||||
chunk_index: ChunkIndex,
|
||||
) -> Option<PartialContractDealingData> {
|
||||
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, DealingIndex),
|
||||
start: Option<Bound<ChunkIndex>>,
|
||||
) -> impl Iterator<Item = StdResult<PartialContractDealing>> + 'a {
|
||||
let dealing_index = prefix.2;
|
||||
Self::prefix(prefix)
|
||||
.range(storage, start, None, Order::Ascending)
|
||||
.map(move |maybe_record| {
|
||||
maybe_record.map(|(chunk_index, data)| PartialContractDealing {
|
||||
dealing_index,
|
||||
chunk_index,
|
||||
data,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// iterate over all values, only to be used in tests due to the amount of data being returned
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) fn unchecked_all_entries(
|
||||
storage: &dyn Storage,
|
||||
) -> Vec<(
|
||||
(EpochId, Addr, (DealingIndex, ChunkIndex)),
|
||||
PartialContractDealingData,
|
||||
)> {
|
||||
type StorageKey<'a> = (EpochId, Dealer<'a>, (DealingIndex, ChunkIndex));
|
||||
|
||||
let empty_prefix: Prefix<StorageKey, PartialContractDealingData, StorageKey> =
|
||||
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::<StdResult<_>>()
|
||||
.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,
|
||||
chunk_index: ChunkIndex,
|
||||
) -> PartialContractDealingData {
|
||||
ContractSafeBytes(
|
||||
format!("{epoch_id},{dealer},{dealing_index},{chunk_index}")
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saving_dealing_chunks() {
|
||||
let mut deps = init_contract();
|
||||
|
||||
fn exists_in_storage(
|
||||
storage: &dyn Storage,
|
||||
epoch_id: EpochId,
|
||||
dealer: Dealer,
|
||||
dealing_index: DealingIndex,
|
||||
chunk_index: ChunkIndex,
|
||||
) -> bool {
|
||||
StoredDealing::storage_key(epoch_id, dealer, dealing_index, chunk_index).has(storage)
|
||||
}
|
||||
|
||||
// make sure to check all combinations of epoch id, dealer address and dealing index to ensure nothing overlaps
|
||||
let epochs = [54, 423, 754];
|
||||
let dealers = [
|
||||
Addr::unchecked("dealer1"),
|
||||
Addr::unchecked("dealer2"),
|
||||
Addr::unchecked("dealer3"),
|
||||
Addr::unchecked("dealer4"),
|
||||
Addr::unchecked("dealer5"),
|
||||
];
|
||||
let dealing_indices = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
let chunk_indices = [0, 1, 2, 3, 4];
|
||||
|
||||
for epoch_id in &epochs {
|
||||
for dealer in &dealers {
|
||||
for dealing_index in &dealing_indices {
|
||||
for chunk_index in &chunk_indices {
|
||||
assert!(!exists_in_storage(
|
||||
&deps.storage,
|
||||
*epoch_id,
|
||||
dealer,
|
||||
*dealing_index,
|
||||
*chunk_index
|
||||
));
|
||||
|
||||
StoredDealing::save(
|
||||
deps.as_mut().storage,
|
||||
*epoch_id,
|
||||
dealer,
|
||||
PartialContractDealing {
|
||||
dealing_index: *dealing_index,
|
||||
chunk_index: *chunk_index,
|
||||
data: dealing_data(*epoch_id, dealer, *dealing_index, *chunk_index),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let all: HashMap<_, _> = StoredDealing::unchecked_all_entries(&deps.storage)
|
||||
.into_iter()
|
||||
.collect();
|
||||
assert_eq!(
|
||||
all.len(),
|
||||
epochs.len() * dealers.len() * dealing_indices.len() * chunk_indices.len()
|
||||
);
|
||||
|
||||
for epoch_id in &epochs {
|
||||
for dealer in &dealers {
|
||||
for dealing_index in &dealing_indices {
|
||||
for chunk_index in &chunk_indices {
|
||||
assert!(exists_in_storage(
|
||||
&deps.storage,
|
||||
*epoch_id,
|
||||
dealer,
|
||||
*dealing_index,
|
||||
*chunk_index
|
||||
));
|
||||
|
||||
let content = StoredDealing::read(
|
||||
&deps.storage,
|
||||
*epoch_id,
|
||||
dealer,
|
||||
*dealing_index,
|
||||
*chunk_index,
|
||||
)
|
||||
.unwrap();
|
||||
let expected =
|
||||
dealing_data(*epoch_id, dealer, *dealing_index, *chunk_index);
|
||||
assert_eq!(expected, content);
|
||||
assert_eq!(
|
||||
&expected,
|
||||
all.get(&(*epoch_id, dealer.clone(), (*dealing_index, *chunk_index)))
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterating_over_dealing_chunks() {
|
||||
let mut deps = init_contract();
|
||||
|
||||
let epochs = [54, 423, 754];
|
||||
let dealers = [
|
||||
Addr::unchecked("dealer1"),
|
||||
Addr::unchecked("dealer2"),
|
||||
Addr::unchecked("dealer3"),
|
||||
Addr::unchecked("dealer4"),
|
||||
Addr::unchecked("dealer5"),
|
||||
];
|
||||
let dealing_indices = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
let chunk_indices = [0, 1, 2, 3, 4];
|
||||
|
||||
for epoch_id in &epochs {
|
||||
for dealer in &dealers {
|
||||
for dealing_index in &dealing_indices {
|
||||
for chunk_index in &chunk_indices {
|
||||
StoredDealing::save(
|
||||
deps.as_mut().storage,
|
||||
*epoch_id,
|
||||
dealer,
|
||||
PartialContractDealing {
|
||||
dealing_index: *dealing_index,
|
||||
chunk_index: *chunk_index,
|
||||
data: dealing_data(*epoch_id, dealer, *dealing_index, *chunk_index),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remember, we're not testing the iterator implementation
|
||||
|
||||
// nothing under epoch 0
|
||||
let dealings =
|
||||
StoredDealing::prefix_range(&deps.storage, (0, &dealers[0], dealing_indices[0]), None)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(dealings.is_empty());
|
||||
|
||||
// nothing for dealer "foo"
|
||||
let foo = Addr::unchecked("foo");
|
||||
let dealings =
|
||||
StoredDealing::prefix_range(&deps.storage, (epochs[0], &foo, dealing_indices[0]), None)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(dealings.is_empty());
|
||||
|
||||
// nothing for dealing index 99
|
||||
let dealings =
|
||||
StoredDealing::prefix_range(&deps.storage, (epochs[0], &dealers[0], 99), None)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(dealings.is_empty());
|
||||
|
||||
let all = StoredDealing::prefix_range(
|
||||
&deps.storage,
|
||||
(epochs[0], &dealers[0], dealing_indices[0]),
|
||||
None,
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(all.len(), chunk_indices.len());
|
||||
|
||||
for (i, dealing) in all.iter().enumerate() {
|
||||
let expected =
|
||||
dealing_data(epochs[0], &dealers[0], dealing_indices[0], chunk_indices[i]);
|
||||
assert_eq!(expected, dealing.as_ref().unwrap().data);
|
||||
assert_eq!(chunk_indices[i], dealing.as_ref().unwrap().chunk_index);
|
||||
}
|
||||
|
||||
// for sanity sake, check another dealer with different epoch and different dealing index
|
||||
let all_other = StoredDealing::prefix_range(
|
||||
&deps.storage,
|
||||
(epochs[2], &dealers[3], dealing_indices[4]),
|
||||
None,
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(all_other.len(), chunk_indices.len());
|
||||
|
||||
for (i, dealing) in all_other.iter().enumerate() {
|
||||
let expected =
|
||||
dealing_data(epochs[2], &dealers[3], dealing_indices[4], chunk_indices[i]);
|
||||
assert_eq!(expected, dealing.as_ref().unwrap().data);
|
||||
assert_eq!(chunk_indices[i], dealing.as_ref().unwrap().chunk_index);
|
||||
}
|
||||
|
||||
let without_first = StoredDealing::prefix_range(
|
||||
&deps.storage,
|
||||
(epochs[0], &dealers[0], dealing_indices[0]),
|
||||
Some(Bound::exclusive(chunk_indices[0])),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(&all[1..], without_first);
|
||||
|
||||
let mid = StoredDealing::prefix_range(
|
||||
&deps.storage,
|
||||
(epochs[0], &dealers[0], dealing_indices[0]),
|
||||
Some(Bound::inclusive(chunk_indices[3])),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(&all[3..], mid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,142 +1,346 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::dealers::storage as dealers_storage;
|
||||
use crate::dealings::storage::DEALINGS_BYTES;
|
||||
use crate::epoch_state::storage::INITIAL_REPLACEMENT_DATA;
|
||||
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 cosmwasm_std::{DepsMut, MessageInfo, Response};
|
||||
use nym_coconut_dkg_common::types::{ContractSafeBytes, EpochState};
|
||||
use crate::state::storage::STATE;
|
||||
use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, Storage};
|
||||
use nym_coconut_dkg_common::dealing::{
|
||||
DealingChunkInfo, DealingMetadata, PartialContractDealing, MAX_DEALING_CHUNKS,
|
||||
};
|
||||
use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochState};
|
||||
|
||||
pub fn try_commit_dealings(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
dealing_bytes: ContractSafeBytes,
|
||||
// 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<Response, ContractError> {
|
||||
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(())
|
||||
}
|
||||
|
||||
// check if this dealer has already committed to all dealings
|
||||
// (we don't want to allow overwriting anything)
|
||||
for dealings in DEALINGS_BYTES {
|
||||
if !dealings.has(deps.storage, &info.sender) {
|
||||
dealings.save(deps.storage, &info.sender, &dealing_bytes)?;
|
||||
return Ok(Response::default());
|
||||
pub fn try_submit_dealings_metadata(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
dealing_index: DealingIndex,
|
||||
chunks: Vec<DealingChunkInfo>,
|
||||
resharing: bool,
|
||||
) -> Result<Response, ContractError> {
|
||||
ensure_permission(deps.storage, &info.sender, resharing)?;
|
||||
|
||||
let state = STATE.load(deps.storage)?;
|
||||
let epoch = CURRENT_EPOCH.load(deps.storage)?;
|
||||
|
||||
// don't allow overwriting existing metadata
|
||||
if metadata_exists(deps.storage, epoch.epoch_id, &info.sender, dealing_index) {
|
||||
return Err(ContractError::MetadataAlreadyExists {
|
||||
epoch_id: epoch.epoch_id,
|
||||
dealer: info.sender,
|
||||
dealing_index,
|
||||
});
|
||||
}
|
||||
|
||||
// make sure the dealing index is in the allowed range
|
||||
// note: dealing indexing starts from 0
|
||||
if dealing_index >= state.key_size {
|
||||
return Err(ContractError::DealingOutOfRange {
|
||||
epoch_id: epoch.epoch_id,
|
||||
dealer: info.sender,
|
||||
index: dealing_index,
|
||||
key_size: state.key_size,
|
||||
});
|
||||
}
|
||||
|
||||
// make sure the metadata is not empty
|
||||
if chunks.is_empty() {
|
||||
return Err(ContractError::EmptyMetadata {
|
||||
epoch_id: epoch.epoch_id,
|
||||
dealer: info.sender,
|
||||
dealing_index,
|
||||
});
|
||||
}
|
||||
|
||||
// make sure the chunks are non empty
|
||||
if chunks.iter().any(|c| c.size == 0) {
|
||||
return Err(ContractError::EmptyMetadata {
|
||||
epoch_id: epoch.epoch_id,
|
||||
dealer: info.sender,
|
||||
dealing_index,
|
||||
});
|
||||
}
|
||||
|
||||
// make sure the number of dealing chunks is in the allowed range
|
||||
// to prevent somebody splitting their dealings into 10B chunks
|
||||
if chunks.len() > MAX_DEALING_CHUNKS {
|
||||
return Err(ContractError::TooFragmentedMetadata {
|
||||
epoch_id: epoch.epoch_id,
|
||||
dealer: info.sender,
|
||||
dealing_index,
|
||||
chunks: chunks.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// make sure all chunks, but the last one, have the same size
|
||||
// SAFETY: we checked for whether `chunks` is empty and returned an error in that case
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let first_chunk_size = chunks.first().unwrap().size;
|
||||
|
||||
for (chunk_index, chunk_info) in chunks.iter().enumerate().take(chunks.len() - 1) {
|
||||
if chunk_info.size != first_chunk_size {
|
||||
return Err(ContractError::UnevenChunkSplit {
|
||||
epoch_id: epoch.epoch_id,
|
||||
dealer: info.sender,
|
||||
dealing_index,
|
||||
chunk_index: chunk_index as ChunkIndex,
|
||||
first_chunk_size,
|
||||
size: chunk_info.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(ContractError::AlreadyCommitted {
|
||||
commitment: String::from("dealing"),
|
||||
})
|
||||
// finally, construct and store the metadata
|
||||
let metadata = DealingMetadata::new(dealing_index, chunks);
|
||||
|
||||
store_metadata(
|
||||
deps.storage,
|
||||
epoch.epoch_id,
|
||||
&info.sender,
|
||||
dealing_index,
|
||||
&metadata,
|
||||
)?;
|
||||
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
pub fn try_commit_dealings_chunk(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
chunk: PartialContractDealing,
|
||||
resharing: bool,
|
||||
) -> Result<Response, ContractError> {
|
||||
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,
|
||||
chunk.dealing_index,
|
||||
)?;
|
||||
|
||||
// check if the received chunk is within the declared range
|
||||
let Some(submission_status) = metadata.submitted_chunks.get_mut(&chunk.chunk_index) else {
|
||||
return Err(ContractError::DealingChunkNotInMetadata {
|
||||
epoch_id: epoch.epoch_id,
|
||||
dealer: info.sender,
|
||||
dealing_index: chunk.dealing_index,
|
||||
chunk_index: chunk.chunk_index,
|
||||
});
|
||||
};
|
||||
|
||||
// check if this dealer has already committed this particular dealing chunk
|
||||
if let Some(submission_height) = submission_status.status.submission_height {
|
||||
return Err(ContractError::DealingChunkAlreadyCommitted {
|
||||
epoch_id: epoch.epoch_id,
|
||||
dealer: info.sender,
|
||||
dealing_index: chunk.dealing_index,
|
||||
chunk_index: chunk.chunk_index,
|
||||
block_height: submission_height,
|
||||
});
|
||||
}
|
||||
|
||||
// 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.status.submission_height = Some(env.block.height);
|
||||
store_metadata(
|
||||
deps.storage,
|
||||
epoch.epoch_id,
|
||||
&info.sender,
|
||||
chunk.dealing_index,
|
||||
&metadata,
|
||||
)?;
|
||||
|
||||
// store the dealing
|
||||
StoredDealing::save(deps.storage, epoch.epoch_id, &info.sender, chunk);
|
||||
|
||||
Ok(Response::new())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::epoch_state::storage::CURRENT_EPOCH;
|
||||
use crate::epoch_state::transactions::advance_epoch_state;
|
||||
use crate::support::tests::fixtures::{dealer_details_fixture, dealing_bytes_fixture};
|
||||
use crate::epoch_state::transactions::{advance_epoch_state, try_initiate_dkg};
|
||||
use crate::support::tests::fixtures::{dealer_details_fixture, partial_dealing_fixture};
|
||||
use crate::support::tests::helpers;
|
||||
use crate::support::tests::helpers::add_fixture_dealer;
|
||||
use crate::support::tests::helpers::{add_fixture_dealer, ADMIN_ADDRESS};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration};
|
||||
use nym_coconut_dkg_common::types::{
|
||||
ContractSafeBytes, InitialReplacementData, TimeConfiguration,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn invalid_commit_dealing() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let owner = Addr::unchecked("owner1");
|
||||
let mut env = mock_env();
|
||||
let info = mock_info(owner.as_str(), &[]);
|
||||
let dealing_bytes = dealing_bytes_fixture();
|
||||
|
||||
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), false)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
ret,
|
||||
ContractError::IncorrectEpochState {
|
||||
current_state: EpochState::default().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_bytes.clone(), false)
|
||||
.unwrap_err();
|
||||
assert_eq!(ret, ContractError::NotADealer);
|
||||
|
||||
let dealer_details = DealerDetails {
|
||||
address: owner.clone(),
|
||||
bte_public_key_with_proof: String::new(),
|
||||
announce_address: String::new(),
|
||||
assigned_index: 1,
|
||||
};
|
||||
dealers_storage::current_dealers()
|
||||
.save(deps.as_mut().storage, &owner, &dealer_details)
|
||||
.unwrap();
|
||||
|
||||
// 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_bytes.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();
|
||||
|
||||
for dealings in DEALINGS_BYTES {
|
||||
assert!(!dealings.has(deps.as_mut().storage, &owner));
|
||||
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), true);
|
||||
assert!(ret.is_ok());
|
||||
assert!(dealings.has(deps.as_mut().storage, &owner));
|
||||
}
|
||||
let ret = try_commit_dealings(deps.as_mut(), info, dealing_bytes, true).unwrap_err();
|
||||
assert_eq!(
|
||||
ret,
|
||||
ContractError::AlreadyCommitted {
|
||||
commitment: String::from("dealing"),
|
||||
}
|
||||
);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,22 +27,29 @@ pub(crate) fn query_initial_dealers(
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use super::*;
|
||||
use crate::support::tests::helpers::init_contract;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use crate::epoch_state::transactions::try_initiate_dkg;
|
||||
use crate::support::tests::helpers::{init_contract, ADMIN_ADDRESS};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use nym_coconut_dkg_common::types::{EpochState, TimeConfiguration};
|
||||
|
||||
#[test]
|
||||
fn query_state() {
|
||||
let mut deps = init_contract();
|
||||
let epoch = query_current_epoch(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(epoch.state, EpochState::WaitingInitialisation);
|
||||
assert_eq!(epoch.finish_timestamp, None);
|
||||
|
||||
let env = mock_env();
|
||||
try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap();
|
||||
|
||||
let epoch = query_current_epoch(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(
|
||||
epoch.state,
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
mock_env()
|
||||
.block
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block
|
||||
.time
|
||||
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs)
|
||||
);
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::dealers::storage::{current_dealers, past_dealers};
|
||||
use crate::dealings::storage::DEALINGS_BYTES;
|
||||
use crate::epoch_state::storage::{CURRENT_EPOCH, INITIAL_REPLACEMENT_DATA, THRESHOLD};
|
||||
use crate::epoch_state::utils::check_epoch_state;
|
||||
use crate::error::ContractError;
|
||||
use crate::state::STATE;
|
||||
use crate::state::storage::{DKG_ADMIN, STATE};
|
||||
use crate::verification_key_shares::storage::verified_dealers;
|
||||
use cosmwasm_std::{Addr, Deps, DepsMut, Env, Order, Response, Storage};
|
||||
use cosmwasm_std::{Addr, Deps, DepsMut, Env, MessageInfo, Order, Response, Storage};
|
||||
use nym_coconut_dkg_common::types::{Epoch, EpochState, InitialReplacementData};
|
||||
|
||||
fn reset_epoch_state(storage: &mut dyn Storage) -> Result<(), ContractError> {
|
||||
fn reset_dkg_state(storage: &mut dyn Storage) -> Result<(), ContractError> {
|
||||
THRESHOLD.remove(storage);
|
||||
let dealers: Vec<_> = current_dealers()
|
||||
.keys(storage, None, None, Order::Ascending)
|
||||
@@ -19,15 +18,6 @@ fn reset_epoch_state(storage: &mut dyn Storage) -> Result<(), ContractError> {
|
||||
|
||||
for dealer_addr in dealers {
|
||||
let details = current_dealers().load(storage, &dealer_addr)?;
|
||||
for dealings in DEALINGS_BYTES {
|
||||
let dealing_keys: Vec<_> = dealings
|
||||
.keys(storage, None, None, Order::Ascending)
|
||||
.flatten()
|
||||
.collect();
|
||||
for key in dealing_keys {
|
||||
dealings.remove(storage, &key);
|
||||
}
|
||||
}
|
||||
current_dealers().remove(storage, &dealer_addr)?;
|
||||
past_dealers().save(storage, &dealer_addr, &details)?;
|
||||
}
|
||||
@@ -80,18 +70,43 @@ fn replacement_threshold_surpassed(deps: &DepsMut<'_>) -> Result<bool, ContractE
|
||||
Ok(removed_dealer_count >= replacement_threshold)
|
||||
}
|
||||
|
||||
pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Response, ContractError> {
|
||||
pub(crate) fn try_initiate_dkg(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
) -> Result<Response, ContractError> {
|
||||
// only the admin is allowed to kick start the process
|
||||
DKG_ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
|
||||
|
||||
let epoch = CURRENT_EPOCH.load(deps.storage)?;
|
||||
if epoch.finish_timestamp > env.block.time {
|
||||
return Err(ContractError::EarlyEpochStateAdvancement(
|
||||
epoch
|
||||
.finish_timestamp
|
||||
.minus_seconds(env.block.time.seconds())
|
||||
.seconds(),
|
||||
));
|
||||
if !matches!(epoch.state, EpochState::WaitingInitialisation) {
|
||||
return Err(ContractError::AlreadyInitialised);
|
||||
}
|
||||
|
||||
// the first exchange won't involve resharing
|
||||
let initial_state = EpochState::PublicKeySubmission { resharing: false };
|
||||
let initial_epoch = Epoch::new(initial_state, 0, epoch.time_configuration, env.block.time);
|
||||
CURRENT_EPOCH.save(deps.storage, &initial_epoch)?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Response, ContractError> {
|
||||
let current_epoch = CURRENT_EPOCH.load(deps.storage)?;
|
||||
if current_epoch.state == EpochState::WaitingInitialisation {
|
||||
return Err(ContractError::WaitingInitialisation);
|
||||
}
|
||||
|
||||
if let Some(finish_timestamp) = current_epoch.finish_timestamp {
|
||||
if finish_timestamp > env.block.time {
|
||||
return Err(ContractError::EarlyEpochStateAdvancement(
|
||||
finish_timestamp
|
||||
.minus_seconds(env.block.time.seconds())
|
||||
.seconds(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let next_epoch = if let Some(state) = current_epoch.state.next() {
|
||||
// We are during DKG process
|
||||
let mut new_state = state;
|
||||
@@ -123,6 +138,8 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
|
||||
} else if dealers_eq_members(&deps)? {
|
||||
// The dealer set hasn't changed, so we only extend the finish timestamp
|
||||
// The epoch remains the same, as we use it as key for storing VKs
|
||||
|
||||
// TODO: change that behaviour in the following PR
|
||||
Epoch::new(
|
||||
current_epoch.state,
|
||||
current_epoch.epoch_id,
|
||||
@@ -139,6 +156,7 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
|
||||
// ... in reshare mode
|
||||
if INITIAL_REPLACEMENT_DATA.may_load(deps.storage)?.is_some() {
|
||||
INITIAL_REPLACEMENT_DATA.update::<_, ContractError>(deps.storage, |mut data| {
|
||||
// TODO: FIXME: for second reshare the added set of dealers won't be allowed to participate
|
||||
data.initial_height = env.block.height;
|
||||
Ok(data)
|
||||
})?;
|
||||
@@ -152,7 +170,7 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
|
||||
|
||||
EpochState::PublicKeySubmission { resharing: true }
|
||||
};
|
||||
reset_epoch_state(deps.storage)?;
|
||||
reset_dkg_state(deps.storage)?;
|
||||
Epoch::new(
|
||||
state,
|
||||
current_epoch.epoch_id + 1,
|
||||
@@ -174,7 +192,7 @@ pub(crate) fn try_surpassed_threshold(
|
||||
let threshold = THRESHOLD.load(deps.storage)?;
|
||||
let dealers = verified_dealers(deps.storage)?;
|
||||
if dealers_still_active(&deps.as_ref(), dealers.into_iter())? < threshold as usize {
|
||||
reset_epoch_state(deps.storage)?;
|
||||
reset_dkg_state(deps.storage)?;
|
||||
CURRENT_EPOCH.update::<_, ContractError>(deps.storage, |epoch| {
|
||||
Ok(Epoch::new(
|
||||
EpochState::default(),
|
||||
@@ -193,20 +211,19 @@ pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::error::ContractError::EarlyEpochStateAdvancement;
|
||||
use crate::support::tests::fixtures::{dealer_details_fixture, vk_share_fixture};
|
||||
use crate::support::tests::helpers::{init_contract, GROUP_MEMBERS};
|
||||
use crate::support::tests::helpers::{init_contract, ADMIN_ADDRESS, GROUP_MEMBERS};
|
||||
use crate::verification_key_shares::storage::vk_shares;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::Addr;
|
||||
use cw4::Member;
|
||||
use nym_coconut_dkg_common::types::{
|
||||
ContractSafeBytes, DealerDetails, EpochState, TimeConfiguration,
|
||||
};
|
||||
use cw_controllers::AdminError;
|
||||
use nym_coconut_dkg_common::types::{DealerDetails, EpochState, TimeConfiguration};
|
||||
use rusty_fork::rusty_fork_test;
|
||||
|
||||
// Because of the global variable handling group, we need individual process for each test
|
||||
|
||||
rusty_fork_test! {
|
||||
// Using values from the DKG document
|
||||
// Using values from the DKG document
|
||||
#[test]
|
||||
fn threshold_surpassed() {
|
||||
let mut deps = init_contract();
|
||||
@@ -217,14 +234,18 @@ pub(crate) mod tests {
|
||||
|
||||
for n in [10, 25, 50, 100] {
|
||||
let dealers: Vec<_> = (0..n).map(dealer_details_fixture).collect();
|
||||
let shares: Vec<_> = (0..n).map(|idx| vk_share_fixture(&format!("owner{}", idx), 0)).collect();
|
||||
let shares: Vec<_> = (0..n)
|
||||
.map(|idx| vk_share_fixture(&format!("owner{}", idx), 0))
|
||||
.collect();
|
||||
let initial_dealers = dealers.iter().map(|d| d.address.clone()).collect();
|
||||
let data = InitialReplacementData {
|
||||
initial_dealers,
|
||||
initial_height: 1,
|
||||
};
|
||||
for share in shares {
|
||||
vk_shares().save(deps.as_mut().storage, (&share.owner, 0), &share).unwrap();
|
||||
vk_shares()
|
||||
.save(deps.as_mut().storage, (&share.owner, 0), &share)
|
||||
.unwrap();
|
||||
}
|
||||
for f in [two_thirds, three_fourths, ninty_pc] {
|
||||
let threshold = f(n);
|
||||
@@ -361,7 +382,8 @@ pub(crate) mod tests {
|
||||
fn advance_state() {
|
||||
let mut deps = init_contract();
|
||||
let mut env = mock_env();
|
||||
{
|
||||
|
||||
{
|
||||
let mut group = GROUP_MEMBERS.lock().unwrap();
|
||||
|
||||
group.push((
|
||||
@@ -394,13 +416,21 @@ pub(crate) mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
// can't advance the state if dkg hasn't been initiated
|
||||
assert_eq!(
|
||||
advance_epoch_state(deps.as_mut(), env.clone()).unwrap_err(),
|
||||
ContractError::WaitingInitialisation
|
||||
);
|
||||
|
||||
try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap();
|
||||
|
||||
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(
|
||||
epoch.state,
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block
|
||||
.time
|
||||
.plus_seconds(epoch.time_configuration.public_key_submission_time_secs)
|
||||
@@ -424,7 +454,8 @@ pub(crate) mod tests {
|
||||
);
|
||||
|
||||
// setup dealer details
|
||||
let all_shares: [_; 4] = std::array::from_fn(|i| vk_share_fixture(&format!("owner{}", i + 1), 0));
|
||||
let all_shares: [_; 4] =
|
||||
std::array::from_fn(|i| vk_share_fixture(&format!("owner{}", i + 1), 0));
|
||||
for share in all_shares.iter() {
|
||||
vk_shares()
|
||||
.save(deps.as_mut().storage, (&share.owner, 0), share)
|
||||
@@ -441,7 +472,10 @@ pub(crate) mod tests {
|
||||
.may_load(&deps.storage)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
env.block.time = env.block.time.plus_seconds(epoch.time_configuration.public_key_submission_time_secs);
|
||||
env.block.time = env
|
||||
.block
|
||||
.time
|
||||
.plus_seconds(epoch.time_configuration.public_key_submission_time_secs);
|
||||
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
|
||||
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(
|
||||
@@ -449,7 +483,7 @@ pub(crate) mod tests {
|
||||
EpochState::DealingExchange { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block
|
||||
.time
|
||||
.plus_seconds(epoch.time_configuration.dealing_exchange_time_secs)
|
||||
@@ -472,7 +506,7 @@ pub(crate) mod tests {
|
||||
EpochState::VerificationKeySubmission { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block.time.plus_seconds(
|
||||
epoch
|
||||
.time_configuration
|
||||
@@ -499,7 +533,7 @@ pub(crate) mod tests {
|
||||
EpochState::VerificationKeyValidation { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block.time.plus_seconds(
|
||||
epoch
|
||||
.time_configuration
|
||||
@@ -526,7 +560,7 @@ pub(crate) mod tests {
|
||||
EpochState::VerificationKeyFinalization { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block.time.plus_seconds(
|
||||
epoch
|
||||
.time_configuration
|
||||
@@ -548,7 +582,7 @@ pub(crate) mod tests {
|
||||
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(epoch.state, EpochState::InProgress);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block
|
||||
.time
|
||||
.plus_seconds(epoch.time_configuration.in_progress_time_secs)
|
||||
@@ -614,7 +648,9 @@ pub(crate) mod tests {
|
||||
|
||||
let all_details: [_; 4] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 2));
|
||||
for details in all_details.iter() {
|
||||
past_dealers().remove(deps.as_mut().storage, &details.address).unwrap();
|
||||
past_dealers()
|
||||
.remove(deps.as_mut().storage, &details.address)
|
||||
.unwrap();
|
||||
current_dealers()
|
||||
.save(deps.as_mut().storage, &details.address, details)
|
||||
.unwrap();
|
||||
@@ -622,9 +658,15 @@ pub(crate) mod tests {
|
||||
for times in [
|
||||
epoch.time_configuration.public_key_submission_time_secs,
|
||||
epoch.time_configuration.dealing_exchange_time_secs,
|
||||
epoch.time_configuration.verification_key_submission_time_secs,
|
||||
epoch.time_configuration.verification_key_validation_time_secs,
|
||||
epoch.time_configuration.verification_key_finalization_time_secs,
|
||||
epoch
|
||||
.time_configuration
|
||||
.verification_key_submission_time_secs,
|
||||
epoch
|
||||
.time_configuration
|
||||
.verification_key_validation_time_secs,
|
||||
epoch
|
||||
.time_configuration
|
||||
.verification_key_finalization_time_secs,
|
||||
] {
|
||||
env.block.time = env.block.time.plus_seconds(times);
|
||||
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
|
||||
@@ -634,7 +676,7 @@ pub(crate) mod tests {
|
||||
let mut share = vk_share_fixture(&format!("owner{}", i + 1), 1);
|
||||
share.verified = i % 2 == 0;
|
||||
share
|
||||
});
|
||||
});
|
||||
for share in all_shares.iter() {
|
||||
vk_shares()
|
||||
.save(deps.as_mut().storage, (&share.owner, 0), share)
|
||||
@@ -670,6 +712,8 @@ pub(crate) mod tests {
|
||||
fn surpass_threshold() {
|
||||
let mut deps = init_contract();
|
||||
let mut env = mock_env();
|
||||
try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap();
|
||||
|
||||
let time_configuration = TimeConfiguration::default();
|
||||
{
|
||||
let mut group = GROUP_MEMBERS.lock().unwrap();
|
||||
@@ -701,13 +745,13 @@ pub(crate) mod tests {
|
||||
assert_eq!(
|
||||
ret,
|
||||
ContractError::IncorrectEpochState {
|
||||
current_state: EpochState::default().to_string(),
|
||||
current_state: EpochState::PublicKeySubmission { resharing: false }.to_string(),
|
||||
expected_state: EpochState::InProgress.to_string()
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
let all_shares: [_; 3] = std::array::from_fn(|i| vk_share_fixture(&format!("owner{}", i + 1), 0));
|
||||
let all_shares: [_; 3] =
|
||||
std::array::from_fn(|i| vk_share_fixture(&format!("owner{}", i + 1), 0));
|
||||
for share in all_shares.iter() {
|
||||
vk_shares()
|
||||
.save(deps.as_mut().storage, (&share.owner, 0), share)
|
||||
@@ -719,7 +763,8 @@ pub(crate) mod tests {
|
||||
.save(deps.as_mut().storage, &details.address, details)
|
||||
.unwrap();
|
||||
}
|
||||
let all_shares: [_; 3] = std::array::from_fn(|i| vk_share_fixture(&format!("owner{}", i + 1), 0));
|
||||
let all_shares: [_; 3] =
|
||||
std::array::from_fn(|i| vk_share_fixture(&format!("owner{}", i + 1), 0));
|
||||
for share in all_shares.iter() {
|
||||
vk_shares()
|
||||
.save(deps.as_mut().storage, (&share.owner, share.epoch_id), share)
|
||||
@@ -778,6 +823,46 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialising_dkg() {
|
||||
let mut deps = init_contract();
|
||||
let env = mock_env();
|
||||
|
||||
let initial_epoch_info = CURRENT_EPOCH.load(&deps.storage).unwrap();
|
||||
assert!(initial_epoch_info.finish_timestamp.is_none());
|
||||
|
||||
// can only be executed by the admin
|
||||
let res = try_initiate_dkg(deps.as_mut(), env.clone(), mock_info("not an admin", &[]))
|
||||
.unwrap_err();
|
||||
assert_eq!(ContractError::Admin(AdminError::NotAdmin {}), res);
|
||||
|
||||
let res = try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[]));
|
||||
assert!(res.is_ok());
|
||||
|
||||
// can't be initialised more than once
|
||||
let res = try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[]))
|
||||
.unwrap_err();
|
||||
assert_eq!(ContractError::AlreadyInitialised, res);
|
||||
|
||||
// sets the correct epoch data
|
||||
let epoch = CURRENT_EPOCH.load(&deps.storage).unwrap();
|
||||
assert_eq!(epoch.epoch_id, 0);
|
||||
assert_eq!(
|
||||
epoch.state,
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.time_configuration,
|
||||
initial_epoch_info.time_configuration
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block
|
||||
.time
|
||||
.plus_seconds(epoch.time_configuration.public_key_submission_time_secs)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_state() {
|
||||
let mut deps = init_contract();
|
||||
@@ -788,27 +873,12 @@ pub(crate) mod tests {
|
||||
current_dealers()
|
||||
.save(deps.as_mut().storage, &details.address, details)
|
||||
.unwrap();
|
||||
for dealings in DEALINGS_BYTES {
|
||||
dealings
|
||||
.save(
|
||||
deps.as_mut().storage,
|
||||
&details.address,
|
||||
&ContractSafeBytes(vec![1, 2, 3]),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
reset_epoch_state(deps.as_mut().storage).unwrap();
|
||||
reset_dkg_state(deps.as_mut().storage).unwrap();
|
||||
|
||||
assert!(THRESHOLD.may_load(&deps.storage).unwrap().is_none());
|
||||
for details in all_details {
|
||||
for dealings in DEALINGS_BYTES {
|
||||
assert!(dealings
|
||||
.may_load(&deps.storage, &details.address)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
assert!(current_dealers()
|
||||
.may_load(deps.as_mut().storage, &details.address)
|
||||
.unwrap()
|
||||
@@ -826,6 +896,7 @@ pub(crate) mod tests {
|
||||
fn verify_threshold() {
|
||||
let mut deps = init_contract();
|
||||
let mut env = mock_env();
|
||||
try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap();
|
||||
|
||||
assert!(THRESHOLD.may_load(deps.as_mut().storage).unwrap().is_none());
|
||||
|
||||
@@ -838,6 +909,7 @@ pub(crate) mod tests {
|
||||
&DealerDetails {
|
||||
address: address.clone(),
|
||||
bte_public_key_with_proof: "bte_public_key_with_proof".to_string(),
|
||||
ed25519_identity: "identity".to_string(),
|
||||
announce_address: "127.0.0.1".to_string(),
|
||||
assigned_index: i,
|
||||
},
|
||||
|
||||
@@ -33,14 +33,14 @@ pub(crate) mod test {
|
||||
let mut deps = init_contract();
|
||||
let env = mock_env();
|
||||
|
||||
for fixed_state in EpochState::default().all_until(EpochState::InProgress) {
|
||||
for fixed_state in EpochState::first().all_until(EpochState::InProgress) {
|
||||
CURRENT_EPOCH
|
||||
.save(
|
||||
deps.as_mut().storage,
|
||||
&Epoch::new(fixed_state, 0, TimeConfiguration::default(), env.block.time),
|
||||
)
|
||||
.unwrap();
|
||||
for against_state in EpochState::default().all_until(EpochState::InProgress) {
|
||||
for against_state in EpochState::first().all_until(EpochState::InProgress) {
|
||||
let ret = check_epoch_state(deps.as_mut().storage, against_state);
|
||||
if fixed_state == against_state {
|
||||
assert!(ret.is_ok());
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::StdError;
|
||||
use cosmwasm_std::{Addr, StdError};
|
||||
use cw_controllers::AdminError;
|
||||
use nym_coconut_dkg_common::dealing::MAX_DEALING_CHUNKS;
|
||||
use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Custom errors for contract failure conditions.
|
||||
@@ -14,6 +16,12 @@ pub enum ContractError {
|
||||
#[error(transparent)]
|
||||
Admin(#[from] AdminError),
|
||||
|
||||
#[error("Dkg hasn't been initialised yet")]
|
||||
WaitingInitialisation,
|
||||
|
||||
#[error("Dkg has already been initialised")]
|
||||
AlreadyInitialised,
|
||||
|
||||
#[error("Group contract invalid address '{addr}'")]
|
||||
InvalidGroup { addr: String },
|
||||
|
||||
@@ -30,7 +38,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,
|
||||
@@ -43,9 +51,89 @@ pub enum ContractError {
|
||||
#[error("This sender is not a dealer for the current resharing epoch")]
|
||||
NotAnInitialDealer,
|
||||
|
||||
#[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,
|
||||
dealing_index: DealingIndex,
|
||||
chunk_index: ChunkIndex,
|
||||
block_height: u64,
|
||||
},
|
||||
|
||||
#[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,
|
||||
index: DealingIndex,
|
||||
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("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,
|
||||
dealing_index: DealingIndex,
|
||||
chunk_index: ChunkIndex,
|
||||
first_chunk_size: usize,
|
||||
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,
|
||||
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 },
|
||||
|
||||
#[error("No verification key committed for owner {owner}")]
|
||||
NoCommitForOwner { owner: String },
|
||||
|
||||
#[error("failed to parse {value} into a valid SemVer version: {error_message}")]
|
||||
SemVerFailure {
|
||||
value: String,
|
||||
error_message: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::Addr;
|
||||
use cw4::Cw4Contract;
|
||||
use cw_controllers::Admin;
|
||||
use cw_storage_plus::Item;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// unique items
|
||||
pub const STATE: Item<State> = Item::new("state");
|
||||
pub const MULTISIG: Admin = Admin::new("multisig");
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||
pub struct State {
|
||||
pub mix_denom: String,
|
||||
pub multisig_addr: Addr,
|
||||
pub group_addr: Cw4Contract,
|
||||
}
|
||||
pub mod queries;
|
||||
pub mod storage;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::state::storage::STATE;
|
||||
use cosmwasm_std::{StdResult, Storage};
|
||||
use nym_coconut_dkg_common::types::State;
|
||||
|
||||
pub(crate) fn query_state(storage: &dyn Storage) -> StdResult<State> {
|
||||
STATE.load(storage)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cw_controllers::Admin;
|
||||
use cw_storage_plus::Item;
|
||||
use nym_coconut_dkg_common::types::State;
|
||||
|
||||
// unique items
|
||||
pub const DKG_ADMIN: Admin = Admin::new("dkg-admin");
|
||||
|
||||
pub const STATE: Item<State> = Item::new("state");
|
||||
|
||||
pub const MULTISIG: Admin = Admin::new("multisig");
|
||||
@@ -1,8 +1,9 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::dealing::PartialContractDealing;
|
||||
use nym_coconut_dkg_common::types::ContractSafeBytes;
|
||||
use nym_coconut_dkg_common::verification_key::ContractVKShare;
|
||||
|
||||
@@ -20,13 +21,22 @@ pub fn vk_share_fixture(owner: &str, index: u64) -> ContractVKShare {
|
||||
}
|
||||
|
||||
pub fn dealing_bytes_fixture() -> ContractSafeBytes {
|
||||
ContractSafeBytes(vec![])
|
||||
ContractSafeBytes(vec![1, 2, 3])
|
||||
}
|
||||
|
||||
pub fn partial_dealing_fixture() -> PartialContractDealing {
|
||||
PartialContractDealing {
|
||||
chunk_index: 0,
|
||||
dealing_index: 0,
|
||||
data: ContractSafeBytes(vec![1, 2, 3]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dealer_details_fixture(assigned_index: u64) -> DealerDetails {
|
||||
DealerDetails {
|
||||
address: Addr::unchecked(format!("owner{}", assigned_index)),
|
||||
bte_public_key_with_proof: "".to_string(),
|
||||
ed25519_identity: "".to_string(),
|
||||
announce_address: "".to_string(),
|
||||
assigned_index,
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use cosmwasm_std::{
|
||||
QuerierResult, SystemResult, WasmQuery,
|
||||
};
|
||||
use cw4::{Cw4QueryMsg, Member, MemberListResponse, MemberResponse};
|
||||
use lazy_static::lazy_static;
|
||||
use nym_coconut_dkg_common::dealing::DEFAULT_DEALINGS;
|
||||
use nym_coconut_dkg_common::msg::InstantiateMsg;
|
||||
use nym_coconut_dkg_common::types::DealerDetails;
|
||||
use std::sync::Mutex;
|
||||
@@ -20,9 +20,7 @@ pub const ADMIN_ADDRESS: &str = "admin address";
|
||||
pub const GROUP_CONTRACT: &str = "group contract address";
|
||||
pub const MULTISIG_CONTRACT: &str = "multisig contract address";
|
||||
|
||||
lazy_static! {
|
||||
pub static ref GROUP_MEMBERS: Mutex<Vec<(Member, u64)>> = Mutex::new(vec![]);
|
||||
}
|
||||
pub(crate) static GROUP_MEMBERS: Mutex<Vec<(Member, u64)>> = Mutex::new(Vec::new());
|
||||
|
||||
pub fn add_fixture_dealer(deps: DepsMut<'_>) {
|
||||
let owner = Addr::unchecked("owner");
|
||||
@@ -33,6 +31,7 @@ pub fn add_fixture_dealer(deps: DepsMut<'_>) {
|
||||
&DealerDetails {
|
||||
address: owner.clone(),
|
||||
bte_public_key_with_proof: String::new(),
|
||||
ed25519_identity: String::new(),
|
||||
announce_address: String::new(),
|
||||
assigned_index: 100,
|
||||
},
|
||||
@@ -87,6 +86,7 @@ pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>>
|
||||
multisig_addr: String::from(MULTISIG_CONTRACT),
|
||||
time_configuration: None,
|
||||
mix_denom: TEST_MIX_DENOM.to_string(),
|
||||
key_size: DEFAULT_DEALINGS as u32,
|
||||
};
|
||||
let env = mock_env();
|
||||
let info = mock_info(ADMIN_ADDRESS, &[]);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::verification_key_shares::storage;
|
||||
@@ -6,7 +6,23 @@ use crate::verification_key_shares::storage::vk_shares;
|
||||
use cosmwasm_std::{Deps, Order, StdResult};
|
||||
use cw_storage_plus::Bound;
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_coconut_dkg_common::verification_key::PagedVKSharesResponse;
|
||||
use nym_coconut_dkg_common::verification_key::{PagedVKSharesResponse, VkShareResponse};
|
||||
|
||||
// TODO: unit tests
|
||||
pub fn query_vk_share(
|
||||
deps: Deps<'_>,
|
||||
owner: String,
|
||||
epoch_id: EpochId,
|
||||
) -> StdResult<VkShareResponse> {
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
let share = vk_shares().may_load(deps.storage, (&owner, epoch_id))?;
|
||||
|
||||
Ok(VkShareResponse {
|
||||
owner,
|
||||
epoch_id,
|
||||
share,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_vk_shares_paged(
|
||||
deps: Deps<'_>,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::constants::{VK_SHARES_EPOCH_ID_IDX_NAMESPACE, VK_SHARES_PK_NAMESPACE};
|
||||
use crate::epoch_state::storage::CURRENT_EPOCH;
|
||||
|
||||
@@ -6,9 +6,9 @@ use crate::dealers::storage as dealers_storage;
|
||||
use crate::epoch_state::storage::CURRENT_EPOCH;
|
||||
use crate::epoch_state::utils::check_epoch_state;
|
||||
use crate::error::ContractError;
|
||||
use crate::state::{MULTISIG, STATE};
|
||||
use crate::state::storage::{MULTISIG, STATE};
|
||||
use crate::verification_key_shares::storage::vk_shares;
|
||||
use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response};
|
||||
use cosmwasm_std::{DepsMut, Env, MessageInfo, Response};
|
||||
use nym_coconut_dkg_common::types::EpochState;
|
||||
use nym_coconut_dkg_common::verification_key::{
|
||||
to_cosmos_msg, ContractVKShare, VerificationKeyShare,
|
||||
@@ -54,6 +54,7 @@ pub fn try_commit_verification_key_share(
|
||||
resharing,
|
||||
env.contract.address.to_string(),
|
||||
STATE.load(deps.storage)?.multisig_addr.to_string(),
|
||||
// TODO: make this value configurable
|
||||
env.block
|
||||
.time
|
||||
.plus_seconds(BLOCK_TIME_FOR_VERIFICATION_SECS),
|
||||
@@ -65,9 +66,11 @@ pub fn try_commit_verification_key_share(
|
||||
pub fn try_verify_verification_key_share(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
owner: Addr,
|
||||
owner: String,
|
||||
resharing: bool,
|
||||
) -> Result<Response, ContractError> {
|
||||
let owner = deps.api.addr_validate(&owner)?;
|
||||
|
||||
check_epoch_state(
|
||||
deps.storage,
|
||||
EpochState::VerificationKeyFinalization { resharing },
|
||||
@@ -91,10 +94,11 @@ pub fn try_verify_verification_key_share(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::epoch_state::transactions::advance_epoch_state;
|
||||
use crate::epoch_state::transactions::{advance_epoch_state, try_initiate_dkg};
|
||||
use crate::support::tests::helpers;
|
||||
use crate::support::tests::helpers::{add_fixture_dealer, MULTISIG_CONTRACT};
|
||||
use crate::support::tests::helpers::{add_fixture_dealer, ADMIN_ADDRESS, MULTISIG_CONTRACT};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::Addr;
|
||||
use cw_controllers::AdminError;
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::types::{EpochState, TimeConfiguration};
|
||||
@@ -103,6 +107,8 @@ mod tests {
|
||||
fn current_epoch_id() {
|
||||
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 info = mock_info("requester", &[]);
|
||||
let share = "share".to_string();
|
||||
|
||||
@@ -122,6 +128,7 @@ mod tests {
|
||||
let dealer_details = DealerDetails {
|
||||
address: dealer.clone(),
|
||||
bte_public_key_with_proof: String::new(),
|
||||
ed25519_identity: String::new(),
|
||||
announce_address: announce_address.clone(),
|
||||
assigned_index: 1,
|
||||
};
|
||||
@@ -149,6 +156,8 @@ mod tests {
|
||||
fn commit_vk_share() {
|
||||
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 info = mock_info("requester", &[]);
|
||||
let share = "share".to_string();
|
||||
|
||||
@@ -163,7 +172,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
ret,
|
||||
ContractError::IncorrectEpochState {
|
||||
current_state: EpochState::default().to_string(),
|
||||
current_state: EpochState::PublicKeySubmission { resharing: false }.to_string(),
|
||||
expected_state: EpochState::VerificationKeySubmission { resharing: false }
|
||||
.to_string()
|
||||
}
|
||||
@@ -193,6 +202,7 @@ mod tests {
|
||||
let dealer_details = DealerDetails {
|
||||
address: dealer.clone(),
|
||||
bte_public_key_with_proof: String::new(),
|
||||
ed25519_identity: String::new(),
|
||||
announce_address: String::new(),
|
||||
assigned_index: 1,
|
||||
};
|
||||
@@ -223,8 +233,10 @@ mod tests {
|
||||
fn invalid_verify_vk_share() {
|
||||
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 info = mock_info("requester", &[]);
|
||||
let owner = Addr::unchecked("owner");
|
||||
let owner = "owner".to_string();
|
||||
let multisig_info = mock_info(MULTISIG_CONTRACT, &[]);
|
||||
|
||||
let ret =
|
||||
@@ -233,7 +245,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
ret,
|
||||
ContractError::IncorrectEpochState {
|
||||
current_state: EpochState::default().to_string(),
|
||||
current_state: EpochState::PublicKeySubmission { resharing: false }.to_string(),
|
||||
expected_state: EpochState::VerificationKeyFinalization { resharing: false }
|
||||
.to_string()
|
||||
}
|
||||
@@ -280,7 +292,9 @@ mod tests {
|
||||
fn verify_vk_share() {
|
||||
let mut deps = helpers::init_contract();
|
||||
let mut env = mock_env();
|
||||
let owner = Addr::unchecked("owner");
|
||||
try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap();
|
||||
|
||||
let owner = "owner".to_string();
|
||||
let info = mock_info(owner.as_ref(), &[]);
|
||||
let share = "share".to_string();
|
||||
let multisig_info = mock_info(MULTISIG_CONTRACT, &[]);
|
||||
@@ -298,13 +312,18 @@ mod tests {
|
||||
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
|
||||
|
||||
let dealer_details = DealerDetails {
|
||||
address: owner.clone(),
|
||||
address: Addr::unchecked(&owner),
|
||||
bte_public_key_with_proof: String::new(),
|
||||
ed25519_identity: String::new(),
|
||||
announce_address: String::new(),
|
||||
assigned_index: 1,
|
||||
};
|
||||
dealers_storage::current_dealers()
|
||||
.save(deps.as_mut().storage, &owner, &dealer_details)
|
||||
.save(
|
||||
deps.as_mut().storage,
|
||||
&Addr::unchecked(&owner),
|
||||
&dealer_details,
|
||||
)
|
||||
.unwrap();
|
||||
try_commit_verification_key_share(deps.as_mut(), env.clone(), info, share, false).unwrap();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use cw4::Member;
|
||||
use cw_multi_test::Executor;
|
||||
use cw_utils::{Duration, Threshold};
|
||||
use nym_coconut_dkg_common::msg::ExecuteMsg::{
|
||||
AdvanceEpochState, CommitVerificationKeyShare, RegisterDealer,
|
||||
AdvanceEpochState, CommitVerificationKeyShare, InitiateDkg, RegisterDealer,
|
||||
};
|
||||
use nym_coconut_dkg_common::msg::InstantiateMsg as DkgInstantiateMsg;
|
||||
use nym_coconut_dkg_common::msg::QueryMsg::GetVerificationKeys;
|
||||
@@ -75,6 +75,7 @@ fn dkg_proposal() {
|
||||
multisig_addr: multisig_contract_addr.to_string(),
|
||||
time_configuration: None,
|
||||
mix_denom: TEST_COIN_DENOM.to_string(),
|
||||
key_size: 5,
|
||||
};
|
||||
let coconut_dkg_contract_addr = app
|
||||
.instantiate_contract(
|
||||
@@ -99,11 +100,20 @@ fn dkg_proposal() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
app.execute_contract(
|
||||
Addr::unchecked(OWNER),
|
||||
coconut_dkg_contract_addr.clone(),
|
||||
&InitiateDkg {},
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
app.execute_contract(
|
||||
Addr::unchecked(MEMBER1),
|
||||
coconut_dkg_contract_addr.clone(),
|
||||
&RegisterDealer {
|
||||
bte_key_with_proof: "bte_key_with_proof".to_string(),
|
||||
identity_key: "identity".to_string(),
|
||||
announce_address: "127.0.0.1:8000".to_string(),
|
||||
resharing: false,
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@ bs58 = "0.4.0"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
time = { version = "0.3", features = ["macros"] }
|
||||
semver = { version = "1.0.16", default-features = false }
|
||||
semver = { workspace = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.2"
|
||||
|
||||
@@ -20,7 +20,7 @@ cw-utils = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" }
|
||||
nym-name-service-common = { path = "../../common/cosmwasm-smart-contracts/name-service" }
|
||||
semver = { version = "1.0.16", default-features = false }
|
||||
semver = { workspace = true, default-features = false }
|
||||
serde = { version = "1.0.155", default-features = false, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ cw-utils = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" }
|
||||
nym-service-provider-directory-common = { path = "../../common/cosmwasm-smart-contracts/service-provider-directory" }
|
||||
semver = { version = "1.0.16", default-features = false }
|
||||
semver = { workspace = true, default-features = false }
|
||||
serde = { version = "1.0.155", default-features = false, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ cw-storage-plus = { workspace = true, features = ["iterator"] }
|
||||
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
thiserror ={ workspace = true }
|
||||
semver = { version = "1.0.16", default-features = false }
|
||||
semver = { workspace = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.3.1"
|
||||
|
||||
@@ -125,3 +125,7 @@ sqlx = { workspace = true, features = [
|
||||
[dev-dependencies]
|
||||
cw3 = { workspace = true }
|
||||
cw-utils = { workspace = true }
|
||||
rand_chacha = "0.3"
|
||||
rand_chacha_02 = { package = "rand_chacha", version = "0.2" }
|
||||
sha2 = "0.9"
|
||||
|
||||
|
||||
@@ -58,7 +58,10 @@ pub async fn post_blind_sign(
|
||||
|
||||
// check if we have the signing key available
|
||||
debug!("checking if we actually have coconut keys derived...");
|
||||
let keypair_guard = state.coconut_keypair.get().await;
|
||||
let maybe_keypair_guard = state.coconut_keypair.get().await;
|
||||
let Some(keypair_guard) = maybe_keypair_guard.as_ref() else {
|
||||
return Err(CoconutError::KeyPairNotDerivedYet);
|
||||
};
|
||||
let Some(signing_key) = keypair_guard.as_ref() else {
|
||||
return Err(CoconutError::KeyPairNotDerivedYet);
|
||||
};
|
||||
@@ -75,7 +78,7 @@ pub async fn post_blind_sign(
|
||||
|
||||
// produce the partial signature
|
||||
debug!("producing the partial credential");
|
||||
let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.secret_key())?;
|
||||
let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.keys.secret_key())?;
|
||||
|
||||
// store the information locally
|
||||
debug!("storing the issued credential in the database");
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
// 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;
|
||||
use cw3::{ProposalResponse, VoteResponse};
|
||||
use cw4::MemberResponse;
|
||||
use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse;
|
||||
use nym_coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse};
|
||||
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::{
|
||||
EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData,
|
||||
ChunkIndex, DealingIndex, EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData,
|
||||
PartialContractDealingData, State,
|
||||
};
|
||||
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
|
||||
use nym_contracts_common::dealings::ContractSafeBytes;
|
||||
use nym_contracts_common::IdentityKey;
|
||||
use nym_dkg::Threshold;
|
||||
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
use nym_validator_client::nyxd::{AccountId, Fee, Hash, TxResponse};
|
||||
@@ -18,36 +23,100 @@ use nym_validator_client::nyxd::{AccountId, Fee, Hash, TxResponse};
|
||||
#[async_trait]
|
||||
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(&self, idx: usize) -> Result<Vec<ContractDealing>>;
|
||||
|
||||
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,
|
||||
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,
|
||||
identity_key: IdentityKey,
|
||||
announce_address: String,
|
||||
resharing: bool,
|
||||
) -> Result<ExecuteResult>;
|
||||
async fn submit_dealing(
|
||||
|
||||
async fn submit_dealing_metadata(
|
||||
&self,
|
||||
dealing_bytes: ContractSafeBytes,
|
||||
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,
|
||||
|
||||
@@ -42,12 +42,18 @@ impl CachedEpoch {
|
||||
|
||||
async fn update(&mut self, epoch: Epoch) -> Result<()> {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let state_end =
|
||||
OffsetDateTime::from_unix_timestamp(epoch.finish_timestamp.seconds() as i64).unwrap();
|
||||
let until_epoch_state_end = state_end - now;
|
||||
|
||||
// make it valid until the next epoch transition or next 5min, whichever is smaller
|
||||
self.valid_until = now + min(until_epoch_state_end, 5 * time::Duration::MINUTE);
|
||||
let validity_duration = if let Some(epoch_finish) = epoch.finish_timestamp {
|
||||
let state_end =
|
||||
OffsetDateTime::from_unix_timestamp(epoch_finish.seconds() as i64).unwrap();
|
||||
let until_epoch_state_end = state_end - now;
|
||||
// make it valid until the next epoch transition or next 5min, whichever is smaller
|
||||
min(until_epoch_state_end, 5 * time::Duration::MINUTE)
|
||||
} else {
|
||||
5 * time::Duration::MINUTE
|
||||
};
|
||||
|
||||
self.valid_until = now + validity_duration;
|
||||
self.current_epoch_id = epoch.epoch_id;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
// 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;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use cw3::ProposalResponse;
|
||||
use cw3::{ProposalResponse, Status, VoteResponse};
|
||||
use cw4::MemberResponse;
|
||||
use nym_coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse};
|
||||
use nym_coconut_dkg_common::dealer::{DealerDetails, DealerDetailsResponse};
|
||||
use nym_coconut_dkg_common::dealing::{
|
||||
DealerDealingsStatusResponse, DealingChunkInfo, PartialContractDealing,
|
||||
};
|
||||
use nym_coconut_dkg_common::types::{
|
||||
EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, NodeIndex,
|
||||
ChunkIndex, DealingIndex, EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData,
|
||||
NodeIndex, PartialContractDealingData, State as ContractState,
|
||||
};
|
||||
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
|
||||
use nym_contracts_common::dealings::ContractSafeBytes;
|
||||
use nym_contracts_common::IdentityKey;
|
||||
use nym_dkg::Threshold;
|
||||
use nym_validator_client::nyxd::cosmwasm_client::logs::{find_attribute, NODE_INDEX};
|
||||
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
|
||||
@@ -21,10 +25,6 @@ pub(crate) struct DkgClient {
|
||||
}
|
||||
|
||||
impl DkgClient {
|
||||
// Some queries simply don't work the first time
|
||||
// Until we determine why that is, retry the query a few more times
|
||||
const RETRIES: usize = 3;
|
||||
|
||||
pub(crate) fn new<C>(nyxd_client: C) -> Self
|
||||
where
|
||||
C: Client + Send + Sync + 'static,
|
||||
@@ -38,15 +38,16 @@ impl DkgClient {
|
||||
self.inner.address().await
|
||||
}
|
||||
|
||||
pub(crate) async fn dkg_contract_address(&self) -> Result<AccountId, CoconutError> {
|
||||
self.inner.dkg_contract_address().await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_current_epoch(&self) -> Result<Epoch, CoconutError> {
|
||||
let mut ret = self.inner.get_current_epoch().await;
|
||||
for _ in 0..Self::RETRIES {
|
||||
if ret.is_ok() {
|
||||
return ret;
|
||||
}
|
||||
ret = self.inner.get_current_epoch().await;
|
||||
}
|
||||
ret
|
||||
self.inner.get_current_epoch().await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_contract_state(&self) -> Result<ContractState, CoconutError> {
|
||||
self.inner.contract_state().await
|
||||
}
|
||||
|
||||
pub(crate) async fn group_member(&self) -> Result<MemberResponse, CoconutError> {
|
||||
@@ -77,18 +78,50 @@ impl DkgClient {
|
||||
self.inner.get_current_dealers().await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_dealings(
|
||||
pub(crate) async fn get_dealings_statuses(
|
||||
&self,
|
||||
idx: usize,
|
||||
) -> Result<Vec<ContractDealing>, CoconutError> {
|
||||
let mut ret = self.inner.get_dealings(idx).await;
|
||||
for _ in 0..Self::RETRIES {
|
||||
if ret.is_ok() {
|
||||
return ret;
|
||||
}
|
||||
ret = self.inner.get_dealings(idx).await;
|
||||
}
|
||||
ret
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
) -> 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>>(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
address: S,
|
||||
) -> Result<Option<ContractVKShare>, CoconutError> {
|
||||
self.inner
|
||||
.get_verification_key_share(epoch_id, address.into())
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_verification_own_key_share(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Option<ContractVKShare>, CoconutError> {
|
||||
let address = self.inner.address().await;
|
||||
self.get_verification_key_share(epoch_id, address).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_verification_key_shares(
|
||||
@@ -98,10 +131,22 @@ impl DkgClient {
|
||||
self.inner.get_verification_key_shares(epoch_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_vote(&self, proposal_id: u64) -> Result<VoteResponse, CoconutError> {
|
||||
let address = self.get_address().await.to_string();
|
||||
self.inner.get_vote(proposal_id, address).await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_proposals(&self) -> Result<Vec<ProposalResponse>, CoconutError> {
|
||||
self.inner.list_proposals().await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_proposal_status(
|
||||
&self,
|
||||
proposal_id: u64,
|
||||
) -> Result<Status, CoconutError> {
|
||||
self.inner.get_proposal(proposal_id).await.map(|p| p.status)
|
||||
}
|
||||
|
||||
pub(crate) async fn advance_epoch_state(&self) -> Result<(), CoconutError> {
|
||||
self.inner.advance_epoch_state().await
|
||||
}
|
||||
@@ -109,12 +154,13 @@ impl DkgClient {
|
||||
pub(crate) async fn register_dealer(
|
||||
&self,
|
||||
bte_key: EncodedBTEPublicKeyWithProof,
|
||||
identity_key: IdentityKey,
|
||||
announce_address: String,
|
||||
resharing: bool,
|
||||
) -> Result<NodeIndex, CoconutError> {
|
||||
let res = self
|
||||
.inner
|
||||
.register_dealer(bte_key, announce_address, resharing)
|
||||
.register_dealer(bte_key, identity_key, announce_address, resharing)
|
||||
.await?;
|
||||
let node_index = find_attribute(&res.logs, "wasm", NODE_INDEX)
|
||||
.ok_or(CoconutError::NodeIndexRecoveryError {
|
||||
@@ -129,12 +175,24 @@ impl DkgClient {
|
||||
Ok(node_index)
|
||||
}
|
||||
|
||||
pub(crate) async fn submit_dealing(
|
||||
pub(crate) async fn submit_dealing_metadata(
|
||||
&self,
|
||||
dealing_bytes: ContractSafeBytes,
|
||||
dealing_index: DealingIndex,
|
||||
chunks: Vec<DealingChunkInfo>,
|
||||
resharing: bool,
|
||||
) -> Result<(), CoconutError> {
|
||||
self.inner.submit_dealing(dealing_bytes, 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(())
|
||||
}
|
||||
|
||||
@@ -143,20 +201,9 @@ impl DkgClient {
|
||||
share: VerificationKeyShare,
|
||||
resharing: bool,
|
||||
) -> Result<ExecuteResult, CoconutError> {
|
||||
let mut ret = self
|
||||
.inner
|
||||
self.inner
|
||||
.submit_verification_key_share(share.clone(), resharing)
|
||||
.await;
|
||||
for _ in 0..Self::RETRIES {
|
||||
if let Ok(res) = ret {
|
||||
return Ok(res);
|
||||
}
|
||||
ret = self
|
||||
.inner
|
||||
.submit_verification_key_share(share.clone(), resharing)
|
||||
.await;
|
||||
}
|
||||
ret
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn vote_verification_key_share(
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub(crate) enum ComplaintReason {
|
||||
MalformedBTEPublicKey,
|
||||
InvalidBTEPublicKey,
|
||||
MissingDealing,
|
||||
MalformedDealing,
|
||||
DealingVerificationError,
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::client::DkgClient;
|
||||
use crate::coconut::dkg::state::{ConsistentState, PersistentState, State};
|
||||
use crate::coconut::dkg::verification_key::{
|
||||
verification_key_finalization, verification_key_validation,
|
||||
};
|
||||
use crate::coconut::dkg::{
|
||||
dealing::dealing_exchange, public_key::public_key_submission,
|
||||
verification_key::verification_key_submission,
|
||||
};
|
||||
use crate::coconut::keypair::KeyPair as CoconutKeyPair;
|
||||
use crate::nyxd;
|
||||
use crate::support::config;
|
||||
use anyhow::{bail, Result};
|
||||
use nym_coconut_dkg_common::types::EpochState;
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use nym_task::{TaskClient, TaskManager};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::time::interval;
|
||||
|
||||
pub(crate) fn init_keypair(config: &config::CoconutSigner) -> Result<()> {
|
||||
let mut rng = OsRng;
|
||||
let dkg_params = nym_dkg::bte::setup();
|
||||
let kp = DkgKeyPair::new(&dkg_params, &mut rng);
|
||||
nym_pemstore::store_keypair(
|
||||
&kp,
|
||||
&nym_pemstore::KeyPairPath::new(
|
||||
&config.storage_paths.decryption_key_path,
|
||||
&config.storage_paths.public_key_with_proof_path,
|
||||
),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) struct DkgController<R> {
|
||||
dkg_client: DkgClient,
|
||||
secret_key_path: PathBuf,
|
||||
verification_key_path: PathBuf,
|
||||
state: State,
|
||||
rng: R,
|
||||
polling_rate: Duration,
|
||||
}
|
||||
|
||||
impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
|
||||
pub(crate) async fn new(
|
||||
config: &config::CoconutSigner,
|
||||
nyxd_client: nyxd::Client,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
rng: R,
|
||||
) -> Result<Self> {
|
||||
let Some(announce_address) = &config.announce_address else {
|
||||
bail!("can't start a DKG controller without specifying an announce address!")
|
||||
};
|
||||
|
||||
let dkg_keypair = nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new(
|
||||
&config.storage_paths.decryption_key_path,
|
||||
&config.storage_paths.public_key_with_proof_path,
|
||||
))?;
|
||||
if let Ok(coconut_keypair_value) =
|
||||
nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new(
|
||||
&config.storage_paths.secret_key_path,
|
||||
&config.storage_paths.verification_key_path,
|
||||
))
|
||||
{
|
||||
coconut_keypair.set(Some(coconut_keypair_value)).await;
|
||||
}
|
||||
let persistent_state =
|
||||
PersistentState::load_from_file(&config.storage_paths.dkg_persistent_state_path)
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(DkgController {
|
||||
dkg_client: DkgClient::new(nyxd_client),
|
||||
secret_key_path: config.storage_paths.secret_key_path.clone(),
|
||||
verification_key_path: config.storage_paths.verification_key_path.clone(),
|
||||
state: State::new(
|
||||
config.storage_paths.dkg_persistent_state_path.clone(),
|
||||
persistent_state,
|
||||
announce_address.clone(),
|
||||
dkg_keypair,
|
||||
coconut_keypair,
|
||||
),
|
||||
rng,
|
||||
polling_rate: config.debug.dkg_contract_polling_rate,
|
||||
})
|
||||
}
|
||||
|
||||
async fn dump_persistent_state(&self) {
|
||||
if !self.state.coconut_keypair_is_some().await {
|
||||
// Delete the files just in case the process is killed before the new keys are generated
|
||||
std::fs::remove_file(&self.secret_key_path).ok();
|
||||
std::fs::remove_file(&self.verification_key_path).ok();
|
||||
}
|
||||
let persistent_state = PersistentState::from(&self.state);
|
||||
if let Err(err) = persistent_state.save_to_file(self.state.persistent_state_path()) {
|
||||
warn!("Could not backup the state for this iteration: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_epoch_state(&mut self) {
|
||||
match self.dkg_client.get_current_epoch().await {
|
||||
Err(err) => warn!("Could not get current epoch state {err}"),
|
||||
Ok(epoch) => {
|
||||
if self
|
||||
.dkg_client
|
||||
.group_member()
|
||||
.await
|
||||
.map(|resp| resp.weight.is_none())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
debug!("Not a member of the group, DKG won't be run");
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.state.is_consistent(epoch.state).await {
|
||||
debug!("Epoch state is corrupted - {err}. Awaiting for a DKG restart.");
|
||||
} else {
|
||||
let ret = match epoch.state {
|
||||
EpochState::PublicKeySubmission { resharing } => {
|
||||
public_key_submission(&self.dkg_client, &mut self.state, resharing)
|
||||
.await
|
||||
}
|
||||
EpochState::DealingExchange { resharing } => {
|
||||
dealing_exchange(
|
||||
&self.dkg_client,
|
||||
&mut self.state,
|
||||
self.rng.clone(),
|
||||
resharing,
|
||||
)
|
||||
.await
|
||||
}
|
||||
EpochState::VerificationKeySubmission { resharing } => {
|
||||
let keypair_path = nym_pemstore::KeyPairPath::new(
|
||||
self.secret_key_path.clone(),
|
||||
self.verification_key_path.clone(),
|
||||
);
|
||||
verification_key_submission(
|
||||
&self.dkg_client,
|
||||
&mut self.state,
|
||||
&keypair_path,
|
||||
resharing,
|
||||
)
|
||||
.await
|
||||
}
|
||||
EpochState::VerificationKeyValidation { resharing } => {
|
||||
verification_key_validation(
|
||||
&self.dkg_client,
|
||||
&mut self.state,
|
||||
resharing,
|
||||
)
|
||||
.await
|
||||
}
|
||||
EpochState::VerificationKeyFinalization { resharing } => {
|
||||
verification_key_finalization(
|
||||
&self.dkg_client,
|
||||
&mut self.state,
|
||||
resharing,
|
||||
)
|
||||
.await
|
||||
}
|
||||
// Just wait, in case we need to redo dkg at some point
|
||||
EpochState::InProgress => {
|
||||
self.state.set_was_in_progress();
|
||||
// We're dumping state here so that we don't do it uselessly during the
|
||||
// long InProgress state
|
||||
self.dump_persistent_state().await;
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
if let Err(err) = ret {
|
||||
warn!("Could not handle this iteration for the epoch state: {err}");
|
||||
} else if epoch.state != EpochState::InProgress {
|
||||
self.dump_persistent_state().await;
|
||||
}
|
||||
}
|
||||
if let Ok(current_timestamp) =
|
||||
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
|
||||
{
|
||||
if current_timestamp.as_secs() >= epoch.finish_timestamp.seconds() {
|
||||
// We try advancing the epoch state, on a best-effort basis
|
||||
info!("DKG: Trying to advance the epoch");
|
||||
self.dkg_client.advance_epoch_state().await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run(mut self, mut shutdown: TaskClient) {
|
||||
let mut interval = interval(self.polling_rate);
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => self.handle_epoch_state().await,
|
||||
_ = shutdown.recv() => {
|
||||
trace!("DkgController: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: can we make it non-async? it seems we'd have to modify `coconut_keypair.set(coconut_keypair_value)` in new
|
||||
// could we do it?
|
||||
pub(crate) async fn start(
|
||||
config: &config::CoconutSigner,
|
||||
nyxd_client: nyxd::Client,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
rng: R,
|
||||
shutdown: &TaskManager,
|
||||
) -> Result<()>
|
||||
where
|
||||
R: Sync + Send + 'static,
|
||||
{
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
let dkg_controller = DkgController::new(config, nyxd_client, coconut_keypair, rng).await?;
|
||||
tokio::spawn(async move { dkg_controller.run(shutdown_listener).await });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::dealing::DealingGenerationError;
|
||||
use crate::coconut::dkg::key_derivation::KeyDerivationError;
|
||||
use crate::coconut::dkg::key_finalization::KeyFinalizationError;
|
||||
use crate::coconut::dkg::key_validation::KeyValidationError;
|
||||
use crate::coconut::dkg::public_key::PublicKeySubmissionError;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DkgError {
|
||||
#[error("failed to persist local state to disk at path {}: {source}", path.display())]
|
||||
StatePersistenceFailure {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: CoconutError,
|
||||
},
|
||||
|
||||
#[error("failed to query for the current DKG epoch state: {source}")]
|
||||
EpochQueryFailure {
|
||||
#[source]
|
||||
source: CoconutError,
|
||||
},
|
||||
|
||||
#[error("failed to query the CW4 group contract for the membership status: {source}")]
|
||||
GroupQueryFailure {
|
||||
#[source]
|
||||
source: CoconutError,
|
||||
},
|
||||
|
||||
#[error("this API is currently not member of the DKG group and thus can't participate in the process")]
|
||||
NotInGroup,
|
||||
|
||||
#[error("failed to submit public keys to the DKG contract: {source}")]
|
||||
PublicKeySubmissionFailure {
|
||||
#[source]
|
||||
source: PublicKeySubmissionError,
|
||||
},
|
||||
|
||||
#[error("failed to submit DKG dealings to the DKG contract: {source}")]
|
||||
DealingExchangeFailure {
|
||||
#[source]
|
||||
source: DealingGenerationError,
|
||||
},
|
||||
|
||||
#[error("failed to submit verification keys to the DKG contract: {source}")]
|
||||
VerificationKeySubmissionFailure {
|
||||
#[source]
|
||||
source: KeyDerivationError,
|
||||
},
|
||||
|
||||
#[error("failed to validate verification keys in the DKG contract: {source}")]
|
||||
VerificationKeyValidationFailure {
|
||||
#[source]
|
||||
source: KeyValidationError,
|
||||
},
|
||||
|
||||
#[error("failed to finalize verification keys in the DKG contract: {source}")]
|
||||
VerificationKeyFinalizationFailure {
|
||||
#[source]
|
||||
source: KeyFinalizationError,
|
||||
},
|
||||
|
||||
#[error("failed to advance the DKG state: {source}")]
|
||||
StateAdvancementFailure {
|
||||
#[source]
|
||||
source: CoconutError,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::client::Client;
|
||||
use crate::coconut::keys::KeyPairWithEpoch;
|
||||
use crate::support::{config, nyxd};
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use nym_coconut_dkg_common::types::{EpochId, EpochState};
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::path::Path;
|
||||
use thiserror::__private::AsDisplay;
|
||||
|
||||
pub(crate) fn init_bte_keypair<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
config: &config::CoconutSigner,
|
||||
) -> anyhow::Result<()> {
|
||||
let dkg_params = nym_dkg::bte::setup();
|
||||
let kp = DkgKeyPair::new(&dkg_params, rng);
|
||||
nym_pemstore::store_keypair(
|
||||
&kp,
|
||||
&nym_pemstore::KeyPairPath::new(
|
||||
&config.storage_paths.decryption_key_path,
|
||||
&config.storage_paths.public_key_with_proof_path,
|
||||
),
|
||||
)
|
||||
.context("DKG BTE keypair store failure")
|
||||
}
|
||||
|
||||
pub(crate) fn load_bte_keypair(config: &config::CoconutSigner) -> anyhow::Result<DkgKeyPair> {
|
||||
nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new(
|
||||
&config.storage_paths.decryption_key_path,
|
||||
&config.storage_paths.public_key_with_proof_path,
|
||||
))
|
||||
.context("bte keypair load failure")
|
||||
}
|
||||
|
||||
pub(crate) fn load_coconut_keypair_if_exists(
|
||||
config: &config::CoconutSigner,
|
||||
) -> anyhow::Result<Option<KeyPairWithEpoch>> {
|
||||
if !config.storage_paths.coconut_key_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
nym_pemstore::load_key(&config.storage_paths.coconut_key_path)
|
||||
.context("coconut key load failure")
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
// the keys can be considered valid if they were generated for the current dkg epoch
|
||||
// and we're either in the "in progress" or "key finalization" states of the DKG
|
||||
pub(crate) async fn can_validate_coconut_keys(
|
||||
nyxd_client: &nyxd::Client,
|
||||
issued_for: EpochId,
|
||||
) -> anyhow::Result<bool> {
|
||||
// validate the keys if they were generated for the current dkg epoch
|
||||
// and we're either in the "in progress" or "key finalization" states of the DKG
|
||||
let current_dkg_epoch = nyxd_client.get_current_epoch().await?;
|
||||
if issued_for != current_dkg_epoch.epoch_id {
|
||||
warn!("managed to load coconut keys, but they were generated for epoch {issued_for}. The current epoch is {}. the keys won't be used for credential issuance", current_dkg_epoch.epoch_id);
|
||||
Ok(false)
|
||||
} else if !matches!(
|
||||
current_dkg_epoch.state,
|
||||
EpochState::InProgress | EpochState::VerificationKeyFinalization { .. }
|
||||
) {
|
||||
warn!("managed to load coconut keys, but the current DKG epoch is at {}. the keys won't (yet) be used for credential issuance", current_dkg_epoch.state);
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn persist_coconut_keypair<P: AsRef<Path>>(
|
||||
keys: &KeyPairWithEpoch,
|
||||
store_path: P,
|
||||
) -> anyhow::Result<()> {
|
||||
nym_pemstore::store_key(keys, store_path).context("coconut key store failure")
|
||||
}
|
||||
|
||||
pub(crate) fn archive_coconut_keypair<P: AsRef<Path>>(
|
||||
store_path: P,
|
||||
epoch_id: EpochId,
|
||||
) -> anyhow::Result<()> {
|
||||
let store_path = store_path.as_ref();
|
||||
if !store_path.exists() {
|
||||
bail!("coconut key does not exist at {}", store_path.as_display())
|
||||
}
|
||||
|
||||
let dir = store_path
|
||||
.parent()
|
||||
.ok_or(anyhow!("the coconut key does not have a valid parent"))?;
|
||||
let filename = store_path
|
||||
.file_name()
|
||||
.ok_or(anyhow!("the coconut key does not have a valid filename"))?
|
||||
.to_str()
|
||||
.ok_or(anyhow!("the coconut key filename is not valid UTF8"))?;
|
||||
let archive_path = dir.join(format!("epoch-{epoch_id}-{filename}.archived"));
|
||||
std::fs::rename(store_path, archive_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::client::DkgClient;
|
||||
use crate::coconut::dkg::controller::error::DkgError;
|
||||
use crate::coconut::dkg::state::{PersistentState, State};
|
||||
use crate::coconut::keys::KeyPair as CoconutKeyPair;
|
||||
use crate::nyxd;
|
||||
use crate::support::config;
|
||||
use anyhow::{bail, Result};
|
||||
use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState};
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use nym_task::{TaskClient, TaskManager};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::{CryptoRng, Rng, RngCore};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::time::interval;
|
||||
|
||||
mod error;
|
||||
pub(crate) mod keys;
|
||||
|
||||
pub(crate) struct DkgController<R = OsRng> {
|
||||
pub(crate) dkg_client: DkgClient,
|
||||
pub(crate) coconut_key_path: PathBuf,
|
||||
pub(crate) state: State,
|
||||
pub(super) rng: R,
|
||||
polling_rate: Duration,
|
||||
}
|
||||
|
||||
impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
|
||||
pub(crate) fn new(
|
||||
config: &config::CoconutSigner,
|
||||
nyxd_client: nyxd::Client,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
dkg_keypair: DkgKeyPair,
|
||||
identity_key: identity::PublicKey,
|
||||
rng: R,
|
||||
) -> Result<Self> {
|
||||
let Some(announce_address) = &config.announce_address else {
|
||||
bail!("can't start a DKG controller without specifying an announce address!")
|
||||
};
|
||||
|
||||
let persistent_state = PersistentState::load_from_file(
|
||||
&config.storage_paths.dkg_persistent_state_path,
|
||||
).unwrap_or_else(|err| {
|
||||
warn!("could not load an existing persistent state from the file. a fresh state will be used: {err}");
|
||||
Default::default()
|
||||
});
|
||||
|
||||
Ok(DkgController {
|
||||
dkg_client: DkgClient::new(nyxd_client),
|
||||
coconut_key_path: config.storage_paths.coconut_key_path.clone(),
|
||||
state: State::new(
|
||||
config.storage_paths.dkg_persistent_state_path.clone(),
|
||||
persistent_state,
|
||||
announce_address.clone(),
|
||||
dkg_keypair,
|
||||
identity_key,
|
||||
coconut_keypair,
|
||||
),
|
||||
rng,
|
||||
polling_rate: config.debug.dkg_contract_polling_rate,
|
||||
})
|
||||
}
|
||||
|
||||
fn persist_state(&self) -> Result<(), DkgError> {
|
||||
let persistent_state = PersistentState::from(&self.state);
|
||||
let save_path = self.state.persistent_state_path();
|
||||
persistent_state.save_to_file(save_path).map_err(|source| {
|
||||
DkgError::StatePersistenceFailure {
|
||||
path: save_path.to_path_buf(),
|
||||
source,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn current_epoch(&self) -> Result<Epoch, DkgError> {
|
||||
self.dkg_client
|
||||
.get_current_epoch()
|
||||
.await
|
||||
.map_err(|source| DkgError::EpochQueryFailure { source })
|
||||
}
|
||||
|
||||
async fn ensure_group_member(&self) -> Result<(), DkgError> {
|
||||
let membership_response = self
|
||||
.dkg_client
|
||||
.group_member()
|
||||
.await
|
||||
.map_err(|source| DkgError::GroupQueryFailure { source })?;
|
||||
|
||||
debug!("CW4 membership response: {membership_response:?}");
|
||||
|
||||
// make sure we are a voting member, i.e. have a non-zero weight
|
||||
if let Some(weight) = membership_response.weight {
|
||||
if weight == 0 {
|
||||
return Err(DkgError::NotInGroup);
|
||||
}
|
||||
} else {
|
||||
return Err(DkgError::NotInGroup);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_awaiting_initialisation(&mut self) -> Result<(), DkgError> {
|
||||
info!("DKG hasn't been initialised yet - nothing to do");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_key_submission(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), DkgError> {
|
||||
debug!("DKG: public key submission (resharing: {resharing})");
|
||||
self.public_key_submission(epoch_id, resharing)
|
||||
.await
|
||||
.map_err(|source| DkgError::PublicKeySubmissionFailure { source })?;
|
||||
self.persist_state()
|
||||
}
|
||||
|
||||
async fn handle_dealing_exchange(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), DkgError> {
|
||||
debug!("DKG: dealing exchange (resharing: {resharing})");
|
||||
self.dealing_exchange(epoch_id, resharing)
|
||||
.await
|
||||
.map_err(|source| DkgError::DealingExchangeFailure { source })?;
|
||||
self.persist_state()
|
||||
}
|
||||
|
||||
async fn handle_verification_key_submission(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), DkgError> {
|
||||
debug!("DKG: verification key submission (resharing: {resharing})");
|
||||
self.verification_key_submission(epoch_id, resharing)
|
||||
.await
|
||||
.map_err(|source| DkgError::VerificationKeySubmissionFailure { source })?;
|
||||
self.persist_state()
|
||||
}
|
||||
|
||||
async fn handle_verification_key_validation(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), DkgError> {
|
||||
debug!("DKG: verification key validation (resharing: {resharing})");
|
||||
|
||||
self.verification_key_validation(epoch_id)
|
||||
.await
|
||||
.map_err(|source| DkgError::VerificationKeyValidationFailure { source })?;
|
||||
self.persist_state()
|
||||
}
|
||||
|
||||
async fn handle_verification_key_finalization(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), DkgError> {
|
||||
debug!("DKG: verification key finalization (resharing: {resharing})");
|
||||
|
||||
self.verification_key_finalization(epoch_id)
|
||||
.await
|
||||
.map_err(|source| DkgError::VerificationKeyFinalizationFailure { source })?;
|
||||
self.persist_state()
|
||||
}
|
||||
|
||||
async fn handle_in_progress(&mut self, epoch_id: EpochId) -> Result<(), DkgError> {
|
||||
debug!("DKG: epoch in progress");
|
||||
|
||||
let Ok(state) = self.state.in_progress_state(epoch_id) else {
|
||||
// we probably just started up the api while the DKG has already finished and we're waiting for new round to join
|
||||
debug!("the DKG has finished without our participation");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if !state.entered {
|
||||
info!("this is the first time this node is in the in progress state - going to clear state from the PREVIOUS epoch...");
|
||||
// if we finished dkg for epoch 123, we no longer care about anything from epoch 122
|
||||
// (but keep track of data from 123 for the future reference)
|
||||
self.state.clear_previous_epoch(epoch_id);
|
||||
|
||||
// SAFETY: we just accessed this item in an immutable way, thus it MUST exist so the unwrap is fine
|
||||
self.state.in_progress_state_mut(epoch_id).unwrap().entered = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_advance_dkg_state(&mut self) -> Result<(), DkgError> {
|
||||
// We try advancing the epoch state, on a best-effort basis
|
||||
info!("DKG: Trying to advance the epoch");
|
||||
self.dkg_client
|
||||
.advance_epoch_state()
|
||||
.await
|
||||
.map_err(|source| DkgError::StateAdvancementFailure { source })
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_epoch_state(&mut self) -> Result<(), DkgError> {
|
||||
self.ensure_group_member().await?;
|
||||
|
||||
let epoch = self.current_epoch().await?;
|
||||
|
||||
match epoch.state {
|
||||
EpochState::WaitingInitialisation => self.handle_awaiting_initialisation().await?,
|
||||
EpochState::PublicKeySubmission { resharing } => {
|
||||
self.handle_key_submission(epoch.epoch_id, resharing)
|
||||
.await?
|
||||
}
|
||||
EpochState::DealingExchange { resharing } => {
|
||||
self.handle_dealing_exchange(epoch.epoch_id, resharing)
|
||||
.await?
|
||||
}
|
||||
EpochState::VerificationKeySubmission { resharing } => {
|
||||
self.handle_verification_key_submission(epoch.epoch_id, resharing)
|
||||
.await?
|
||||
}
|
||||
EpochState::VerificationKeyValidation { resharing } => {
|
||||
self.handle_verification_key_validation(epoch.epoch_id, resharing)
|
||||
.await?
|
||||
}
|
||||
EpochState::VerificationKeyFinalization { resharing } => {
|
||||
self.handle_verification_key_finalization(epoch.epoch_id, resharing)
|
||||
.await?
|
||||
}
|
||||
// Just wait, in case we need to redo dkg at some point
|
||||
EpochState::InProgress => self.handle_in_progress(epoch.epoch_id).await?,
|
||||
};
|
||||
|
||||
// add a bit of variance so that all apis wouldn't attempt to trigger it at the same time
|
||||
let variance = self.rng.gen_range(0..=60);
|
||||
if let Some(epoch_finish) = epoch.finish_timestamp {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
if now.unix_timestamp() > epoch_finish.seconds() as i64 + variance {
|
||||
// TODO: make sure to not overload validator in case its running slow
|
||||
// i.e. send it once at most every X seconds
|
||||
self.try_advance_dkg_state().await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn run(mut self, mut shutdown: TaskClient) {
|
||||
let mut interval = interval(self.polling_rate);
|
||||
|
||||
// sometimes when the process is running behind, the ticker resolves multiple times in quick succession
|
||||
// so explicitly track those instances and make sure we don't overload the validator with contract calls
|
||||
let mut last_polled = OffsetDateTime::now_utc();
|
||||
let mut last_tick_duration = Default::default();
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let tick_duration = now - last_polled;
|
||||
last_polled = now;
|
||||
|
||||
if tick_duration < self.polling_rate {
|
||||
warn!("it seems the process is running behind. The current tick rate is lower than the polling rate. rate: {:?}, current tick: {}, previous tick: {}", self.polling_rate, tick_duration, last_tick_duration);
|
||||
last_tick_duration = tick_duration;
|
||||
continue
|
||||
}
|
||||
last_tick_duration = tick_duration;
|
||||
|
||||
if let Err(err) = self.handle_epoch_state().await {
|
||||
error!("failed to update the DKG state: {err}")
|
||||
}
|
||||
}
|
||||
_ = shutdown.recv() => {
|
||||
trace!("DkgController: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start(
|
||||
config: &config::CoconutSigner,
|
||||
nyxd_client: nyxd::Client,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
dkg_bte_keypair: DkgKeyPair,
|
||||
identity_key: identity::PublicKey,
|
||||
rng: R,
|
||||
shutdown: &TaskManager,
|
||||
) -> Result<()>
|
||||
where
|
||||
R: Sync + Send + 'static,
|
||||
{
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
let dkg_controller = DkgController::new(
|
||||
config,
|
||||
nyxd_client,
|
||||
coconut_keypair,
|
||||
dkg_bte_keypair,
|
||||
identity_key,
|
||||
rng,
|
||||
)?;
|
||||
tokio::spawn(async move { dkg_controller.run(shutdown_listener).await });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl DkgController {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn default_test_mock(
|
||||
dkg_client: DkgClient,
|
||||
state: State,
|
||||
) -> DkgController<rand_chacha::ChaCha20Rng> {
|
||||
DkgController {
|
||||
dkg_client,
|
||||
coconut_key_path: Default::default(),
|
||||
state,
|
||||
rng: crate::coconut::tests::fixtures::test_rng([1u8; 32]),
|
||||
polling_rate: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn test_mock(
|
||||
rng: rand_chacha::ChaCha20Rng,
|
||||
dkg_client: DkgClient,
|
||||
state: State,
|
||||
coconut_key_path: PathBuf,
|
||||
) -> DkgController<rand_chacha::ChaCha20Rng> {
|
||||
DkgController {
|
||||
dkg_client,
|
||||
coconut_key_path,
|
||||
state,
|
||||
rng,
|
||||
polling_rate: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+695
-309
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::controller::DkgController;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use cw3::{ProposalResponse, Status};
|
||||
use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs;
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::collections::HashMap;
|
||||
|
||||
impl<R: RngCore + CryptoRng> DkgController<R> {
|
||||
fn filter_proposal(
|
||||
&self,
|
||||
dkg_contract: &AccountId,
|
||||
proposal: &ProposalResponse,
|
||||
) -> 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
|
||||
// - contains valid verification request (checked inside `owner_from_cosmos_msgs`)
|
||||
if proposal.status == Status::Open && proposal.proposer.as_str() == dkg_contract.as_ref() {
|
||||
if let Some(owner) = owner_from_cosmos_msgs(&proposal.msgs) {
|
||||
return Some((owner, proposal.id));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn get_validation_proposals(
|
||||
&self,
|
||||
) -> 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,
|
||||
// we don't care about expired proposals from epochs 0-49...
|
||||
// to do it, we'll need to have dkg contract store proposal ids,
|
||||
// which will require usage of submsgs and replies so that might be a future project
|
||||
let all_proposals = self.dkg_client.list_proposals().await?;
|
||||
|
||||
let mut deduped_proposals = HashMap::new();
|
||||
|
||||
// for each proposal, make sure it's a valid validation request;
|
||||
// if for some reason there exist multiple proposals from the same owner, choose the one
|
||||
// with the higher id (there might be multiple since we're grabbing them across epochs)
|
||||
for proposal in all_proposals {
|
||||
if let Some((owner, id)) = self.filter_proposal(&dkg_contract, &proposal) {
|
||||
if let Some(old_id) = deduped_proposals.get(&owner) {
|
||||
if old_id < &id {
|
||||
deduped_proposals.insert(owner, id);
|
||||
}
|
||||
} else {
|
||||
deduped_proposals.insert(owner, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UNHANDLED EDGE CASE:
|
||||
// since currently proposals are **NOT** tied to epochs,
|
||||
// we might run into proposals from older epochs we don't have to vote on or might not even have data for
|
||||
Ok(deduped_proposals)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,131 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::controller::DkgController;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use cw3::Status;
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KeyFinalizationError {
|
||||
#[error(transparent)]
|
||||
CoconutError(#[from] CoconutError),
|
||||
|
||||
#[error("our proposal for key verification is still open (or is pending) (proposal id: {proposal_id}) ")]
|
||||
UnresolvedProposal { proposal_id: u64 },
|
||||
|
||||
#[error("our proposal for key verification has been rejected (proposal id: {proposal_id})")]
|
||||
RejectedProposal { proposal_id: u64 },
|
||||
|
||||
#[error("can't complete key finalization without key validation")]
|
||||
IncompleteKeyValidation,
|
||||
}
|
||||
|
||||
impl<R: RngCore + CryptoRng> DkgController<R> {
|
||||
pub(crate) async fn verification_key_finalization(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<(), KeyFinalizationError> {
|
||||
let key_finalization_state = self.state.key_finalization_state(epoch_id)?;
|
||||
|
||||
// check if we have already executed our own proposal
|
||||
if key_finalization_state.completed() {
|
||||
// the only way this could be a false positive is if the chain forked and blocks got reverted,
|
||||
// but I don't think we have to worry about that
|
||||
debug!("our key has already been verified");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !self.state.key_validation_state(epoch_id)?.completed {
|
||||
return Err(KeyFinalizationError::IncompleteKeyValidation);
|
||||
}
|
||||
|
||||
let proposal_id = self.state.proposal_id(epoch_id)?;
|
||||
|
||||
// check whether our key has already been verified with executed proposal,
|
||||
// either by us in previous iteration after a timeout
|
||||
// or by another party
|
||||
let status = self.dkg_client.get_proposal_status(proposal_id).await?;
|
||||
match status {
|
||||
// if the proposal hasn't been resolved, there's not much we can do but wait and pray
|
||||
Status::Pending | Status::Open => {
|
||||
// 'theoretically' it's possible that more votes are going to come in, but it's very unlikely
|
||||
warn!("our proposal ({proposal_id}) still hasn't received enough votes to get accepted");
|
||||
return Err(KeyFinalizationError::UnresolvedProposal { proposal_id });
|
||||
}
|
||||
// if the proposal has been rejected, there's nothing we can do, we failed the DKG
|
||||
Status::Rejected => {
|
||||
// technically there's nothing enforcing this, so as long as our keys have been properly generated
|
||||
// (even though they've been rejected by other parties), they could still issue [cryptographically] valid credentials
|
||||
error!("our key verification proposal ({proposal_id}) has been rejected - we can't use our derived keys!");
|
||||
self.state.key_finalization_state_mut(epoch_id)?.completed = true;
|
||||
return Err(KeyFinalizationError::RejectedProposal { proposal_id });
|
||||
}
|
||||
// if the proposal has passed, execute it to finalize our key
|
||||
Status::Passed => {
|
||||
self.dkg_client
|
||||
.execute_verification_key_share(proposal_id)
|
||||
.await?;
|
||||
}
|
||||
// if they proposal has already been executed, we're done!
|
||||
Status::Executed => {
|
||||
// generally each dealer is responsible for executing its own proposals,
|
||||
// but technically there's nothing preventing other dealers from executing them
|
||||
debug!("our dkg proposal has already been executed");
|
||||
}
|
||||
}
|
||||
|
||||
self.state.key_finalization_state_mut(epoch_id)?.completed = true;
|
||||
self.state.validate_coconut_keypair();
|
||||
info!("DKG: Finalized own verification key on chain");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: the following tests currently do NOT cover all cases
|
||||
// I've (@JS) only updated old, existing, tests. nothing more
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::coconut::tests::helpers::{
|
||||
derive_keypairs, exchange_dealings, initialise_controllers, initialise_dkg,
|
||||
submit_public_keys, validate_keys,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn finalize_verification_key() -> anyhow::Result<()> {
|
||||
let validators = 4;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let epoch = chain.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
|
||||
initialise_dkg(&mut controllers, false).await;
|
||||
submit_public_keys(&mut controllers, false).await;
|
||||
exchange_dealings(&mut controllers, false).await;
|
||||
derive_keypairs(&mut controllers, false).await;
|
||||
validate_keys(&mut controllers, false).await;
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let res = controller.verification_key_finalization(epoch).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
assert!(controller.state.key_finalization_state(epoch)?.completed);
|
||||
}
|
||||
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let guard = chain.lock().unwrap();
|
||||
let proposals = &guard.multisig_contract.proposals;
|
||||
assert_eq!(proposals.len(), validators);
|
||||
|
||||
for proposal in proposals.values() {
|
||||
assert_eq!(Status::Executed, proposal.status)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::controller::DkgController;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::state::bandwidth_voucher_params;
|
||||
use cosmwasm_std::Addr;
|
||||
use cw3::Vote;
|
||||
use nym_coconut::{check_vk_pairing, Base58, VerificationKey};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_coconut_dkg_common::verification_key::ContractVKShare;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
fn vote_matches(voted_yes: bool, chain_vote: Vote) -> bool {
|
||||
if voted_yes && chain_vote == Vote::Yes {
|
||||
true
|
||||
} else {
|
||||
!voted_yes && chain_vote == Vote::No
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KeyValidationError {
|
||||
#[error(transparent)]
|
||||
CoconutError(#[from] CoconutError),
|
||||
|
||||
#[error("can't complete key validation without key derivation")]
|
||||
IncompleteKeyDerivation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ShareRejectionReason {
|
||||
#[error("{owner} does not appear to be present in the list of receivers for epoch {epoch_id}")]
|
||||
NotAReceiver { epoch_id: EpochId, owner: Addr },
|
||||
|
||||
#[error("the share from {owner} for epoch {epoch_id} already appears as verified on chain!")]
|
||||
AlreadyVerifiedOnChain { epoch_id: EpochId, owner: Addr },
|
||||
|
||||
#[error(
|
||||
"the share from {owner} for epoch {epoch_id} does not use valid base58 encoding: {source}"
|
||||
)]
|
||||
MalformedKeyEncoding {
|
||||
epoch_id: EpochId,
|
||||
owner: Addr,
|
||||
#[source]
|
||||
source: nym_coconut::CoconutError,
|
||||
},
|
||||
|
||||
#[error("did not derive partial keys for {owner} at index {receiver_index} for epoch {epoch_id} during the dealings exchange")]
|
||||
MissingDerivedPartialKey {
|
||||
epoch_id: EpochId,
|
||||
owner: Addr,
|
||||
receiver_index: usize,
|
||||
},
|
||||
|
||||
#[error("the provided keys {owner} at index {receiver_index} for epoch {epoch_id} either did not match the partial keys derived during the dealings exchange or failed the local bilinear pairing consistency check")]
|
||||
InconsistentKeys {
|
||||
epoch_id: EpochId,
|
||||
owner: Addr,
|
||||
receiver_index: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl<R: RngCore + CryptoRng> DkgController<R> {
|
||||
async fn verify_share(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
share: ContractVKShare,
|
||||
) -> Result<(Option<bool>, Option<ShareRejectionReason>), KeyValidationError> {
|
||||
fn reject(
|
||||
reason: ShareRejectionReason,
|
||||
) -> Result<(Option<bool>, Option<ShareRejectionReason>), KeyValidationError> {
|
||||
Ok((Some(false), Some(reason)))
|
||||
}
|
||||
|
||||
let owner = share.owner;
|
||||
|
||||
if share.verified {
|
||||
error!("the share from {owner} has already been validated on chain - this should be impossible unless this machine is running seriously behind");
|
||||
let reason = ShareRejectionReason::AlreadyVerifiedOnChain { epoch_id, owner };
|
||||
// explicitly return 'None' for the vote as we don't have to (nor even should) vote for this share
|
||||
return Ok((None, Some(reason)));
|
||||
}
|
||||
|
||||
// get the receiver index [of the dealings] for this participant
|
||||
let Some(receiver_index) = self
|
||||
.state
|
||||
.valid_epoch_receivers(epoch_id)?
|
||||
.iter()
|
||||
.position(|(addr, _)| addr == owner)
|
||||
else {
|
||||
return reject(ShareRejectionReason::NotAReceiver { epoch_id, owner });
|
||||
};
|
||||
|
||||
// attempt to recover the underlying key from its bs58 representation
|
||||
let recovered_key = match VerificationKey::try_from_bs58(share.share) {
|
||||
Ok(key) => key,
|
||||
Err(source) => {
|
||||
return reject(ShareRejectionReason::MalformedKeyEncoding {
|
||||
epoch_id,
|
||||
owner,
|
||||
source,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// retrieve the key we have recovered ourselves during the dealings exchange
|
||||
let Some(self_derived) = self
|
||||
.state
|
||||
.key_derivation_state(epoch_id)?
|
||||
.derived_partials_for(receiver_index)
|
||||
else {
|
||||
return reject(ShareRejectionReason::MissingDerivedPartialKey {
|
||||
epoch_id,
|
||||
owner,
|
||||
receiver_index,
|
||||
});
|
||||
};
|
||||
|
||||
if !check_vk_pairing(bandwidth_voucher_params(), &self_derived, &recovered_key) {
|
||||
return reject(ShareRejectionReason::InconsistentKeys {
|
||||
epoch_id,
|
||||
owner,
|
||||
receiver_index,
|
||||
});
|
||||
}
|
||||
|
||||
// all is good -> accept the keys!
|
||||
Ok((Some(true), None))
|
||||
}
|
||||
|
||||
async fn generate_votes(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<HashMap<u64, bool>, KeyValidationError> {
|
||||
let proposals = self.get_validation_proposals().await?;
|
||||
let vk_shares = self
|
||||
.dkg_client
|
||||
.get_verification_key_shares(epoch_id)
|
||||
.await?;
|
||||
|
||||
let mut votes = HashMap::new();
|
||||
for contract_share in vk_shares {
|
||||
let owner = contract_share.owner.clone();
|
||||
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.as_ref()) else {
|
||||
warn!("there does not seem to exist proposal for share validation from {owner}");
|
||||
continue;
|
||||
};
|
||||
|
||||
// if this is our share, obviously vote for yes without spending time on verification
|
||||
if owner.as_ref() == self.dkg_client.get_address().await.as_ref() {
|
||||
votes.insert(*proposal_id, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
let (vote, rejection_reason) = self.verify_share(epoch_id, contract_share).await?;
|
||||
if let Some(vote) = vote {
|
||||
votes.insert(*proposal_id, vote);
|
||||
}
|
||||
if let Some(rejection_reason) = rejection_reason {
|
||||
warn!("rejecting share from {owner} (proposal: {proposal_id}): {rejection_reason}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(votes)
|
||||
}
|
||||
|
||||
async fn resubmit_validation_votes(&self, epoch_id: EpochId) -> Result<(), KeyValidationError> {
|
||||
let key_validation_state = self.state.key_validation_state(epoch_id)?;
|
||||
|
||||
for (&proposal, &vote) in &key_validation_state.votes {
|
||||
// check whether we might have already voted on this particular proposal
|
||||
// (the vote might have gotten stuck in the mempool)
|
||||
let chain_vote = self.dkg_client.get_vote(proposal).await?;
|
||||
if let Some(chain_vote) = chain_vote.vote {
|
||||
warn!("we have already voted for proposal {proposal} before - we probably crashed or the chain timed out!");
|
||||
|
||||
// that's an extremely weird behaviour -> perhaps the user voted manually outside the nym-api,
|
||||
// but we can't do anything about it
|
||||
if !vote_matches(vote, chain_vote.vote) {
|
||||
error!("our vote for proposal {proposal} doesn't match the on-chain data! We decided to vote '{vote}' but the chain has {:?}", chain_vote.vote);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
warn!("we have already decided on the vote status for proposal {proposal} before (vote: {vote}), but failed to submit it");
|
||||
self.dkg_client
|
||||
.vote_verification_key_share(proposal, vote)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn verification_key_validation(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<(), KeyValidationError> {
|
||||
let key_validation_state = self.state.key_validation_state(epoch_id)?;
|
||||
|
||||
// check if we have already validated and voted for all keys
|
||||
if key_validation_state.completed() {
|
||||
// the only way this could be a false positive is if the chain forked and blocks got reverted,
|
||||
// but I don't think we have to worry about that
|
||||
debug!("we have already voted in all validation proposals");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !self
|
||||
.state
|
||||
.key_derivation_state(epoch_id)?
|
||||
.completed_with_success()
|
||||
{
|
||||
return Err(KeyValidationError::IncompleteKeyDerivation);
|
||||
}
|
||||
|
||||
// FAILURE CASE:
|
||||
// check if we have already verified the keys, but some voting txs either didn't get executed
|
||||
// or got executed without us knowing about it
|
||||
if !key_validation_state.votes.is_empty() {
|
||||
debug!(
|
||||
"we have already validated all keys for this epoch, but might have failed to vote"
|
||||
);
|
||||
self.resubmit_validation_votes(epoch_id).await?;
|
||||
|
||||
// if we managed to resubmit the votes (i.e. we didn't return an error)
|
||||
// it means the state is complete now
|
||||
info!("DKG: resubmitted previously generated votes - finished key validation");
|
||||
self.state.key_validation_state_mut(epoch_id)?.completed = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let votes = self.generate_votes(epoch_id).await?;
|
||||
self.state.key_validation_state_mut(epoch_id)?.votes = votes.clone();
|
||||
|
||||
// send the votes
|
||||
for (proposal, vote) in votes {
|
||||
// FUTURE OPTIMIZATION: we could batch them in a single tx
|
||||
self.dkg_client
|
||||
.vote_verification_key_share(proposal, vote)
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.state.key_validation_state_mut(epoch_id)?.completed = true;
|
||||
|
||||
info!("DKG: validated all the other verification keys");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: the following tests currently do NOT cover all cases
|
||||
// I've (@JS) only updated old, existing, tests. nothing more
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::coconut::tests::helpers::{
|
||||
derive_keypairs, exchange_dealings, initialise_controllers, initialise_dkg,
|
||||
submit_public_keys,
|
||||
};
|
||||
use cw3::Status;
|
||||
use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn validate_verification_key() -> anyhow::Result<()> {
|
||||
let validators = 4;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let epoch = chain.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
|
||||
initialise_dkg(&mut controllers, false).await;
|
||||
submit_public_keys(&mut controllers, false).await;
|
||||
exchange_dealings(&mut controllers, false).await;
|
||||
derive_keypairs(&mut controllers, false).await;
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let res = controller.verification_key_validation(epoch).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
assert!(controller.state.key_validation_state(epoch)?.completed);
|
||||
}
|
||||
|
||||
let guard = chain.lock().unwrap();
|
||||
let proposals = &guard.multisig_contract.proposals;
|
||||
assert_eq!(proposals.len(), validators);
|
||||
|
||||
for proposal in proposals.values() {
|
||||
assert_eq!(Status::Passed, proposal.status)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn validate_verification_key_malformed_share() -> anyhow::Result<()> {
|
||||
let validators = 4;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let epoch = chain.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
|
||||
initialise_dkg(&mut controllers, false).await;
|
||||
submit_public_keys(&mut controllers, false).await;
|
||||
exchange_dealings(&mut controllers, false).await;
|
||||
derive_keypairs(&mut controllers, false).await;
|
||||
|
||||
let first_dealer = controllers[0].dkg_client.get_address().await;
|
||||
|
||||
{
|
||||
let mut guard = chain.lock().unwrap();
|
||||
let shares = guard
|
||||
.dkg_contract
|
||||
.verification_shares
|
||||
.get_mut(&epoch)
|
||||
.unwrap();
|
||||
let share = shares.get_mut(first_dealer.as_ref()).unwrap();
|
||||
// mess up the share
|
||||
share.share.push('x');
|
||||
}
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let res = controller.verification_key_validation(epoch).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
assert!(controller.state.key_validation_state(epoch)?.completed);
|
||||
}
|
||||
|
||||
let guard = chain.lock().unwrap();
|
||||
let proposals = &guard.multisig_contract.proposals;
|
||||
assert_eq!(proposals.len(), validators);
|
||||
|
||||
// the proposal from the first dealer would have gotten rejected
|
||||
for proposal in proposals.values() {
|
||||
let addr = owner_from_cosmos_msgs(&proposal.msgs).unwrap();
|
||||
if addr.as_str() == first_dealer.as_ref() {
|
||||
assert_eq!(Status::Rejected, proposal.status)
|
||||
} else {
|
||||
assert_eq!(Status::Passed, proposal.status)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn validate_verification_key_unpaired_share() -> anyhow::Result<()> {
|
||||
let validators = 2;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let epoch = chain.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
|
||||
initialise_dkg(&mut controllers, false).await;
|
||||
submit_public_keys(&mut controllers, false).await;
|
||||
exchange_dealings(&mut controllers, false).await;
|
||||
derive_keypairs(&mut controllers, false).await;
|
||||
|
||||
let first_dealer = controllers[0].dkg_client.get_address().await;
|
||||
let second_dealer = controllers[1].dkg_client.get_address().await;
|
||||
|
||||
{
|
||||
let mut guard = chain.lock().unwrap();
|
||||
let shares = guard
|
||||
.dkg_contract
|
||||
.verification_shares
|
||||
.get_mut(&epoch)
|
||||
.unwrap();
|
||||
let second_share = shares.get(second_dealer.as_ref()).unwrap().clone();
|
||||
|
||||
let share = shares.get_mut(first_dealer.as_ref()).unwrap();
|
||||
// mess up the share
|
||||
share.share = second_share.share;
|
||||
}
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let res = controller.verification_key_validation(epoch).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
assert!(controller.state.key_validation_state(epoch)?.completed);
|
||||
}
|
||||
|
||||
let guard = chain.lock().unwrap();
|
||||
let proposals = &guard.multisig_contract.proposals;
|
||||
assert_eq!(proposals.len(), validators);
|
||||
|
||||
// the proposal from the first dealer would have gotten rejected
|
||||
for proposal in proposals.values() {
|
||||
let addr = owner_from_cosmos_msgs(&proposal.msgs).unwrap();
|
||||
if addr.as_str() == first_dealer.as_ref() {
|
||||
assert_eq!(Status::Rejected, proposal.status)
|
||||
} else {
|
||||
assert_eq!(Status::Passed, proposal.status)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,102 @@
|
||||
// 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 std::sync::OnceLock;
|
||||
|
||||
pub(crate) fn params() -> &'static nym_dkg::bte::Params {
|
||||
static PARAMS: OnceLock<nym_dkg::bte::Params> = OnceLock::new();
|
||||
PARAMS.get_or_init(nym_dkg::bte::setup)
|
||||
}
|
||||
|
||||
pub(crate) mod client;
|
||||
pub(crate) mod complaints;
|
||||
pub(crate) mod controller;
|
||||
pub(crate) mod dealing;
|
||||
mod helpers;
|
||||
pub(crate) mod key_derivation;
|
||||
pub(crate) mod key_finalization;
|
||||
pub(crate) mod key_validation;
|
||||
pub(crate) mod public_key;
|
||||
pub(crate) mod state;
|
||||
pub(crate) mod verification_key;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::coconut::tests::helpers::{
|
||||
derive_keypairs, exchange_dealings, finalize, init_chain, initialise_controller,
|
||||
initialise_dkg, submit_public_keys, validate_keys,
|
||||
};
|
||||
use nym_coconut::aggregate_verification_keys;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn reshare_preserves_master_key() -> anyhow::Result<()> {
|
||||
let validators = 4;
|
||||
let chain = init_chain();
|
||||
|
||||
let mut controllers = vec![];
|
||||
for i in 0..validators {
|
||||
controllers.push(initialise_controller(chain.clone(), i).await)
|
||||
}
|
||||
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let epoch = chain.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
|
||||
// EPOCH 0 DKG
|
||||
initialise_dkg(&mut controllers, false).await;
|
||||
submit_public_keys(&mut controllers, false).await;
|
||||
exchange_dealings(&mut controllers, false).await;
|
||||
derive_keypairs(&mut controllers, false).await;
|
||||
validate_keys(&mut controllers, false).await;
|
||||
finalize(&mut controllers).await;
|
||||
|
||||
// get the master key
|
||||
let mut vks = vec![];
|
||||
let mut indices = vec![];
|
||||
for controller in controllers.iter() {
|
||||
let vk = controller.unchecked_coconut_vk().await;
|
||||
let index = controller.state.assigned_index(epoch)?;
|
||||
vks.push(vk);
|
||||
indices.push(index);
|
||||
}
|
||||
let initial_first_key = vks[0].clone();
|
||||
let initial_master_vk = aggregate_verification_keys(&vks, Some(&indices))?;
|
||||
|
||||
let new_controller = initialise_controller(chain.clone(), validators).await;
|
||||
controllers.push(new_controller);
|
||||
|
||||
chain.lock().unwrap().advance_epoch_in_reshare_mode();
|
||||
|
||||
let next_epoch = epoch + 1;
|
||||
// sanity check
|
||||
assert_eq!(
|
||||
next_epoch,
|
||||
chain.lock().unwrap().dkg_contract.epoch.epoch_id
|
||||
);
|
||||
|
||||
// EPOCH 1 DKG (resharing)
|
||||
submit_public_keys(&mut controllers, true).await;
|
||||
exchange_dealings(&mut controllers, true).await;
|
||||
derive_keypairs(&mut controllers, true).await;
|
||||
validate_keys(&mut controllers, true).await;
|
||||
finalize(&mut controllers).await;
|
||||
|
||||
let mut vks = vec![];
|
||||
let mut indices = vec![];
|
||||
for controller in controllers.iter() {
|
||||
let vk = controller.unchecked_coconut_vk().await;
|
||||
let index = controller.state.assigned_index(next_epoch)?;
|
||||
vks.push(vk);
|
||||
indices.push(index);
|
||||
}
|
||||
|
||||
let updated_first_key = vks[0].clone();
|
||||
let reshared_master_vk = aggregate_verification_keys(&vks, Some(&indices))?;
|
||||
|
||||
// individual keys changed
|
||||
assert_ne!(initial_first_key, updated_first_key);
|
||||
|
||||
// but master didn't
|
||||
assert_eq!(initial_master_vk, reshared_master_vk);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,112 +1,132 @@
|
||||
// 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::dkg::client::DkgClient;
|
||||
use crate::coconut::dkg::state::State;
|
||||
use crate::coconut::dkg::controller::DkgController;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use log::debug;
|
||||
use nym_coconut_dkg_common::dealer::DealerType;
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) async fn public_key_submission(
|
||||
dkg_client: &DkgClient,
|
||||
state: &mut State,
|
||||
resharing: bool,
|
||||
) -> Result<(), CoconutError> {
|
||||
if state.was_in_progress() {
|
||||
let own_address = dkg_client.get_address().await.as_ref().to_string();
|
||||
let is_initial_dealer = dkg_client
|
||||
.get_initial_dealers()
|
||||
.await?
|
||||
.map(|data| data.initial_dealers.iter().any(|d| *d == own_address))
|
||||
.unwrap_or(false);
|
||||
let reset_coconut_keypair = !resharing || !is_initial_dealer;
|
||||
debug!(
|
||||
"Resetting state, with coconut keypair reset: {}",
|
||||
reset_coconut_keypair
|
||||
);
|
||||
state.reset_persistent(reset_coconut_keypair).await;
|
||||
}
|
||||
if state.node_index().is_some() {
|
||||
debug!("Node index was set previously, nothing to do");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let bte_key = bs58::encode(&state.dkg_keypair().public_key().to_bytes()).into_string();
|
||||
let dealer_details = dkg_client.get_self_registered_dealer_details().await?;
|
||||
let index = if let Some(details) = dealer_details.details {
|
||||
if dealer_details.dealer_type == DealerType::Past {
|
||||
// If it was a dealer in a previous epoch, re-register it for this epoch
|
||||
debug!("Registering for the current DKG round, with keys from a previous epoch");
|
||||
dkg_client
|
||||
.register_dealer(bte_key, state.announce_address().to_string(), resharing)
|
||||
.await?;
|
||||
}
|
||||
details.assigned_index
|
||||
} else {
|
||||
debug!("Registering for the first time to be a dealer");
|
||||
// First time registration
|
||||
dkg_client
|
||||
.register_dealer(bte_key, state.announce_address().to_string(), resharing)
|
||||
.await?
|
||||
};
|
||||
state.set_node_index(Some(index));
|
||||
info!("DKG: Using node index {}", index);
|
||||
|
||||
Ok(())
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PublicKeySubmissionError {
|
||||
#[error(transparent)]
|
||||
CoconutError(#[from] CoconutError),
|
||||
}
|
||||
|
||||
impl<R: RngCore + CryptoRng> DkgController<R> {
|
||||
/// First step of the DKG process during which the nym api will register for the key exchange
|
||||
/// by submitting its:
|
||||
/// - BTE public key (alongside the proof of discrete log)
|
||||
/// - ed25519 public key
|
||||
/// - announce address to be used by clients for obtaining credentials
|
||||
/// Upon successful registration, the node will receive a unique "NodeIndex"
|
||||
/// which is the x-coordinate of the to be derived keys.
|
||||
///
|
||||
/// During this step any prior coconut keys will be invalidated, i.e. keys from the previous epoch
|
||||
/// won't be used for issuing new credentials.
|
||||
///
|
||||
/// Furthermore, if the node experienced any failures during this step, a recovery will be attempted.
|
||||
pub(crate) async fn public_key_submission(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), PublicKeySubmissionError> {
|
||||
self.state.maybe_init_dkg_state(epoch_id);
|
||||
let registration_state = self.state.registration_state(epoch_id)?;
|
||||
|
||||
// check if we have already submitted the key
|
||||
if registration_state.completed() {
|
||||
// the only way this could be a false positive is if the chain forked and blocks got reverted,
|
||||
// but I don't think we have to worry about that
|
||||
debug!("we have already submitted the keys for this epoch");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// if we have coconut keys available, it means we have already completed the DKG before (in previous epoch)
|
||||
// in which case, invalidate it so that it wouldn't be used for credential issuance
|
||||
self.state.invalidate_coconut_keypair();
|
||||
|
||||
// FAILURE CASE:
|
||||
// check if we have already sent the registration transaction, but it timed out or got stuck in the mempool and
|
||||
// eventually got executed without us knowing about it
|
||||
// in that case we MUST recover the assigned index since we won't be allowed to register again
|
||||
let dealer_details = self.dkg_client.get_self_registered_dealer_details().await?;
|
||||
if dealer_details.dealer_type.is_current() {
|
||||
if let Some(details) = dealer_details.details {
|
||||
// the tx did actually go through
|
||||
self.state.registration_state_mut(epoch_id)?.assigned_index =
|
||||
Some(details.assigned_index);
|
||||
info!("DKG: recovered node index: {}", details.assigned_index);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// perform the full registration instead
|
||||
let bte_key = bs58::encode(&self.state.dkg_keypair().public_key().to_bytes()).into_string();
|
||||
let identity_key = self.state.identity_key().to_base58_string();
|
||||
let announce_address = self.state.announce_address().to_string();
|
||||
|
||||
let assigned_index = self
|
||||
.dkg_client
|
||||
.register_dealer(bte_key, identity_key, announce_address, resharing)
|
||||
.await?;
|
||||
self.state.registration_state_mut(epoch_id)?.assigned_index = Some(assigned_index);
|
||||
info!("DKG: Using node index {assigned_index}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: the following tests currently do NOT cover all cases
|
||||
// I've (@JS) only updated old, existing, tests. nothing more
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::coconut::dkg::state::PersistentState;
|
||||
use crate::coconut::tests::DummyClient;
|
||||
use crate::coconut::KeyPair;
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use rand::rngs::OsRng;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use url::Url;
|
||||
|
||||
const TEST_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
|
||||
use crate::coconut::tests::fixtures;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn submit_public_key() {
|
||||
let dkg_client = DkgClient::new(DummyClient::new(
|
||||
AccountId::from_str(TEST_VALIDATOR_ADDRESS).unwrap(),
|
||||
));
|
||||
let mut state = State::new(
|
||||
PathBuf::default(),
|
||||
PersistentState::default(),
|
||||
Url::parse("localhost:8000").unwrap(),
|
||||
DkgKeyPair::new(&nym_dkg::bte::setup(), OsRng),
|
||||
KeyPair::new(),
|
||||
);
|
||||
async fn submit_public_key() -> anyhow::Result<()> {
|
||||
let mut controller = fixtures::dkg_controller_fixture().await;
|
||||
let epoch = controller.dkg_client.get_current_epoch().await?.epoch_id;
|
||||
|
||||
assert!(dkg_client
|
||||
assert!(controller
|
||||
.dkg_client
|
||||
.get_self_registered_dealer_details()
|
||||
.await
|
||||
.unwrap()
|
||||
.await?
|
||||
.details
|
||||
.is_none());
|
||||
public_key_submission(&dkg_client, &mut state, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let client_idx = dkg_client
|
||||
controller.public_key_submission(epoch, false).await?;
|
||||
let client_idx = controller
|
||||
.dkg_client
|
||||
.get_self_registered_dealer_details()
|
||||
.await
|
||||
.unwrap()
|
||||
.await?
|
||||
.details
|
||||
.unwrap()
|
||||
.assigned_index;
|
||||
assert_eq!(state.node_index().unwrap(), client_idx);
|
||||
assert_eq!(
|
||||
controller
|
||||
.state
|
||||
.registration_state(epoch)?
|
||||
.assigned_index
|
||||
.unwrap(),
|
||||
client_idx
|
||||
);
|
||||
|
||||
// keeps the same index from chain, not calling register_dealer again
|
||||
state.set_node_index(None);
|
||||
public_key_submission(&dkg_client, &mut state, false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(state.node_index().unwrap(), client_idx);
|
||||
controller
|
||||
.state
|
||||
.registration_state_mut(epoch)?
|
||||
.assigned_index = None;
|
||||
controller.public_key_submission(epoch, false).await?;
|
||||
assert_eq!(
|
||||
controller
|
||||
.state
|
||||
.registration_state(epoch)?
|
||||
.assigned_index
|
||||
.unwrap(),
|
||||
client_idx
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::complaints::ComplaintReason;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::keypair::KeyPair as CoconutKeyPair;
|
||||
use cosmwasm_std::Addr;
|
||||
use log::debug;
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::types::EpochState;
|
||||
use nym_dkg::bte::{keys::KeyPair as DkgKeyPair, PublicKey, PublicKeyWithProof};
|
||||
use nym_dkg::{NodeIndex, RecoveredVerificationKeys, Threshold};
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use url::Url;
|
||||
|
||||
fn bte_pk_serialize<S: Serializer>(
|
||||
val: &PublicKeyWithProof,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
val.to_bytes().serialize(serializer)
|
||||
}
|
||||
|
||||
fn bte_pk_deserialize<'de, D>(deserializer: D) -> Result<PublicKeyWithProof, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let vec: Vec<u8> = Deserialize::deserialize(deserializer)?;
|
||||
PublicKeyWithProof::try_from_bytes(&vec).map_err(|err| Error::custom(format_args!("{:?}", err)))
|
||||
}
|
||||
|
||||
// note: each dealer is also a receiver which simplifies some logic significantly
|
||||
#[derive(Clone, Deserialize, Debug, Serialize)]
|
||||
pub(crate) struct DkgParticipant {
|
||||
pub(crate) _address: Addr,
|
||||
#[serde(serialize_with = "bte_pk_serialize")]
|
||||
#[serde(deserialize_with = "bte_pk_deserialize")]
|
||||
pub(crate) bte_public_key_with_proof: PublicKeyWithProof,
|
||||
pub(crate) assigned_index: NodeIndex,
|
||||
}
|
||||
|
||||
impl TryFrom<DealerDetails> for DkgParticipant {
|
||||
type Error = ComplaintReason;
|
||||
|
||||
fn try_from(dealer: DealerDetails) -> Result<Self, Self::Error> {
|
||||
let bte_public_key_with_proof = bs58::decode(dealer.bte_public_key_with_proof)
|
||||
.into_vec()
|
||||
.map(|bytes| PublicKeyWithProof::try_from_bytes(&bytes))
|
||||
.map_err(|_| ComplaintReason::MalformedBTEPublicKey)?
|
||||
.map_err(|_| ComplaintReason::MalformedBTEPublicKey)?;
|
||||
|
||||
if !bte_public_key_with_proof.verify() {
|
||||
return Err(ComplaintReason::InvalidBTEPublicKey);
|
||||
}
|
||||
|
||||
Ok(DkgParticipant {
|
||||
_address: dealer.address,
|
||||
bte_public_key_with_proof,
|
||||
assigned_index: dealer.assigned_index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub(crate) trait ConsistentState {
|
||||
fn node_index_value(&self) -> Result<NodeIndex, CoconutError>;
|
||||
fn receiver_index_value(&self) -> Result<usize, CoconutError>;
|
||||
fn threshold(&self) -> Result<Threshold, CoconutError>;
|
||||
async fn coconut_keypair_is_some(&self) -> Result<(), CoconutError>;
|
||||
fn proposal_id_value(&self) -> Result<u64, CoconutError>;
|
||||
async fn is_consistent(&self, epoch_state: EpochState) -> Result<(), CoconutError> {
|
||||
match epoch_state {
|
||||
EpochState::PublicKeySubmission { .. } => {}
|
||||
EpochState::DealingExchange { .. } => {
|
||||
self.node_index_value()?;
|
||||
}
|
||||
EpochState::VerificationKeySubmission { .. } => {
|
||||
self.receiver_index_value()?;
|
||||
self.threshold()?;
|
||||
}
|
||||
EpochState::VerificationKeyValidation { .. } => {
|
||||
self.coconut_keypair_is_some().await?;
|
||||
}
|
||||
EpochState::VerificationKeyFinalization { .. } => {
|
||||
self.proposal_id_value()?;
|
||||
}
|
||||
EpochState::InProgress => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ConsistentState for State {
|
||||
fn node_index_value(&self) -> Result<NodeIndex, CoconutError> {
|
||||
self.node_index.ok_or(CoconutError::UnrecoverableState {
|
||||
reason: String::from("Node index should have been set"),
|
||||
})
|
||||
}
|
||||
|
||||
fn receiver_index_value(&self) -> Result<usize, CoconutError> {
|
||||
self.receiver_index.ok_or(CoconutError::UnrecoverableState {
|
||||
reason: String::from("Receiver index should have been set"),
|
||||
})
|
||||
}
|
||||
|
||||
fn threshold(&self) -> Result<Threshold, CoconutError> {
|
||||
let threshold = self.threshold.ok_or(CoconutError::UnrecoverableState {
|
||||
reason: String::from("Threshold should have been set"),
|
||||
})?;
|
||||
if self.current_dealers_by_idx().len() < threshold as usize {
|
||||
Err(CoconutError::UnrecoverableState {
|
||||
reason: String::from(
|
||||
"Not enough good dealers in the signer set to achieve threshold",
|
||||
),
|
||||
})
|
||||
} else {
|
||||
Ok(threshold)
|
||||
}
|
||||
}
|
||||
|
||||
async fn coconut_keypair_is_some(&self) -> Result<(), CoconutError> {
|
||||
if self.coconut_keypair_is_some().await {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CoconutError::UnrecoverableState {
|
||||
reason: String::from("Coconut keypair should have been set"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn proposal_id_value(&self) -> Result<u64, CoconutError> {
|
||||
self.proposal_id.ok_or(CoconutError::UnrecoverableState {
|
||||
reason: String::from("Proposal id should have been set"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn vks_serialize<S: Serializer>(
|
||||
val: &[RecoveredVerificationKeys],
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let vec: Vec<Vec<u8>> = val.iter().map(|vk| vk.to_bytes()).collect();
|
||||
vec.serialize(serializer)
|
||||
}
|
||||
|
||||
fn vks_deserialize<'de, D>(deserializer: D) -> Result<Vec<RecoveredVerificationKeys>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let vec: Vec<Vec<u8>> = Deserialize::deserialize(deserializer)?;
|
||||
vec.into_iter()
|
||||
.map(|b| {
|
||||
RecoveredVerificationKeys::try_from_bytes(&b)
|
||||
.map_err(|err| D::Error::custom(format_args!("{:?}", err)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
pub(crate) struct PersistentState {
|
||||
node_index: Option<NodeIndex>,
|
||||
dealers: BTreeMap<Addr, Result<DkgParticipant, ComplaintReason>>,
|
||||
receiver_index: Option<usize>,
|
||||
threshold: Option<Threshold>,
|
||||
#[serde(serialize_with = "vks_serialize")]
|
||||
#[serde(deserialize_with = "vks_deserialize")]
|
||||
recovered_vks: Vec<RecoveredVerificationKeys>,
|
||||
proposal_id: Option<u64>,
|
||||
voted_vks: bool,
|
||||
executed_proposal: bool,
|
||||
was_in_progress: bool,
|
||||
}
|
||||
|
||||
impl From<&State> for PersistentState {
|
||||
fn from(s: &State) -> Self {
|
||||
PersistentState {
|
||||
node_index: s.node_index,
|
||||
dealers: s.dealers.clone(),
|
||||
receiver_index: s.receiver_index,
|
||||
threshold: s.threshold,
|
||||
recovered_vks: s.recovered_vks.clone(),
|
||||
proposal_id: s.proposal_id,
|
||||
voted_vks: s.voted_vks,
|
||||
executed_proposal: s.executed_proposal,
|
||||
was_in_progress: s.was_in_progress,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PersistentState {
|
||||
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), CoconutError> {
|
||||
std::fs::write(path, serde_json::to_string(self)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, CoconutError> {
|
||||
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct State {
|
||||
persistent_state_path: PathBuf,
|
||||
announce_address: Url,
|
||||
dkg_keypair: DkgKeyPair,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
node_index: Option<NodeIndex>,
|
||||
dealers: BTreeMap<Addr, Result<DkgParticipant, ComplaintReason>>,
|
||||
receiver_index: Option<usize>,
|
||||
threshold: Option<Threshold>,
|
||||
recovered_vks: Vec<RecoveredVerificationKeys>,
|
||||
proposal_id: Option<u64>,
|
||||
voted_vks: bool,
|
||||
executed_proposal: bool,
|
||||
was_in_progress: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(
|
||||
persistent_state_path: PathBuf,
|
||||
persistent_state: PersistentState,
|
||||
announce_address: Url,
|
||||
dkg_keypair: DkgKeyPair,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
) -> Self {
|
||||
State {
|
||||
persistent_state_path,
|
||||
announce_address,
|
||||
dkg_keypair,
|
||||
coconut_keypair,
|
||||
node_index: persistent_state.node_index,
|
||||
dealers: persistent_state.dealers,
|
||||
receiver_index: persistent_state.receiver_index,
|
||||
threshold: persistent_state.threshold,
|
||||
recovered_vks: persistent_state.recovered_vks,
|
||||
proposal_id: persistent_state.proposal_id,
|
||||
voted_vks: persistent_state.voted_vks,
|
||||
executed_proposal: persistent_state.executed_proposal,
|
||||
was_in_progress: persistent_state.was_in_progress,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reset_persistent(&mut self, reset_coconut_keypair: bool) {
|
||||
if reset_coconut_keypair {
|
||||
self.coconut_keypair.set(None).await;
|
||||
}
|
||||
self.node_index = Default::default();
|
||||
self.dealers = Default::default();
|
||||
self.receiver_index = Default::default();
|
||||
self.threshold = Default::default();
|
||||
self.recovered_vks = Default::default();
|
||||
self.proposal_id = Default::default();
|
||||
self.voted_vks = Default::default();
|
||||
self.executed_proposal = Default::default();
|
||||
self.was_in_progress = Default::default();
|
||||
}
|
||||
|
||||
pub fn persistent_state_path(&self) -> PathBuf {
|
||||
self.persistent_state_path.clone()
|
||||
}
|
||||
|
||||
pub fn announce_address(&self) -> &Url {
|
||||
&self.announce_address
|
||||
}
|
||||
|
||||
pub fn dkg_keypair(&self) -> &DkgKeyPair {
|
||||
&self.dkg_keypair
|
||||
}
|
||||
|
||||
pub async fn coconut_keypair_is_some(&self) -> bool {
|
||||
self.coconut_keypair.get().await.is_some()
|
||||
}
|
||||
|
||||
pub async fn take_coconut_keypair(&self) -> Option<nym_coconut::KeyPair> {
|
||||
self.coconut_keypair.take().await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn coconut_keypair(
|
||||
&self,
|
||||
) -> tokio::sync::RwLockReadGuard<'_, Option<nym_coconut::KeyPair>> {
|
||||
self.coconut_keypair.get().await
|
||||
}
|
||||
|
||||
pub fn node_index(&self) -> Option<NodeIndex> {
|
||||
self.node_index
|
||||
}
|
||||
|
||||
pub fn receiver_index(&self) -> Option<usize> {
|
||||
self.receiver_index
|
||||
}
|
||||
|
||||
pub fn current_dealers_by_addr(&self) -> BTreeMap<Addr, NodeIndex> {
|
||||
self.dealers
|
||||
.iter()
|
||||
.filter_map(|(addr, dealer)| {
|
||||
dealer
|
||||
.as_ref()
|
||||
.ok()
|
||||
.map(|participant| (addr.clone(), participant.assigned_index))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn current_dealers_by_idx(&self) -> BTreeMap<NodeIndex, PublicKey> {
|
||||
self.dealers
|
||||
.iter()
|
||||
.filter_map(|(_, dealer)| {
|
||||
dealer.as_ref().ok().map(|participant| {
|
||||
(
|
||||
participant.assigned_index,
|
||||
*participant.bte_public_key_with_proof.public_key(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn recovered_vks(&self) -> &Vec<RecoveredVerificationKeys> {
|
||||
&self.recovered_vks
|
||||
}
|
||||
|
||||
pub fn voted_vks(&self) -> bool {
|
||||
self.voted_vks
|
||||
}
|
||||
|
||||
pub fn executed_proposal(&self) -> bool {
|
||||
self.executed_proposal
|
||||
}
|
||||
|
||||
pub fn was_in_progress(&self) -> bool {
|
||||
self.was_in_progress
|
||||
}
|
||||
|
||||
pub fn set_recovered_vks(&mut self, recovered_vks: Vec<RecoveredVerificationKeys>) {
|
||||
self.recovered_vks = recovered_vks;
|
||||
}
|
||||
|
||||
pub async fn set_coconut_keypair(
|
||||
&mut self,
|
||||
coconut_keypair: Option<nym_coconut_interface::KeyPair>,
|
||||
) {
|
||||
self.coconut_keypair.set(coconut_keypair).await
|
||||
}
|
||||
|
||||
pub fn set_node_index(&mut self, node_index: Option<NodeIndex>) {
|
||||
self.node_index = node_index;
|
||||
}
|
||||
|
||||
pub fn set_dealers(&mut self, dealers: Vec<DealerDetails>) {
|
||||
self.dealers = BTreeMap::from_iter(
|
||||
dealers
|
||||
.into_iter()
|
||||
.map(|details| (details.address.clone(), DkgParticipant::try_from(details))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn mark_bad_dealer(&mut self, dealer_addr: &Addr, reason: ComplaintReason) {
|
||||
if let Some((_, value)) = self
|
||||
.dealers
|
||||
.iter_mut()
|
||||
.find(|(addr, _)| *addr == dealer_addr)
|
||||
{
|
||||
debug!(
|
||||
"Dealer {} misbehaved: {:?}. It will be marked locally as bad dealer and ignored",
|
||||
dealer_addr, reason
|
||||
);
|
||||
*value = Err(reason);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_receiver_index(&mut self, receiver_index: Option<usize>) {
|
||||
self.receiver_index = receiver_index;
|
||||
}
|
||||
|
||||
pub fn set_threshold(&mut self, threshold: Option<Threshold>) {
|
||||
self.threshold = threshold;
|
||||
}
|
||||
|
||||
pub fn set_proposal_id(&mut self, proposal_id: u64) {
|
||||
self.proposal_id = Some(proposal_id);
|
||||
}
|
||||
|
||||
pub fn set_voted_vks(&mut self) {
|
||||
self.voted_vks = true;
|
||||
}
|
||||
|
||||
pub fn set_executed_proposal(&mut self) {
|
||||
self.executed_proposal = true;
|
||||
}
|
||||
|
||||
pub fn set_was_in_progress(&mut self) {
|
||||
self.was_in_progress = true;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn all_dealers(&self) -> &BTreeMap<Addr, Result<DkgParticipant, ComplaintReason>> {
|
||||
&self.dealers
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::serde_helpers::generated_dealings;
|
||||
use crate::coconut::dkg::state::DkgParticipant;
|
||||
use nym_coconut_dkg_common::types::DealingIndex;
|
||||
use nym_dkg::{Dealing, NodeIndex};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct DealingExchangeState {
|
||||
pub(crate) dealers: BTreeMap<NodeIndex, DkgParticipant>,
|
||||
|
||||
#[serde(with = "generated_dealings")]
|
||||
pub(crate) generated_dealings: HashMap<DealingIndex, Dealing>,
|
||||
|
||||
pub(crate) receiver_index: Option<usize>,
|
||||
|
||||
pub(crate) completed: bool,
|
||||
}
|
||||
|
||||
impl DealingExchangeState {
|
||||
/// Specifies whether this dealer has already shared dealings in this DKG epoch
|
||||
pub fn completed(&self) -> bool {
|
||||
self.completed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct InProgressState {
|
||||
// indicate whether this node has been in this state before and performed any one-off tasks
|
||||
pub(crate) entered: bool,
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::serde_helpers::recovered_keys;
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut_dkg_common::types::{DealingIndex, EpochId};
|
||||
use nym_dkg::{G2Projective, NodeIndex, RecoveredVerificationKeys, Threshold};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use thiserror::Error;
|
||||
|
||||
type ReceiverIndex = usize;
|
||||
|
||||
#[derive(Debug, Clone, Error, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DerivationFailure {
|
||||
#[error("there were no valid dealings in epoch {epoch_id}")]
|
||||
NoValidDealings { epoch_id: EpochId },
|
||||
|
||||
#[error("did not receive sufficient number of dealings for key derivation. got {available} per key fragment whislt the threshold is {threshold}")]
|
||||
InsufficientNumberOfDealings {
|
||||
available: usize,
|
||||
threshold: Threshold,
|
||||
},
|
||||
|
||||
#[error("could not recover partial verification keys for index {dealing_index}: {err_msg}")]
|
||||
KeyRecoveryFailure {
|
||||
dealing_index: DealingIndex,
|
||||
err_msg: String,
|
||||
},
|
||||
|
||||
#[error("could not decrypt share at index {dealing_index} generated by dealer at index {dealer_index}: {err_msg}")]
|
||||
ShareDecryptionFailure {
|
||||
dealing_index: DealingIndex,
|
||||
dealer_index: NodeIndex,
|
||||
err_msg: String,
|
||||
},
|
||||
|
||||
#[error("the derived verification key does not match the expected partial elements")]
|
||||
MismatchedPartialKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DealerRejectionReason {
|
||||
#[error("no dealings were provided")]
|
||||
NoDealingsProvided,
|
||||
|
||||
#[error("insufficient number of dealings was provided. got {got} but expected {expected}")]
|
||||
InsufficientNumberOfDealingsProvided { got: usize, expected: usize },
|
||||
|
||||
#[error("no [verified] verification key from the previous epoch was available")]
|
||||
MissingVerifiedLastEpochKey,
|
||||
|
||||
#[error("the key size from the previous epoch does not match the resharing dealing requirements: {key_size} vs {expected}")]
|
||||
LastEpochKeyOfWrongSize { key_size: usize, expected: usize },
|
||||
|
||||
#[error("the dealing at index {index} is malformed: {err_msg}")]
|
||||
MalformedDealing {
|
||||
index: DealingIndex,
|
||||
err_msg: String,
|
||||
},
|
||||
|
||||
#[error("the dealing at index {index} is [cryptographically] invalid: {err_msg}")]
|
||||
InvalidDealing {
|
||||
index: DealingIndex,
|
||||
err_msg: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct KeyDerivationState {
|
||||
pub(crate) expected_threshold: Option<Threshold>,
|
||||
|
||||
#[serde(with = "recovered_keys")]
|
||||
pub(crate) derived_partials: BTreeMap<DealingIndex, RecoveredVerificationKeys>,
|
||||
|
||||
pub(crate) rejected_dealers: HashMap<Addr, DealerRejectionReason>,
|
||||
|
||||
pub(crate) proposal_id: Option<u64>,
|
||||
|
||||
pub(crate) completed: Option<Result<(), DerivationFailure>>,
|
||||
}
|
||||
|
||||
impl KeyDerivationState {
|
||||
pub fn derived_partials_for(&self, receiver_index: ReceiverIndex) -> Option<Vec<G2Projective>> {
|
||||
let mut recovered = Vec::new();
|
||||
for keys in self.derived_partials.values() {
|
||||
// SAFETY:
|
||||
// make sure the receiver index of this receiver/dealer is within the size of the derived keys
|
||||
if keys.recovered_partials.len() <= receiver_index {
|
||||
return None;
|
||||
};
|
||||
recovered.push(keys.recovered_partials[receiver_index])
|
||||
}
|
||||
Some(recovered)
|
||||
}
|
||||
|
||||
pub fn completed_with_success(&self) -> bool {
|
||||
matches!(self.completed, Some(Ok(_)))
|
||||
}
|
||||
|
||||
pub fn completion_failure(&self) -> Option<DerivationFailure> {
|
||||
self.completed.clone().and_then(Result::err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct FinalizationState {
|
||||
pub(crate) completed: bool,
|
||||
}
|
||||
|
||||
impl FinalizationState {
|
||||
/// Specifies whether this (or another) dealer has already executed its verification proposal
|
||||
pub fn completed(&self) -> bool {
|
||||
self.completed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
type ProposalId = u64;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct ValidationState {
|
||||
pub votes: HashMap<ProposalId, bool>,
|
||||
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl ValidationState {
|
||||
/// Specifies whether this dealer has already registered in the particular DKG epoch
|
||||
pub fn completed(&self) -> bool {
|
||||
self.completed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::state::dealing_exchange::DealingExchangeState;
|
||||
use crate::coconut::dkg::state::in_progress::InProgressState;
|
||||
use crate::coconut::dkg::state::key_derivation::KeyDerivationState;
|
||||
use crate::coconut::dkg::state::key_finalization::FinalizationState;
|
||||
use crate::coconut::dkg::state::key_validation::ValidationState;
|
||||
use crate::coconut::dkg::state::registration::{
|
||||
DkgParticipant, ParticipantState, RegistrationState,
|
||||
};
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch};
|
||||
use cosmwasm_std::Addr;
|
||||
use log::debug;
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use nym_dkg::{bte, NodeIndex, Threshold};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::{Path, PathBuf};
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
pub(crate) mod dealing_exchange;
|
||||
pub(crate) mod in_progress;
|
||||
pub(crate) mod key_derivation;
|
||||
pub(crate) mod key_finalization;
|
||||
pub(crate) mod key_validation;
|
||||
pub(crate) mod registration;
|
||||
pub(crate) mod serde_helpers;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub(crate) struct PersistentState {
|
||||
timestamp: OffsetDateTime,
|
||||
|
||||
dkg_instances: HashMap<EpochId, DkgState>,
|
||||
}
|
||||
|
||||
impl Default for PersistentState {
|
||||
fn default() -> Self {
|
||||
PersistentState {
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
|
||||
dkg_instances: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&State> for PersistentState {
|
||||
fn from(s: &State) -> Self {
|
||||
PersistentState {
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
|
||||
dkg_instances: s.dkg_instances.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PersistentState {
|
||||
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), CoconutError> {
|
||||
debug!("persisting the dkg state");
|
||||
std::fs::write(path, serde_json::to_string(self)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, CoconutError> {
|
||||
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct DkgState {
|
||||
pub(crate) registration: RegistrationState,
|
||||
|
||||
pub(crate) dealing_exchange: DealingExchangeState,
|
||||
|
||||
pub(crate) key_generation: KeyDerivationState,
|
||||
|
||||
pub(crate) key_validation: ValidationState,
|
||||
|
||||
pub(crate) key_finalization: FinalizationState,
|
||||
|
||||
pub(crate) in_progress: InProgressState,
|
||||
}
|
||||
|
||||
impl DkgState {
|
||||
pub(crate) fn set_dealers(&mut self, raw_dealers: Vec<DealerDetails>) {
|
||||
assert!(self.dealing_exchange.dealers.is_empty());
|
||||
for raw_dealer in raw_dealers {
|
||||
let dkg_participant = DkgParticipant::from(raw_dealer);
|
||||
let address = &dkg_participant.address;
|
||||
if let ParticipantState::Invalid(rejection) = &dkg_participant.state {
|
||||
warn!("{address} dealer is malformed: {rejection}",)
|
||||
}
|
||||
self.dealing_exchange
|
||||
.dealers
|
||||
.insert(dkg_participant.assigned_index, dkg_participant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct State {
|
||||
/// Path to the file containing the persistent state
|
||||
persistent_state_path: PathBuf,
|
||||
|
||||
dkg_instances: HashMap<EpochId, DkgState>,
|
||||
|
||||
announce_address: Url,
|
||||
|
||||
identity_key: identity::PublicKey,
|
||||
|
||||
dkg_keypair: DkgKeyPair,
|
||||
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(
|
||||
persistent_state_path: PathBuf,
|
||||
persistent_state: PersistentState,
|
||||
announce_address: Url,
|
||||
dkg_keypair: DkgKeyPair,
|
||||
identity_key: identity::PublicKey,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
) -> Self {
|
||||
State {
|
||||
persistent_state_path,
|
||||
dkg_instances: persistent_state.dkg_instances,
|
||||
announce_address,
|
||||
identity_key,
|
||||
dkg_keypair,
|
||||
coconut_keypair,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn persist(&self) -> Result<(), CoconutError> {
|
||||
PersistentState::from(self).save_to_file(self.persistent_state_path())
|
||||
}
|
||||
|
||||
pub fn clear_previous_epoch(&mut self, current_epoch: EpochId) {
|
||||
if let Some(previous) = current_epoch.checked_sub(1) {
|
||||
self.dkg_instances.remove(&previous);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_init_dkg_state(&mut self, epoch_id: EpochId) {
|
||||
// given we're not using that entry here, I think the explicit check and insert is more readable
|
||||
#[allow(clippy::map_entry)]
|
||||
if !self.dkg_instances.contains_key(&epoch_id) {
|
||||
self.dkg_instances.insert(epoch_id, Default::default());
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtain the list of dealers for the provided epoch that have submitted valid public keys.
|
||||
pub fn valid_epoch_receivers(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Vec<(Addr, NodeIndex)>, CoconutError> {
|
||||
Ok(self
|
||||
.dealing_exchange_state(epoch_id)?
|
||||
.dealers
|
||||
.values()
|
||||
.filter_map(|d| {
|
||||
if d.state.is_valid() {
|
||||
Some((d.address.clone(), d.assigned_index))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Filters out DKG participants based on whether they submitted valid public key
|
||||
pub fn valid_epoch_receivers_keys(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<BTreeMap<NodeIndex, bte::PublicKey>, CoconutError> {
|
||||
Ok(self
|
||||
.dealing_exchange_state(epoch_id)?
|
||||
.dealers
|
||||
.values()
|
||||
.filter_map(|d| d.state.public_key().map(|k| (d.assigned_index, k)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn dkg_state(&self, epoch_id: EpochId) -> Result<&DkgState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn dkg_state_mut(&mut self, epoch_id: EpochId) -> Result<&mut DkgState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn registration_state(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&RegistrationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.map(|state| &state.registration)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn registration_state_mut(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&mut RegistrationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.map(|state| &mut state.registration)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn in_progress_state(&self, epoch_id: EpochId) -> Result<&InProgressState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.map(|state| &state.in_progress)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn in_progress_state_mut(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&mut InProgressState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.map(|state| &mut state.in_progress)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn dealing_exchange_state(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&DealingExchangeState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.map(|state| &state.dealing_exchange)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn dealing_exchange_state_mut(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&mut DealingExchangeState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.map(|state| &mut state.dealing_exchange)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn key_derivation_state(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&KeyDerivationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.map(|state| &state.key_generation)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn key_derivation_state_mut(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&mut KeyDerivationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.map(|state| &mut state.key_generation)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn key_validation_state(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&ValidationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.map(|state| &state.key_validation)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn key_validation_state_mut(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&mut ValidationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.map(|state| &mut state.key_validation)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn key_finalization_state(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&FinalizationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.map(|state| &state.key_finalization)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn key_finalization_state_mut(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&mut FinalizationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.map(|state| &mut state.key_finalization)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn threshold(&self, epoch_id: EpochId) -> Result<Threshold, CoconutError> {
|
||||
self.key_derivation_state(epoch_id)?
|
||||
.expected_threshold
|
||||
.ok_or(CoconutError::UnavailableThreshold { epoch_id })
|
||||
}
|
||||
|
||||
pub fn assigned_index(&self, epoch_id: EpochId) -> Result<NodeIndex, CoconutError> {
|
||||
self.registration_state(epoch_id)?
|
||||
.assigned_index
|
||||
.ok_or(CoconutError::UnavailableAssignedIndex { epoch_id })
|
||||
}
|
||||
|
||||
pub fn receiver_index(&self, epoch_id: EpochId) -> Result<usize, CoconutError> {
|
||||
self.dealing_exchange_state(epoch_id)?
|
||||
.receiver_index
|
||||
.ok_or(CoconutError::UnavailableReceiverIndex { epoch_id })
|
||||
}
|
||||
|
||||
pub fn proposal_id(&self, epoch_id: EpochId) -> Result<u64, CoconutError> {
|
||||
self.key_derivation_state(epoch_id)?
|
||||
.proposal_id
|
||||
.ok_or(CoconutError::UnavailableProposalId { epoch_id })
|
||||
}
|
||||
|
||||
pub fn persistent_state_path(&self) -> &Path {
|
||||
self.persistent_state_path.as_path()
|
||||
}
|
||||
|
||||
pub fn announce_address(&self) -> &Url {
|
||||
&self.announce_address
|
||||
}
|
||||
|
||||
pub fn identity_key(&self) -> identity::PublicKey {
|
||||
self.identity_key
|
||||
}
|
||||
|
||||
pub fn dkg_keypair(&self) -> &DkgKeyPair {
|
||||
&self.dkg_keypair
|
||||
}
|
||||
|
||||
pub async fn coconut_keypair_is_some(&self) -> bool {
|
||||
self.coconut_keypair.get().await.is_some()
|
||||
}
|
||||
|
||||
pub async fn take_coconut_keypair(&self) -> Option<KeyPairWithEpoch> {
|
||||
self.coconut_keypair.take().await
|
||||
}
|
||||
|
||||
pub fn invalidate_coconut_keypair(&self) {
|
||||
self.coconut_keypair.invalidate()
|
||||
}
|
||||
|
||||
pub fn validate_coconut_keypair(&self) {
|
||||
self.coconut_keypair.validate()
|
||||
}
|
||||
|
||||
pub async fn unchecked_coconut_keypair(
|
||||
&self,
|
||||
) -> tokio::sync::RwLockReadGuard<'_, Option<KeyPairWithEpoch>> {
|
||||
self.coconut_keypair.read_keys().await
|
||||
}
|
||||
|
||||
pub async fn set_coconut_keypair(&mut self, coconut_keypair: KeyPairWithEpoch) {
|
||||
self.coconut_keypair.set(coconut_keypair).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::state::serde_helpers::bte_pk_serde;
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::types::EncodedBTEPublicKeyWithProof;
|
||||
use nym_dkg::bte::PublicKeyWithProof;
|
||||
use nym_dkg::{bte, NodeIndex};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Deserialize, Debug, Serialize)]
|
||||
pub(crate) enum ParticipantState {
|
||||
Invalid(KeyRejectionReason),
|
||||
VerifiedKey(#[serde(with = "bte_pk_serde")] Box<PublicKeyWithProof>),
|
||||
}
|
||||
|
||||
impl ParticipantState {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
matches!(self, ParticipantState::VerifiedKey(..))
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> Option<bte::PublicKey> {
|
||||
match self {
|
||||
ParticipantState::Invalid(_) => None,
|
||||
ParticipantState::VerifiedKey(key_with_proof) => Some(*key_with_proof.public_key()),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_raw_encoded_key(raw: EncodedBTEPublicKeyWithProof) -> Self {
|
||||
let bytes = match bs58::decode(raw).into_vec() {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => {
|
||||
return ParticipantState::Invalid(
|
||||
KeyRejectionReason::MalformedBTEPublicKeyEncoding {
|
||||
err_msg: err.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let key = match PublicKeyWithProof::try_from_bytes(&bytes) {
|
||||
Ok(key) => key,
|
||||
Err(err) => {
|
||||
return ParticipantState::Invalid(KeyRejectionReason::MalformedBTEPublicKey {
|
||||
err_msg: err.to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if !key.verify() {
|
||||
return ParticipantState::Invalid(KeyRejectionReason::InvalidBTEPublicKey);
|
||||
}
|
||||
|
||||
ParticipantState::VerifiedKey(Box::new(key))
|
||||
}
|
||||
}
|
||||
|
||||
// note: each dealer is also a receiver which simplifies some logic significantly
|
||||
#[derive(Clone, Deserialize, Debug, Serialize)]
|
||||
pub(crate) struct DkgParticipant {
|
||||
pub(crate) address: Addr,
|
||||
pub(crate) assigned_index: NodeIndex,
|
||||
pub(crate) state: ParticipantState,
|
||||
}
|
||||
|
||||
impl From<DealerDetails> for DkgParticipant {
|
||||
fn from(dealer: DealerDetails) -> Self {
|
||||
DkgParticipant {
|
||||
address: dealer.address,
|
||||
state: ParticipantState::from_raw_encoded_key(dealer.bte_public_key_with_proof),
|
||||
assigned_index: dealer.assigned_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DkgParticipant {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn unwrap_key(&self) -> PublicKeyWithProof {
|
||||
if let ParticipantState::VerifiedKey(key) = &self.state {
|
||||
return *key.clone();
|
||||
}
|
||||
panic!("no key")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn unwrap_rejection(&self) -> KeyRejectionReason {
|
||||
if let ParticipantState::Invalid(rejection) = &self.state {
|
||||
return rejection.clone();
|
||||
}
|
||||
panic!("not rejected")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Error, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum KeyRejectionReason {
|
||||
#[error("provided BTE Public key encoding is malformed: {err_msg}")]
|
||||
MalformedBTEPublicKeyEncoding { err_msg: String },
|
||||
|
||||
#[error("provided BTE Public key has invalid byte representation: {err_msg}")]
|
||||
MalformedBTEPublicKey { err_msg: String },
|
||||
|
||||
#[error("provided BTE public key does not verify correctly")]
|
||||
InvalidBTEPublicKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct RegistrationState {
|
||||
pub(crate) assigned_index: Option<NodeIndex>,
|
||||
}
|
||||
|
||||
impl RegistrationState {
|
||||
/// Specifies whether this dealer has already registered in the particular DKG epoch
|
||||
pub fn completed(&self) -> bool {
|
||||
self.assigned_index.is_some()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub(super) mod bte_pk_serde {
|
||||
use nym_dkg::bte::PublicKeyWithProof;
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub fn serialize<S: Serializer>(
|
||||
val: &PublicKeyWithProof,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
val.to_bytes().serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<PublicKeyWithProof>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let vec: Vec<u8> = Deserialize::deserialize(deserializer)?;
|
||||
PublicKeyWithProof::try_from_bytes(&vec)
|
||||
.map_err(|err| Error::custom(format_args!("{:?}", err)))
|
||||
.map(Box::new)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) mod recovered_keys {
|
||||
use nym_coconut_dkg_common::types::DealingIndex;
|
||||
use nym_dkg::RecoveredVerificationKeys;
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
type Helper = BTreeMap<DealingIndex, Vec<u8>>;
|
||||
|
||||
pub fn serialize<S: Serializer>(
|
||||
val: &BTreeMap<DealingIndex, RecoveredVerificationKeys>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let helper: Helper = val
|
||||
.iter()
|
||||
.map(|(idx, rec)| (*idx, rec.to_bytes()))
|
||||
.collect();
|
||||
helper.serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<BTreeMap<DealingIndex, RecoveredVerificationKeys>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let helper = Helper::deserialize(deserializer)?;
|
||||
helper
|
||||
.into_iter()
|
||||
.map(|(idx, rec)| {
|
||||
RecoveredVerificationKeys::try_from_bytes(&rec)
|
||||
.map_err(|err| D::Error::custom(format_args!("{:?}", err)))
|
||||
.map(|vk| (idx, vk))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) mod generated_dealings {
|
||||
use nym_coconut_dkg_common::types::DealingIndex;
|
||||
use nym_dkg::Dealing;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn serialize<S: Serializer>(
|
||||
dealings: &HashMap<DealingIndex, Dealing>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let mut helper = HashMap::new();
|
||||
|
||||
for (dealing_index, dealing) in dealings {
|
||||
helper.insert(*dealing_index, dealing.to_bytes());
|
||||
}
|
||||
|
||||
helper.serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<HashMap<DealingIndex, Dealing>, D::Error> {
|
||||
<HashMap<DealingIndex, Vec<u8>>>::deserialize(deserializer)?
|
||||
.into_iter()
|
||||
.map(|(index, raw_dealing)| {
|
||||
Dealing::try_from_bytes(&raw_dealing)
|
||||
.map_err(serde::de::Error::custom)
|
||||
.map(|dealing| (index, dealing))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +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 rocket::http::{ContentType, Status};
|
||||
use rocket::response::Responder;
|
||||
use rocket::{response, Request, Response};
|
||||
use std::io::Cursor;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::node_status_api::models::NymApiStorageError;
|
||||
use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId};
|
||||
use nym_crypto::asymmetric::{
|
||||
encryption::KeyRecoveryError,
|
||||
identity::{Ed25519RecoveryError, SignatureError},
|
||||
@@ -14,8 +10,11 @@ use nym_crypto::asymmetric::{
|
||||
use nym_dkg::error::DkgError;
|
||||
use nym_validator_client::coconut::CoconutApiError;
|
||||
use nym_validator_client::nyxd::error::{NyxdError, TendermintError};
|
||||
|
||||
use crate::node_status_api::models::NymApiStorageError;
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::response::Responder;
|
||||
use rocket::{response, Request, Response};
|
||||
use std::io::Cursor;
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, CoconutError>;
|
||||
|
||||
@@ -124,6 +123,31 @@ pub enum CoconutError {
|
||||
// I guess we should make this one a bit more detailed
|
||||
#[error("the provided query arguments were invalid")]
|
||||
InvalidQueryArguments,
|
||||
|
||||
#[error("the internal dkg state for epoch {epoch_id} is missing - we might have joined mid exchange")]
|
||||
MissingDkgState { epoch_id: EpochId },
|
||||
|
||||
#[error(
|
||||
"the node index value for epoch {epoch_id} is not available - are you sure we are a dealer?"
|
||||
)]
|
||||
UnavailableAssignedIndex { epoch_id: EpochId },
|
||||
|
||||
#[error("the receiver index value for epoch {epoch_id} is not available - are you sure we are a receiver?")]
|
||||
UnavailableReceiverIndex { epoch_id: EpochId },
|
||||
|
||||
#[error("the threshold value for epoch {epoch_id} is not available")]
|
||||
UnavailableThreshold { epoch_id: EpochId },
|
||||
|
||||
#[error("the proposal id value for epoch {epoch_id} is not available")]
|
||||
UnavailableProposalId { epoch_id: EpochId },
|
||||
|
||||
#[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,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::state::BANDWIDTH_CREDENTIAL_PARAMS;
|
||||
use crate::coconut::state::bandwidth_voucher_params;
|
||||
use nym_api_requests::coconut::BlindSignRequestBody;
|
||||
use nym_coconut::{BlindedSignature, SecretKey};
|
||||
use nym_validator_client::nyxd::error::NyxdError::AbciError;
|
||||
@@ -29,7 +29,7 @@ pub(crate) fn blind_sign(
|
||||
let attributes_ref = public_attributes.iter().collect::<Vec<_>>();
|
||||
|
||||
Ok(nym_coconut_interface::blind_sign(
|
||||
&BANDWIDTH_CREDENTIAL_PARAMS,
|
||||
bandwidth_voucher_params(),
|
||||
signing_key,
|
||||
&request.inner_sign_request,
|
||||
&attributes_ref,
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeyPair {
|
||||
inner: Arc<RwLock<Option<nym_coconut_interface::KeyPair>>>,
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn take(&self) -> Option<nym_coconut::KeyPair> {
|
||||
self.inner.write().await.take()
|
||||
}
|
||||
|
||||
pub async fn get(&self) -> RwLockReadGuard<'_, Option<nym_coconut::KeyPair>> {
|
||||
self.inner.read().await
|
||||
}
|
||||
|
||||
pub async fn set(&self, keypair: Option<nym_coconut_interface::KeyPair>) {
|
||||
let mut w_lock = self.inner.write().await;
|
||||
*w_lock = keypair;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_dkg::Scalar;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
|
||||
mod persistence;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeyPair {
|
||||
// keys: Arc<RwLock<HashMap<EpochId, nym_coconut_interface::KeyPair>>>,
|
||||
keys: Arc<RwLock<Option<KeyPairWithEpoch>>>,
|
||||
valid: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KeyPairWithEpoch {
|
||||
pub(crate) keys: nym_coconut_interface::KeyPair,
|
||||
pub(crate) issued_for_epoch: EpochId,
|
||||
}
|
||||
|
||||
impl KeyPairWithEpoch {
|
||||
pub(crate) fn new(keys: nym_coconut_interface::KeyPair, issued_for_epoch: EpochId) -> Self {
|
||||
KeyPairWithEpoch {
|
||||
keys,
|
||||
issued_for_epoch,
|
||||
}
|
||||
}
|
||||
|
||||
// extract underlying secrets from the coconut's secret key.
|
||||
// the caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized
|
||||
// `KeyPair` and `SecretKey` implement ZeroizeOnDrop; `Scalar` does not (it implements `Copy` -> important to keep in mind)
|
||||
pub(crate) fn hazmat_into_secrets(self) -> Vec<Scalar> {
|
||||
let (x, mut secrets) = self.keys.secret_key().hazmat_to_raw();
|
||||
|
||||
secrets.insert(0, x);
|
||||
secrets
|
||||
// since `nym_coconut_interface::KeyPair` implements `ZeroizeOnDrop` and we took ownership of the keypair,
|
||||
// it will get zeroized after we exit this scope
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
keys: Arc::new(RwLock::new(None)),
|
||||
valid: Arc::new(Default::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn take(&self) -> Option<KeyPairWithEpoch> {
|
||||
self.keys.write().await.take()
|
||||
}
|
||||
|
||||
pub async fn get(&self) -> Option<RwLockReadGuard<'_, Option<KeyPairWithEpoch>>> {
|
||||
if self.is_valid() {
|
||||
Some(self.read_keys().await)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_keys(&self) -> RwLockReadGuard<'_, Option<KeyPairWithEpoch>> {
|
||||
self.keys.read().await
|
||||
}
|
||||
|
||||
pub async fn set(&self, keypair: KeyPairWithEpoch) {
|
||||
let mut w_lock = self.keys.write().await;
|
||||
*w_lock = Some(keypair);
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.valid.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn validate(&self) {
|
||||
self.valid.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn invalidate(&self) {
|
||||
self.valid.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::keys::KeyPairWithEpoch;
|
||||
use crate::coconut::state::bandwidth_voucher_params;
|
||||
use nym_coconut::{CoconutError, KeyPair, SecretKey};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_pemstore::traits::PemStorableKey;
|
||||
use std::mem;
|
||||
|
||||
impl PemStorableKey for KeyPairWithEpoch {
|
||||
// that's not the best error for this, but it felt like an overkill to define a dedicated struct just for this purpose
|
||||
type Error = CoconutError;
|
||||
|
||||
fn pem_type() -> &'static str {
|
||||
"COCONUT KEY WITH EPOCH"
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = self.issued_for_epoch.to_be_bytes().to_vec();
|
||||
bytes.append(&mut self.keys.secret_key().to_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
if bytes.len() <= mem::size_of::<EpochId>() {
|
||||
return Err(CoconutError::Deserialization(
|
||||
"insufficient number of bytes to decode secret key with epoch id".into(),
|
||||
));
|
||||
}
|
||||
let epoch_id = EpochId::from_be_bytes([
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
]);
|
||||
|
||||
let sk = SecretKey::from_bytes(&bytes[mem::size_of::<EpochId>()..])?;
|
||||
let vk = sk.verification_key(bandwidth_voucher_params());
|
||||
|
||||
Ok(KeyPairWithEpoch {
|
||||
keys: KeyPair::from_keys(sk, vk),
|
||||
issued_for_epoch: epoch_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use self::comm::APICommunicationChannel;
|
||||
use crate::coconut::client::Client as LocalClient;
|
||||
use crate::coconut::state::State;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use keypair::KeyPair;
|
||||
use keys::KeyPair;
|
||||
use nym_config::defaults::NYM_API_VERSION;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_validator_client::nym_api::routes::{BANDWIDTH, COCONUT_ROUTES};
|
||||
@@ -18,7 +18,7 @@ mod deposit;
|
||||
pub(crate) mod dkg;
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod keypair;
|
||||
pub(crate) mod keys;
|
||||
pub(crate) mod state;
|
||||
pub(crate) mod storage;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -5,10 +5,9 @@ use crate::coconut::client::Client as LocalClient;
|
||||
use crate::coconut::comm::APICommunicationChannel;
|
||||
use crate::coconut::deposit::validate_deposit_tx;
|
||||
use crate::coconut::error::Result;
|
||||
use crate::coconut::keypair::KeyPair;
|
||||
use crate::coconut::keys::KeyPair;
|
||||
use crate::coconut::storage::CoconutStorageExt;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use lazy_static::lazy_static;
|
||||
use nym_api_requests::coconut::helpers::issued_credential_plaintext;
|
||||
use nym_api_requests::coconut::BlindSignRequestBody;
|
||||
use nym_coconut::Parameters;
|
||||
@@ -17,16 +16,16 @@ use nym_coconut_interface::{BlindedSignature, VerificationKey};
|
||||
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_validator_client::nyxd::{Hash, TxResponse};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
// keep it as a global static due to relatively high cost of computing the curve points;
|
||||
// plus we expect all clients to use the same set of parameters
|
||||
//
|
||||
// future note: once we allow for credentials with variable number of attributes, just create Parameters(max_allowed_attributes)
|
||||
// and take as many hs elements as required (since they will match for all variants)
|
||||
lazy_static! {
|
||||
pub(crate) static ref BANDWIDTH_CREDENTIAL_PARAMS: Parameters =
|
||||
BandwidthVoucher::default_parameters();
|
||||
pub(crate) fn bandwidth_voucher_params() -> &'static Parameters {
|
||||
static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock<Parameters> = OnceLock::new();
|
||||
BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(BandwidthVoucher::default_parameters)
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg;
|
||||
use crate::coconut::dkg::client::DkgClient;
|
||||
use crate::coconut::dkg::controller::keys::persist_coconut_keypair;
|
||||
use crate::coconut::dkg::controller::DkgController;
|
||||
use crate::coconut::dkg::state::State;
|
||||
use crate::coconut::keys::KeyPair;
|
||||
use crate::coconut::tests::{DummyClient, SharedFakeChain};
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut::VerificationKey;
|
||||
use nym_coconut_dkg_common::types::{DealerDetails, EpochId, InitialReplacementData};
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use nym_dkg::{NodeIndex, Threshold};
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use rand_chacha::{
|
||||
rand_core::{RngCore, SeedableRng},
|
||||
ChaCha20Rng,
|
||||
};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
pub fn test_rng(seed: [u8; 32]) -> ChaCha20Rng {
|
||||
ChaCha20Rng::from_seed(seed)
|
||||
}
|
||||
|
||||
pub fn test_rng_07(seed: [u8; 32]) -> rand_chacha_02::ChaCha20Rng {
|
||||
use rand_chacha_02::rand_core::SeedableRng;
|
||||
rand_chacha_02::ChaCha20Rng::from_seed(seed)
|
||||
}
|
||||
|
||||
pub fn pseudorandom_account(rng: &mut ChaCha20Rng) -> AccountId {
|
||||
let mut dummy_account_key_hash = [0u8; 32];
|
||||
rng.fill_bytes(&mut dummy_account_key_hash);
|
||||
AccountId::new("n", &dummy_account_key_hash).unwrap()
|
||||
}
|
||||
|
||||
pub fn dealer_fixture(mut rng: &mut ChaCha20Rng, id: NodeIndex) -> DealerDetails {
|
||||
// we might possibly need that private key later on
|
||||
let keypair = DkgKeyPair::new(dkg::params(), &mut rng);
|
||||
|
||||
// lol, instantiate rng with an rng due to incompatibility, but even though it looks dodgy AF,
|
||||
// it's 100% deterministic
|
||||
let mut secondary_seed = [0u8; 32];
|
||||
rng.fill_bytes(&mut secondary_seed);
|
||||
|
||||
let addr = pseudorandom_account(rng);
|
||||
let identity_keypair = identity::KeyPair::new(&mut test_rng_07(secondary_seed));
|
||||
let bte_public_key_with_proof = bs58::encode(&keypair.public_key().to_bytes()).into_string();
|
||||
|
||||
let port = 8080 + id;
|
||||
DealerDetails {
|
||||
address: Addr::unchecked(addr.to_string()),
|
||||
bte_public_key_with_proof,
|
||||
ed25519_identity: identity_keypair.public_key().to_base58_string(),
|
||||
announce_address: format!("http://localhost:{port}"),
|
||||
assigned_index: id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dealers_fixtures(rng: &mut ChaCha20Rng, n: usize) -> Vec<DealerDetails> {
|
||||
let mut dealers = Vec::new();
|
||||
for i in 1..=n {
|
||||
dealers.push(dealer_fixture(rng, i as NodeIndex))
|
||||
}
|
||||
dealers
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TestingDkgControllerBuilder {
|
||||
rng: Option<ChaCha20Rng>,
|
||||
rng_seed: Option<[u8; 32]>,
|
||||
address: Option<AccountId>,
|
||||
keypair: Option<KeyPair>,
|
||||
|
||||
chain_state: Option<SharedFakeChain>,
|
||||
|
||||
epoch_id: Option<EpochId>,
|
||||
threshold: Option<Threshold>,
|
||||
self_dealer: Option<DealerDetails>,
|
||||
dealers: Vec<DealerDetails>,
|
||||
initial_dealers: Option<InitialReplacementData>,
|
||||
}
|
||||
|
||||
impl TestingDkgControllerBuilder {
|
||||
pub fn with_magic_seed_val(mut self, val: u8) -> Self {
|
||||
self.rng_seed = Some([val; 32]);
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_rng(mut self, rng: ChaCha20Rng) -> Self {
|
||||
self.rng = Some(rng);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_initial_epoch_id(mut self, initial: EpochId) -> Self {
|
||||
self.epoch_id = Some(initial);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_keypair(mut self, keypair: KeyPair) -> Self {
|
||||
self.keypair = Some(keypair);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_shared_chain_state(mut self, fake_chain: SharedFakeChain) -> Self {
|
||||
self.chain_state = Some(fake_chain);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_as_dealer(mut self, dealer_details: DealerDetails) -> Self {
|
||||
self.self_dealer = Some(dealer_details);
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_dealer(mut self, dealer_details: DealerDetails) -> Self {
|
||||
self.dealers.push(dealer_details);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_dealers(mut self, dealers_details: Vec<DealerDetails>) -> Self {
|
||||
self.dealers = dealers_details;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_initial_dealers(mut self, initial_dealers: InitialReplacementData) -> Self {
|
||||
self.initial_dealers = Some(initial_dealers);
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_address(mut self, address: impl Into<String>) -> Self {
|
||||
let addr = address.into();
|
||||
self.address = Some(addr.parse().unwrap());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_threshold(mut self, threshold: Threshold) -> Self {
|
||||
self.threshold = Some(threshold);
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn build(self) -> TestingDkgController {
|
||||
let mut rng = self.rng.unwrap_or_else(|| {
|
||||
let rng_seed = self.rng_seed.unwrap_or([69u8; 32]);
|
||||
test_rng(rng_seed)
|
||||
});
|
||||
|
||||
let had_dealer_info = self.self_dealer.is_some();
|
||||
// let had_keypair = self.keypair.is_some();
|
||||
|
||||
// is this ideal? no, but it works : P
|
||||
let self_dealer = self.self_dealer.unwrap_or_else(|| {
|
||||
let address = self
|
||||
.address
|
||||
.unwrap_or_else(|| pseudorandom_account(&mut rng));
|
||||
let mut secondary_seed = [0u8; 32];
|
||||
rng.fill_bytes(&mut secondary_seed);
|
||||
|
||||
let identity_keypair = identity::KeyPair::new(&mut test_rng_07(secondary_seed));
|
||||
|
||||
DealerDetails {
|
||||
address: Addr::unchecked(address.as_ref()),
|
||||
bte_public_key_with_proof: "foomp".to_string(),
|
||||
ed25519_identity: identity_keypair.public_key().to_base58_string(),
|
||||
announce_address: "http://localhost:8080".to_string(),
|
||||
assigned_index: 1,
|
||||
}
|
||||
});
|
||||
|
||||
let chain_state = self.chain_state.unwrap_or_default();
|
||||
let dummy_client = DummyClient::new(
|
||||
self_dealer.address.to_string().parse().unwrap(),
|
||||
chain_state.clone(),
|
||||
);
|
||||
|
||||
// insert initial data into the chain state
|
||||
{
|
||||
let mut state_guard = chain_state.lock().unwrap();
|
||||
if let Some(threshold) = self.threshold {
|
||||
state_guard.dkg_contract.threshold = Some(threshold)
|
||||
}
|
||||
for dealer in self.dealers {
|
||||
state_guard
|
||||
.dkg_contract
|
||||
.dealers
|
||||
.insert(dealer.assigned_index, dealer);
|
||||
}
|
||||
if let Some(epoch_id) = self.epoch_id {
|
||||
state_guard.dkg_contract.epoch.epoch_id = epoch_id;
|
||||
}
|
||||
if let Some(initial_dealers) = self.initial_dealers {
|
||||
state_guard.dkg_contract.initial_dealers = Some(initial_dealers)
|
||||
}
|
||||
}
|
||||
|
||||
let dummy_client = DkgClient::new(dummy_client);
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
|
||||
let dkg_state_path = tmp_dir.path().join("persistent_state.json");
|
||||
let coconut_key_path = tmp_dir.path().join("coconut_keypair.pem");
|
||||
|
||||
// if we had a keypair, make sure to put it on disk otherwise, if we're testing dealing exchange,
|
||||
// we'll fail to archive it
|
||||
let keypair = if let Some(keypair) = self.keypair {
|
||||
if let Some(keys) = keypair.read_keys().await.as_ref() {
|
||||
persist_coconut_keypair(keys, &coconut_key_path).unwrap();
|
||||
}
|
||||
keypair
|
||||
} else {
|
||||
KeyPair::new()
|
||||
};
|
||||
|
||||
let mut state = State::new(
|
||||
dkg_state_path,
|
||||
Default::default(),
|
||||
self_dealer.announce_address.parse().unwrap(),
|
||||
// TODO: we might need to fix up the key here
|
||||
DkgKeyPair::new(&nym_dkg::bte::setup(), &mut rng),
|
||||
self_dealer.ed25519_identity.parse().unwrap(),
|
||||
keypair,
|
||||
);
|
||||
|
||||
let epoch = chain_state.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
if had_dealer_info {
|
||||
// if we had dealer info it means we must have gone through key registration
|
||||
state.maybe_init_dkg_state(epoch);
|
||||
state.registration_state_mut(epoch).unwrap().assigned_index =
|
||||
Some(self_dealer.assigned_index);
|
||||
}
|
||||
|
||||
// if had_keypair {
|
||||
// // if we had keypair, it means we must have gone through dealing exchange
|
||||
// state.dealing_exchange_state(epoch).unwrap();
|
||||
// }
|
||||
|
||||
TestingDkgController {
|
||||
controller: DkgController::test_mock(rng, dummy_client, state, coconut_key_path),
|
||||
chain_state,
|
||||
_tmp_dir: tmp_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dkg_controller_fixture() -> TestingDkgController {
|
||||
TestingDkgControllerBuilder::default().build().await
|
||||
}
|
||||
|
||||
pub(crate) struct TestingDkgController {
|
||||
pub(crate) controller: DkgController<ChaCha20Rng>,
|
||||
|
||||
pub(crate) chain_state: SharedFakeChain,
|
||||
|
||||
_tmp_dir: TempDir,
|
||||
}
|
||||
|
||||
impl TestingDkgController {
|
||||
pub async fn address(&self) -> AccountId {
|
||||
self.dkg_client.get_address().await
|
||||
}
|
||||
|
||||
pub async fn cw_address(&self) -> Addr {
|
||||
Addr::unchecked(self.address().await.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) async fn unchecked_coconut_vk(&self) -> VerificationKey {
|
||||
self.state
|
||||
.unchecked_coconut_keypair()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.keys
|
||||
.verification_key()
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TestingDkgController {
|
||||
type Target = DkgController<ChaCha20Rng>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.controller
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for TestingDkgController {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.controller
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user