Compare commits

...

58 Commits

Author SHA1 Message Date
Jędrzej Stuczyński 7f9ac1286b fixed nym-api tests 2024-02-02 13:56:21 +00:00
Jędrzej Stuczyński 961775417f handle chunking on nym-api side 2024-02-02 11:53:53 +00:00
Jędrzej Stuczyński 044d94ad02 updated dkg client traits 2024-02-01 15:19:39 +00:00
Jędrzej Stuczyński 35d7525d1e contract changes 2024-02-01 12:14:43 +00:00
Jędrzej Stuczyński b393405db8 dealing metadata storage logic 2024-01-31 15:46:32 +00:00
Jędrzej Stuczyński 6974f3d785 resharing test + bugfixes 2024-01-30 18:33:58 +00:00
Jędrzej Stuczyński 85d96deded fixed bug in DKG to allow for different sets of dealers and receivers 2024-01-30 15:47:31 +00:00
Jędrzej Stuczyński 4fe7ce0f12 restored key derivation tests 2024-01-30 15:40:20 +00:00
Jędrzej Stuczyński 5886361dc5 restored dealings tests 2024-01-29 11:58:56 +00:00
Jędrzej Stuczyński 40c26b1326 updated contract schema 2024-01-26 10:59:40 +00:00
Jędrzej Stuczyński 46a482cfcb clippy fixes 2024-01-26 10:38:48 +00:00
Jędrzej Stuczyński 761b6c2cac test code compiles
but doesnt fully work yet
2024-01-26 09:52:19 +00:00
Jędrzej Stuczyński 0afdd7bc82 cleanup 2024-01-25 16:11:07 +00:00
Jędrzej Stuczyński 7b9fe3dc09 more completed key derivation 2024-01-25 16:10:35 +00:00
Jędrzej Stuczyński d734174f6e more completed key validation 2024-01-24 12:20:27 +00:00
Jędrzej Stuczyński 6187d94b68 more completed key finalization 2024-01-24 10:34:42 +00:00
Jędrzej Stuczyński a7dca2f07c happy path for key finalization 2024-01-23 17:45:56 +00:00
Jędrzej Stuczyński 2ed4449e0b happy path for key validation 2024-01-23 16:22:41 +00:00
Jędrzej Stuczyński 605176551b actually working happy path with a unit test 2024-01-22 16:55:04 +00:00
Jędrzej Stuczyński ac3830e677 happy path for key derivation 2024-01-22 14:07:26 +00:00
Jędrzej Stuczyński c00e4655f4 improved test fixture + dealing exchange test 2024-01-19 17:21:26 +00:00
Jędrzej Stuczyński 8fa84a28e6 [wip] dealing exchange 2024-01-19 13:42:40 +00:00
Jędrzej Stuczyński 94d3bf087a storing epoch id alongside coconut key 2024-01-17 14:15:25 +00:00
Jędrzej Stuczyński 7a9b989db9 [wip]: improving error recovery during key submission phase 2024-01-17 14:15:25 +00:00
Jędrzej Stuczyński 9f1b89616a more explicit errors in the controller outer loop 2024-01-17 14:15:25 +00:00
Jędrzej Stuczyński 89315f0c2a cleaned up key loading 2024-01-17 14:15:25 +00:00
Jędrzej Stuczyński 858fafb1a5 removed the dkg client retries 2024-01-17 14:15:25 +00:00
Jędrzej Stuczyński 169f8f2c1c cargo fmt 2024-01-17 14:12:41 +00:00
Jędrzej Stuczyński 8782fd7bb8 making nym-api aware of the changes 2024-01-17 14:12:39 +00:00
Jędrzej Stuczyński 146c3bd358 client support 2024-01-17 14:11:02 +00:00
Jędrzej Stuczyński e68ebdc2b8 schema 2024-01-17 14:11:02 +00:00
Jędrzej Stuczyński 397b03267a making dkg kick off when a start message is sent 2024-01-17 14:11:02 +00:00
Jędrzej Stuczyński 2a021b46ac fixed the return type of the query 2024-01-17 14:10:34 +00:00
Jędrzej Stuczyński c43cb657c6 added a query msg for the data 2024-01-17 14:10:34 +00:00
Jędrzej Stuczyński 6f66b377e2 added cw2 interface to dkg contract 2024-01-17 14:10:34 +00:00
Jędrzej Stuczyński 5ea67a9376 missing test fix 2024-01-17 14:10:08 +00:00
Jędrzej Stuczyński f566dffc5b fixed tests 2024-01-17 14:10:08 +00:00
Jędrzej Stuczyński 05f8beedad api support: submit ed25519 public key alongside the bte public key 2024-01-17 14:10:07 +00:00
Jędrzej Stuczyński 2fff051e28 submit ed25519 public key alongside the bte public key 2024-01-17 14:10:07 +00:00
Jędrzej Stuczyński 44bd70c546 reusing already generated dealings 2024-01-17 14:07:03 +00:00
Jędrzej Stuczyński 702354d127 client support 2024-01-17 14:07:02 +00:00
Jędrzej Stuczyński 56b1010d16 schema 2024-01-17 14:05:53 +00:00
Jędrzej Stuczyński 0632517f5d contract query for dealing status 2024-01-17 14:05:53 +00:00
Jędrzej Stuczyński bfcc5e9b41 fixed dealings query arguments 2024-01-17 13:59:06 +00:00
Jędrzej Stuczyński 337aacd442 more clippy 2024-01-17 13:59:06 +00:00
Jędrzej Stuczyński da5b7302b5 updated dkg schema 2024-01-17 13:59:06 +00:00
Jędrzej Stuczyński 7a53e86b40 clippy 2024-01-17 13:59:06 +00:00
Jędrzej Stuczyński 654dd07d19 removed old debug code 2024-01-17 13:59:06 +00:00
Jędrzej Stuczyński ef0765face ephemera contract fix 2024-01-17 13:59:05 +00:00
Jędrzej Stuczyński 3685b4681c fixes 2024-01-17 13:59:05 +00:00
Jędrzej Stuczyński 47e2af2caa reintroducing bug in deterministic_filter_dealers to make tests pass
yes, it's as bad as it sounds
2024-01-17 13:59:03 +00:00
Jędrzej Stuczyński 5be555d79f ability to query for dkg contract state 2024-01-17 13:58:26 +00:00
Jędrzej Stuczyński 8cc2b3167e client support 2024-01-17 13:56:33 +00:00
Jędrzej Stuczyński 4d95955961 renaming 2024-01-17 13:50:23 +00:00
Jędrzej Stuczyński e36ae4091f removed todos from commented tests 2024-01-17 13:50:23 +00:00
Jędrzej Stuczyński b566147f2f storage and query tests 2024-01-17 13:50:22 +00:00
Jędrzej Stuczyński 12242bb3c6 updated dealings queries 2024-01-17 13:50:22 +00:00
Jędrzej Stuczyński 0a0b0e80f4 storing dealings in new map 2024-01-17 13:50:21 +00:00
109 changed files with 9655 additions and 3965 deletions
Generated
+5
View File
@@ -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(),
};
}
}
@@ -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::{
+1 -1
View File
@@ -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() {
+19 -9
View File
@@ -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;
}
}
+1 -2
View File
@@ -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],
+1 -2
View File
@@ -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,
+1 -2
View File
@@ -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>,
+1 -2
View File
@@ -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,
+1 -2
View File
@@ -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
View File
@@ -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, &params, dealer_index, threshold, &receivers, None).0
(
dealer_index,
Dealing::create(&mut rng, &params, 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, &params, dealer_index, threshold, &receivers, None).0
(
dealer_index,
Dealing::create(&mut rng, &params, 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(&params, &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, &params, 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() {
+1 -1
View File
@@ -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,
+1
View File
@@ -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::*;
+51 -36
View File
@@ -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, &params, dealer_index, threshold, &receivers, None).0
(
dealer_index,
Dealing::create(&mut rng, &params, dealer_index, threshold, &receivers, None).0,
)
})
.collect::<Vec<_>>();
for dealing in dealings.iter() {
.collect::<BTreeMap<_, _>>();
for dealing in dealings.values() {
dealing
.verify(&params, 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, &params, dealer_index, threshold, &receivers, None).0
(
dealer_index,
Dealing::create(&mut rng, &params, 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,
&params,
(
dealer_index,
threshold,
&receivers,
Some(*prior_secret),
Dealing::create(
&mut rng,
&params,
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(&params, 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, &params, dealer_index, threshold, &receivers, None).0
(
dealer_index,
Dealing::create(&mut rng, &params, 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,
&params,
(
dealer_index,
threshold,
&receivers,
Some(*prior_secret),
Dealing::create(
&mut rng,
&params,
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();
+3 -1
View File
@@ -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())
}
+6 -4
View File
@@ -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(&params);
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(&params, &dkg_values, vk));
}
+6 -3
View File
@@ -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"
+1
View File
@@ -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"
+2 -1
View File
@@ -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]
+454 -42
View File
@@ -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
+38 -3
View File
@@ -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"
},
+130 -4
View File
@@ -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
}
}
}
+131 -22
View File
@@ -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(),
}
);
}
+342 -184
View File
@@ -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());
// }
// }
// }
// }
+415 -27
View File
@@ -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);
}
}
+308 -104
View File
@@ -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());
+90 -2
View File
@@ -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,
},
}
+3 -17
View File
@@ -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,
},
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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 }
+1 -1
View File
@@ -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"
+4
View File
@@ -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"
+5 -2
View File
@@ -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");
+77 -8
View File
@@ -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,
+11 -5
View File
@@ -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(())
+92 -45
View File
@@ -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(
-13
View File
@@ -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,
}
-221
View File
@@ -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,
},
}
+100
View File
@@ -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(())
}
+340
View File
@@ -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(),
}
}
}
File diff suppressed because it is too large Load Diff
+63
View File
@@ -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
+131
View File
@@ -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(())
}
}
+403
View File
@@ -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(())
}
}
+95 -3
View File
@@ -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(())
}
}
+109 -89
View File
@@ -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(())
}
}
-402
View File
@@ -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
}
}
+383
View File
@@ -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
+33 -9
View File
@@ -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 -2
View File
@@ -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,
-31
View File
@@ -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;
}
}
+86
View File
@@ -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);
}
}
+43
View File
@@ -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,
})
}
}
+2 -2
View File
@@ -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 -6
View File
@@ -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 {
+294
View File
@@ -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