Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6974f3d785 | |||
| 85d96deded | |||
| 4fe7ce0f12 | |||
| 5886361dc5 | |||
| 40c26b1326 | |||
| 46a482cfcb | |||
| 761b6c2cac | |||
| 0afdd7bc82 | |||
| 7b9fe3dc09 | |||
| d734174f6e | |||
| 6187d94b68 | |||
| a7dca2f07c | |||
| 2ed4449e0b | |||
| 605176551b | |||
| ac3830e677 | |||
| c00e4655f4 | |||
| 8fa84a28e6 | |||
| 94d3bf087a | |||
| 7a9b989db9 | |||
| 9f1b89616a | |||
| 89315f0c2a | |||
| 858fafb1a5 | |||
| 169f8f2c1c | |||
| 8782fd7bb8 | |||
| 146c3bd358 | |||
| e68ebdc2b8 | |||
| 397b03267a | |||
| 2a021b46ac | |||
| c43cb657c6 | |||
| 6f66b377e2 | |||
| 5ea67a9376 | |||
| f566dffc5b | |||
| 05f8beedad | |||
| 2fff051e28 | |||
| 44bd70c546 | |||
| 702354d127 | |||
| 56b1010d16 | |||
| 0632517f5d | |||
| bfcc5e9b41 | |||
| 337aacd442 | |||
| da5b7302b5 | |||
| 7a53e86b40 | |||
| 654dd07d19 | |||
| ef0765face | |||
| 3685b4681c | |||
| 47e2af2caa | |||
| 5be555d79f | |||
| 8cc2b3167e | |||
| 4d95955961 | |||
| e36ae4091f | |||
| b566147f2f | |||
| 12242bb3c6 | |||
| 0a0b0e80f4 |
Generated
+5
@@ -5983,6 +5983,8 @@ dependencies = [
|
||||
"pin-project",
|
||||
"rand 0.7.3",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.2.2",
|
||||
"rand_chacha 0.3.1",
|
||||
"reqwest",
|
||||
"rocket",
|
||||
"rocket_cors",
|
||||
@@ -5991,6 +5993,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"sha2 0.9.9",
|
||||
"sqlx",
|
||||
"tap",
|
||||
"tempfile",
|
||||
@@ -6309,6 +6312,8 @@ dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"cw4",
|
||||
"nym-contracts-common",
|
||||
"nym-multisig-contract-common",
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::collect_paged;
|
||||
@@ -7,14 +7,21 @@ 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 serde::Deserialize;
|
||||
|
||||
pub use nym_coconut_dkg_common::{
|
||||
dealer::{
|
||||
DealerDetailsResponse, DealingResponse, DealingStatusResponse, PagedDealerResponse,
|
||||
PagedDealingsResponse,
|
||||
},
|
||||
msg::QueryMsg as DkgQueryMsg,
|
||||
types::{
|
||||
DealerDetails, DealingIndex, Epoch, EpochId, EpochState, InitialReplacementData,
|
||||
PartialContractDealing, 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 +29,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,20 +77,61 @@ pub trait DkgQueryClient {
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_dealings_paged(
|
||||
async fn get_dealing_status(
|
||||
&self,
|
||||
idx: u64,
|
||||
start_after: Option<String>,
|
||||
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(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
) -> Result<DealingResponse, NyxdError> {
|
||||
let request = DkgQueryMsg::GetDealing {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
};
|
||||
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_dealer_dealings_paged(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
dealer: &str,
|
||||
start_after: Option<DealingIndex>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PagedDealingsResponse, NyxdError> {
|
||||
let request = DkgQueryMsg::GetDealing {
|
||||
idx,
|
||||
let request = DkgQueryMsg::GetDealings {
|
||||
epoch_id,
|
||||
dealer: dealer.to_string(),
|
||||
limit,
|
||||
start_after,
|
||||
};
|
||||
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
|
||||
}
|
||||
|
||||
async fn get_vk_shares_paged(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
@@ -91,6 +145,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,8 +165,12 @@ 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_dealer_dealings(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
dealer: &str,
|
||||
) -> Result<Vec<PartialContractDealing>, NyxdError> {
|
||||
collect_paged!(self, get_dealer_dealings_paged, dealings, epoch_id, dealer)
|
||||
}
|
||||
|
||||
async fn get_all_verification_key_shares(
|
||||
@@ -151,6 +214,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 +229,29 @@ mod tests {
|
||||
DkgQueryMsg::GetPastDealers { limit, start_after } => {
|
||||
client.get_past_dealers_paged(start_after, limit).ignore()
|
||||
}
|
||||
DkgQueryMsg::GetDealingStatus {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
} => client
|
||||
.get_dealing_status(epoch_id, dealer, dealing_index)
|
||||
.ignore(),
|
||||
DkgQueryMsg::GetDealing {
|
||||
idx,
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
} => client.get_dealing(epoch_id, dealer, dealing_index).ignore(),
|
||||
DkgQueryMsg::GetDealings {
|
||||
epoch_id,
|
||||
dealer,
|
||||
limit,
|
||||
start_after,
|
||||
} => client.get_dealings_paged(idx, start_after, limit).ignore(),
|
||||
} => client
|
||||
.get_dealer_dealings_paged(epoch_id, &dealer, limit, start_after)
|
||||
.ignore(),
|
||||
DkgQueryMsg::GetVerificationKey { epoch_id, owner } => {
|
||||
client.get_vk_share(epoch_id, owner).ignore()
|
||||
}
|
||||
DkgQueryMsg::GetVerificationKeys {
|
||||
epoch_id,
|
||||
limit,
|
||||
@@ -177,6 +259,7 @@ mod tests {
|
||||
} => client
|
||||
.get_vk_shares_paged(epoch_id, start_after, limit)
|
||||
.ignore(),
|
||||
DkgQueryMsg::GetCW2ContractVersion {} => client.get_contract_cw2_version().ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+24
-13
@@ -10,9 +10,9 @@ use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut_dkg_common::msg::ExecuteMsg as DkgExecuteMsg;
|
||||
use nym_coconut_dkg_common::types::EncodedBTEPublicKeyWithProof;
|
||||
use nym_coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, PartialContractDealing};
|
||||
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,
|
||||
};
|
||||
@@ -58,14 +67,11 @@ pub trait DkgSigningClient {
|
||||
|
||||
async fn submit_dealing_bytes(
|
||||
&self,
|
||||
dealing_bytes: ContractSafeBytes,
|
||||
dealing: PartialContractDealing,
|
||||
resharing: bool,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
let req = DkgExecuteMsg::CommitDealing {
|
||||
dealing_bytes,
|
||||
resharing,
|
||||
};
|
||||
let req = DkgExecuteMsg::CommitDealing { dealing, resharing };
|
||||
|
||||
self.execute_dkg_contract(fee, req, "dealing commitment".to_string(), vec![])
|
||||
.await
|
||||
@@ -146,18 +152,23 @@ 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,
|
||||
resharing,
|
||||
} => client
|
||||
.submit_dealing_bytes(dealing_bytes, resharing, None)
|
||||
DkgExecuteMsg::CommitDealing { dealing, resharing } => client
|
||||
.submit_dealing_bytes(dealing, resharing, None)
|
||||
.ignore(),
|
||||
DkgExecuteMsg::CommitVerificationKeyShare { share, resharing } => client
|
||||
.submit_verification_key_share(share, resharing, None)
|
||||
|
||||
@@ -8,26 +8,26 @@ use std::str::FromStr;
|
||||
// TODO: all of those could/should be derived via a macro
|
||||
|
||||
// query clients
|
||||
mod coconut_bandwidth_query_client;
|
||||
mod dkg_query_client;
|
||||
mod ephemera_query_client;
|
||||
mod group_query_client;
|
||||
mod mixnet_query_client;
|
||||
mod multisig_query_client;
|
||||
mod name_service_query_client;
|
||||
mod sp_directory_query_client;
|
||||
mod vesting_query_client;
|
||||
pub mod coconut_bandwidth_query_client;
|
||||
pub mod dkg_query_client;
|
||||
pub mod ephemera_query_client;
|
||||
pub mod group_query_client;
|
||||
pub mod mixnet_query_client;
|
||||
pub mod multisig_query_client;
|
||||
pub mod name_service_query_client;
|
||||
pub mod sp_directory_query_client;
|
||||
pub mod vesting_query_client;
|
||||
|
||||
// signing clients
|
||||
mod coconut_bandwidth_signing_client;
|
||||
mod dkg_signing_client;
|
||||
mod ephemera_signing_client;
|
||||
mod group_signing_client;
|
||||
mod mixnet_signing_client;
|
||||
mod multisig_signing_client;
|
||||
mod name_service_signing_client;
|
||||
mod sp_directory_signing_client;
|
||||
mod vesting_signing_client;
|
||||
pub mod coconut_bandwidth_signing_client;
|
||||
pub mod dkg_signing_client;
|
||||
pub mod ephemera_signing_client;
|
||||
pub mod group_signing_client;
|
||||
pub mod mixnet_signing_client;
|
||||
pub mod multisig_signing_client;
|
||||
pub mod name_service_signing_client;
|
||||
pub mod sp_directory_signing_client;
|
||||
pub mod vesting_signing_client;
|
||||
|
||||
// re-export query traits
|
||||
pub use coconut_bandwidth_query_client::{
|
||||
|
||||
@@ -14,7 +14,7 @@ pub use nym_coconut::{
|
||||
aggregate_signature_shares, aggregate_verification_keys, blind_sign, hash_to_scalar,
|
||||
prepare_blind_sign, prove_bandwidth_credential, Attribute, Base58, BlindSignRequest,
|
||||
BlindedSignature, Bytable, CoconutError, KeyPair, Parameters, PrivateAttribute,
|
||||
PublicAttribute, Signature, SignatureShare, Theta, VerificationKey,
|
||||
PublicAttribute, SecretKey, Signature, SignatureShare, Theta, VerificationKey,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Getters, CopyGetters, Clone, PartialEq, Eq)]
|
||||
|
||||
@@ -6,7 +6,7 @@ use log::{debug, info};
|
||||
use std::str::FromStr;
|
||||
|
||||
use nym_coconut_dkg_common::msg::InstantiateMsg;
|
||||
use nym_coconut_dkg_common::types::TimeConfiguration;
|
||||
use nym_coconut_dkg_common::types::{TimeConfiguration, DEFAULT_DEALINGS};
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -93,6 +93,7 @@ pub async fn generate(args: Args) {
|
||||
multisig_addr: multisig_addr.to_string(),
|
||||
time_configuration: Some(time_configuration),
|
||||
mix_denom,
|
||||
key_size: DEFAULT_DEALINGS as u32,
|
||||
};
|
||||
|
||||
debug!("instantiate_msg: {:?}", instantiate_msg);
|
||||
|
||||
@@ -10,6 +10,8 @@ license.workspace = true
|
||||
cosmwasm-schema = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
cw-utils = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw4 = { workspace = true }
|
||||
|
||||
contracts-common = { path = "../contracts-common", package = "nym-contracts-common" }
|
||||
nym-multisig-contract-common = { path = "../multisig-contract" }
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof, NodeIndex};
|
||||
use crate::types::{
|
||||
ContractDealing, DealingIndex, EncodedBTEPublicKeyWithProof, EpochId, NodeIndex,
|
||||
PartialContractDealing,
|
||||
};
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::Addr;
|
||||
|
||||
@@ -9,6 +12,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,
|
||||
}
|
||||
@@ -66,35 +70,50 @@ impl PagedDealerResponse {
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct ContractDealing {
|
||||
pub dealing: ContractSafeBytes,
|
||||
pub struct DealingResponse {
|
||||
pub epoch_id: EpochId,
|
||||
|
||||
pub dealer: Addr,
|
||||
|
||||
pub dealing_index: DealingIndex,
|
||||
|
||||
pub dealing: Option<ContractDealing>,
|
||||
}
|
||||
|
||||
impl ContractDealing {
|
||||
pub fn new(dealing: ContractSafeBytes, dealer: Addr) -> Self {
|
||||
ContractDealing { dealing, dealer }
|
||||
}
|
||||
#[cw_serde]
|
||||
pub struct DealingStatusResponse {
|
||||
pub epoch_id: EpochId,
|
||||
|
||||
pub dealer: Addr,
|
||||
|
||||
pub dealing_index: DealingIndex,
|
||||
|
||||
pub dealing_submitted: bool,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct PagedDealingsResponse {
|
||||
pub dealings: Vec<ContractDealing>,
|
||||
pub per_page: usize,
|
||||
pub epoch_id: EpochId,
|
||||
|
||||
pub dealer: Addr,
|
||||
|
||||
pub dealings: Vec<PartialContractDealing>,
|
||||
|
||||
/// Field indicating paging information for the following queries if the caller wishes to get further entries.
|
||||
pub start_next_after: Option<Addr>,
|
||||
pub start_next_after: Option<DealingIndex>,
|
||||
}
|
||||
|
||||
impl PagedDealingsResponse {
|
||||
pub fn new(
|
||||
dealings: Vec<ContractDealing>,
|
||||
per_page: usize,
|
||||
start_next_after: Option<Addr>,
|
||||
epoch_id: EpochId,
|
||||
dealer: Addr,
|
||||
dealings: Vec<PartialContractDealing>,
|
||||
start_next_after: Option<DealingIndex>,
|
||||
) -> Self {
|
||||
PagedDealingsResponse {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealings,
|
||||
per_page,
|
||||
start_next_after,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof, EpochId, TimeConfiguration};
|
||||
use crate::types::{
|
||||
DealingIndex, EncodedBTEPublicKeyWithProof, EpochId, PartialContractDealing, TimeConfiguration,
|
||||
};
|
||||
use crate::verification_key::VerificationKeyShare;
|
||||
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, DealingResponse, DealingStatusResponse, PagedDealerResponse,
|
||||
PagedDealingsResponse,
|
||||
},
|
||||
types::{Epoch, InitialReplacementData, State},
|
||||
verification_key::{PagedVKSharesResponse, VkShareResponse},
|
||||
};
|
||||
use contracts_common::IdentityKey;
|
||||
#[cfg(feature = "schema")]
|
||||
use cosmwasm_schema::QueryResponses;
|
||||
|
||||
@@ -21,18 +27,25 @@ 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,
|
||||
dealing: PartialContractDealing,
|
||||
resharing: bool,
|
||||
},
|
||||
|
||||
@@ -55,6 +68,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 +95,42 @@ 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(DealingStatusResponse))]
|
||||
GetDealingStatus {
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(DealingResponse))]
|
||||
GetDealing {
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(PagedDealingsResponse))]
|
||||
GetDealings {
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
limit: Option<u32>,
|
||||
start_after: Option<DealingIndex>,
|
||||
},
|
||||
|
||||
#[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,38 @@ 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;
|
||||
pub type DealingIndex = u32;
|
||||
pub type ContractDealing = ContractSafeBytes;
|
||||
|
||||
// 2 public attributes, 2 private attributes, 1 fixed for coconut credential
|
||||
pub const TOTAL_DEALINGS: usize = 2 + 2 + 1;
|
||||
pub const DEFAULT_DEALINGS: usize = 2 + 2 + 1;
|
||||
|
||||
#[cw_serde]
|
||||
pub struct PartialContractDealing {
|
||||
pub index: DealingIndex,
|
||||
pub data: ContractDealing,
|
||||
}
|
||||
|
||||
impl PartialContractDealing {
|
||||
pub fn new(index: DealingIndex, data: ContractDealing) -> Self {
|
||||
PartialContractDealing { index, data }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(DealingIndex, ContractDealing)> for PartialContractDealing {
|
||||
fn from(value: (DealingIndex, ContractDealing)) -> Self {
|
||||
PartialContractDealing {
|
||||
index: value.0,
|
||||
data: value.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct InitialReplacementData {
|
||||
@@ -73,13 +97,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 +124,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 +175,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 +195,7 @@ impl Epoch {
|
||||
#[cw_serde]
|
||||
#[derive(Copy)]
|
||||
pub enum EpochState {
|
||||
WaitingInitialisation,
|
||||
PublicKeySubmission { resharing: bool },
|
||||
DealingExchange { resharing: bool },
|
||||
VerificationKeySubmission { resharing: bool },
|
||||
@@ -166,13 +206,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 +235,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>,
|
||||
@@ -57,7 +64,14 @@ pub fn to_cosmos_msg(
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
// 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<Addr> {
|
||||
if msgs.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr: _,
|
||||
msg,
|
||||
|
||||
@@ -23,6 +23,12 @@ impl Deref for ContractSafeBytes {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for ContractSafeBytes {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
ContractSafeBytes(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContractSafeBytes {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
if !self.0.is_empty() {
|
||||
|
||||
@@ -5,7 +5,9 @@ use nym_bandwidth_controller::acquire::state::State;
|
||||
use nym_client_core::config::disk_persistence::CommonClientPaths;
|
||||
use nym_config::DEFAULT_DATA_DIR;
|
||||
use nym_credential_storage::persistent_storage::PersistentStorage;
|
||||
use nym_validator_client::nyxd::contract_traits::{CoconutBandwidthSigningClient, DkgQueryClient};
|
||||
use nym_validator_client::nyxd::contract_traits::{
|
||||
dkg_query_client::EpochState, CoconutBandwidthSigningClient, DkgQueryClient,
|
||||
};
|
||||
use nym_validator_client::nyxd::Coin;
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
@@ -87,21 +89,29 @@ where
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("the system clock is set to 01/01/1970 (or earlier)")
|
||||
.as_secs();
|
||||
|
||||
if epoch.state.is_final() {
|
||||
if current_timestamp_secs + SAFETY_BUFFER_SECS >= epoch.finish_timestamp.seconds() {
|
||||
info!("In the next {} minute(s), a transition will take place in the coconut system. Deposits should be halted in this time for safety reasons.", SAFETY_BUFFER_SECS / 60);
|
||||
exit(0);
|
||||
if let Some(finish_timestamp) = epoch.finish_timestamp {
|
||||
if current_timestamp_secs + SAFETY_BUFFER_SECS >= finish_timestamp.seconds() {
|
||||
info!("In the next {} minute(s), a transition will take place in the coconut system. Deposits should be halted in this time for safety reasons.", SAFETY_BUFFER_SECS / 60);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
} else {
|
||||
} else if let Some(final_timestamp) = epoch.final_timestamp_secs() {
|
||||
// Use 1 additional second to not start the next iteration immediately and spam get_current_epoch queries
|
||||
let secs_until_final = epoch
|
||||
.final_timestamp_secs()
|
||||
.saturating_sub(current_timestamp_secs)
|
||||
+ 1;
|
||||
let secs_until_final = final_timestamp.saturating_sub(current_timestamp_secs) + 1;
|
||||
info!("Approximately {} seconds until coconut is available. Sleeping until then. You can safely kill the process at any moment.", secs_until_final);
|
||||
tokio::time::sleep(Duration::from_secs(secs_until_final)).await;
|
||||
} else if matches!(epoch.state, EpochState::WaitingInitialisation) {
|
||||
info!("dkg hasn't been initialised yet and it is not known when it will be. Going to check again later");
|
||||
tokio::time::sleep(Duration::from_secs(60 * 5)).await;
|
||||
} else {
|
||||
// this should never be the case since the only case where final timestamp is unknown is when it's waiting for initialisation,
|
||||
// but let's guard ourselves against future changes
|
||||
info!("it is unknown when coconut will be come available. Going to check again later");
|
||||
tokio::time::sleep(Duration::from_secs(60 * 5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ use std::collections::HashMap;
|
||||
use std::ops::Neg;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(Clone, PartialEq, Eq))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Ciphertexts {
|
||||
pub rr: [G1Projective; NUM_CHUNKS],
|
||||
pub ss: [G1Projective; NUM_CHUNKS],
|
||||
|
||||
@@ -53,8 +53,7 @@ impl PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PublicKeyWithProof {
|
||||
pub(crate) key: PublicKey,
|
||||
pub(crate) proof: ProofOfDiscreteLog,
|
||||
|
||||
@@ -67,8 +67,7 @@ impl<'a> Instance<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(Clone, PartialEq, Eq))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ProofOfChunking {
|
||||
y0: G1Projective,
|
||||
bb: Vec<G1Projective>,
|
||||
|
||||
@@ -13,8 +13,7 @@ use zeroize::Zeroize;
|
||||
const DISCRETE_LOG_DOMAIN: &[u8] =
|
||||
b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381_XMD:SHA-256_SSWU_RO_PROOF_DISCRETE_LOG";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ProofOfDiscreteLog {
|
||||
pub(crate) rand_commitment: G1Projective,
|
||||
pub(crate) response: Scalar,
|
||||
|
||||
@@ -76,8 +76,7 @@ impl<'a> Instance<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(Clone, PartialEq, Eq))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ProofOfSecretSharing {
|
||||
ff: G1Projective,
|
||||
aa: G2Projective,
|
||||
|
||||
+100
-20
@@ -82,8 +82,7 @@ impl RecoveredVerificationKeys {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Dealing {
|
||||
pub public_coefficients: PublicCoefficients,
|
||||
pub ciphertexts: Ciphertexts,
|
||||
@@ -321,9 +320,17 @@ impl<'a> TryFrom<&'a nym_contracts_common::dealings::ContractSafeBytes> for Deal
|
||||
}
|
||||
}
|
||||
|
||||
// this assumes all dealings have been verified
|
||||
/// Attempt to run the `VkCombine` algorithm to obtain the public master verification key, `VK`
|
||||
/// alongside shares of the verification key, `shvk_{1}`, `shvk_{2}`, ... `svhk_{n}`, where n is the number of receivers.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `dealings`: map of dealer indices to dealings they generated
|
||||
/// * `threshold`: explicit threshold value of the associated dealings
|
||||
/// * `receivers`:map of receiver indices to their public keys
|
||||
// note: this function assumes all dealings have already been verified
|
||||
pub fn try_recover_verification_keys(
|
||||
dealings: &[Dealing],
|
||||
dealings: &BTreeMap<NodeIndex, Dealing>,
|
||||
threshold: Threshold,
|
||||
receivers: &BTreeMap<NodeIndex, PublicKey>,
|
||||
) -> Result<RecoveredVerificationKeys, DkgError> {
|
||||
@@ -331,24 +338,31 @@ pub fn try_recover_verification_keys(
|
||||
return Err(DkgError::NoDealingsAvailable);
|
||||
}
|
||||
|
||||
let threshold_usize = threshold as usize;
|
||||
let threshold = threshold as usize;
|
||||
|
||||
if dealings.len() < threshold {
|
||||
return Err(DkgError::NotEnoughDealingsAvailable {
|
||||
available: dealings.len(),
|
||||
required: threshold,
|
||||
});
|
||||
}
|
||||
|
||||
if !dealings
|
||||
.iter()
|
||||
.all(|dealing| dealing.public_coefficients.size() == threshold_usize)
|
||||
.values()
|
||||
.all(|dealing| dealing.public_coefficients.size() == threshold)
|
||||
{
|
||||
return Err(DkgError::MismatchedDealings);
|
||||
}
|
||||
|
||||
let indices = receivers.keys().collect::<Vec<_>>();
|
||||
let dealer_indices = dealings.keys().collect::<Vec<_>>();
|
||||
|
||||
// Compute A0, ..., A_{t-1}
|
||||
let mut interpolated_coefficients = Vec::with_capacity(threshold_usize);
|
||||
for k in 0..threshold_usize {
|
||||
let mut samples = Vec::with_capacity(indices.len());
|
||||
for (j, dealing) in dealings.iter().enumerate() {
|
||||
let mut interpolated_coefficients = Vec::with_capacity(threshold);
|
||||
for k in 0..threshold {
|
||||
let mut samples = Vec::with_capacity(dealer_indices.len());
|
||||
for (dealer_index, dealing) in dealings.iter() {
|
||||
samples.push((
|
||||
Scalar::from(*indices[j]),
|
||||
Scalar::from(*dealer_index),
|
||||
*dealing.public_coefficients.nth(k),
|
||||
))
|
||||
}
|
||||
@@ -365,7 +379,7 @@ pub fn try_recover_verification_keys(
|
||||
// shvk_j = A0^{j^0} * A1^{j^1} * ... * A_{t-1}^{j^{t-1}}
|
||||
let verification_key_shares = receivers
|
||||
.keys()
|
||||
.map(|index| interpolated_coefficients.evaluate_at(&Scalar::from(*index)))
|
||||
.map(|receiver_index| interpolated_coefficients.evaluate_at(&Scalar::from(*receiver_index)))
|
||||
.collect();
|
||||
|
||||
Ok(RecoveredVerificationKeys {
|
||||
@@ -457,14 +471,17 @@ mod tests {
|
||||
let dealings = node_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0
|
||||
(
|
||||
dealer_index,
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let mut derived_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.iter()
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
derived_secrets.push(
|
||||
@@ -513,9 +530,12 @@ mod tests {
|
||||
let dealings = node_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0
|
||||
(
|
||||
dealer_index,
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let RecoveredVerificationKeys {
|
||||
recovered_master,
|
||||
@@ -531,6 +551,66 @@ mod tests {
|
||||
.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn verifying_partial_verification_keys_with_different_dealers_and_receivers() {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
let params = setup();
|
||||
|
||||
let dealer_indices = [1, 2, 3, 8];
|
||||
let receiver_indices = [3, 4, 5, 6, 7];
|
||||
let threshold = 3;
|
||||
|
||||
let mut receivers = BTreeMap::new();
|
||||
let mut full_keys = Vec::new();
|
||||
for index in &receiver_indices {
|
||||
let (dk, pk) = keygen(¶ms, &mut rng);
|
||||
receivers.insert(*index, *pk.public_key());
|
||||
full_keys.push((dk, pk))
|
||||
}
|
||||
|
||||
let dealings = dealer_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
(
|
||||
dealer_index,
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0,
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let RecoveredVerificationKeys {
|
||||
recovered_master,
|
||||
recovered_partials,
|
||||
} = try_recover_verification_keys(&dealings, threshold, &receivers).unwrap();
|
||||
|
||||
let g2 = G2Projective::generator();
|
||||
|
||||
let mut derived_secrets = Vec::new();
|
||||
for (i, (dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret = combine_shares(shares, &dealer_indices).unwrap();
|
||||
|
||||
// make sure it matches the associated vk
|
||||
assert_eq!(recovered_partials[i], g2 * recovered_secret);
|
||||
|
||||
derived_secrets.push(recovered_secret)
|
||||
}
|
||||
|
||||
assert!(verify_verification_keys(
|
||||
&recovered_master,
|
||||
&recovered_partials,
|
||||
&receivers,
|
||||
threshold
|
||||
)
|
||||
.is_ok())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // expensive test
|
||||
fn dealing_roundtrip() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[derive(Debug, Error, Clone)]
|
||||
pub enum DkgError {
|
||||
#[error("Provided set of values contained duplicate coordinate")]
|
||||
DuplicateCoordinate,
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod dealing;
|
||||
pub(crate) mod share;
|
||||
pub(crate) mod utils;
|
||||
|
||||
pub use bls12_381::{G2Projective, Scalar};
|
||||
pub use dealing::*;
|
||||
pub use share::*;
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ fn single_sender() {
|
||||
.unwrap();
|
||||
|
||||
// make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check)
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap();
|
||||
}
|
||||
|
||||
@@ -91,10 +91,13 @@ fn full_threshold_secret_sharing() {
|
||||
let dealings = node_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0
|
||||
(
|
||||
dealer_index,
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for dealing in dealings.iter() {
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
for dealing in dealings.values() {
|
||||
dealing
|
||||
.verify(¶ms, threshold, &receivers, None)
|
||||
.unwrap();
|
||||
@@ -109,9 +112,9 @@ fn full_threshold_secret_sharing() {
|
||||
let g2 = G2Projective::generator();
|
||||
|
||||
let mut derived_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.iter()
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
@@ -169,9 +172,12 @@ fn full_threshold_secret_resharing() {
|
||||
let first_dealings = node_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0
|
||||
(
|
||||
dealer_index,
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// recover verification keys
|
||||
let RecoveredVerificationKeys {
|
||||
@@ -180,9 +186,9 @@ fn full_threshold_secret_resharing() {
|
||||
} = try_recover_verification_keys(&first_dealings, threshold, &receivers).unwrap();
|
||||
|
||||
let mut derived_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = first_dealings
|
||||
.iter()
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
@@ -203,19 +209,22 @@ fn full_threshold_secret_resharing() {
|
||||
.iter()
|
||||
.zip(derived_secrets.iter())
|
||||
.map(|(&dealer_index, prior_secret)| {
|
||||
Dealing::create(
|
||||
&mut rng,
|
||||
¶ms,
|
||||
(
|
||||
dealer_index,
|
||||
threshold,
|
||||
&receivers,
|
||||
Some(*prior_secret),
|
||||
Dealing::create(
|
||||
&mut rng,
|
||||
¶ms,
|
||||
dealer_index,
|
||||
threshold,
|
||||
&receivers,
|
||||
Some(*prior_secret),
|
||||
)
|
||||
.0,
|
||||
)
|
||||
.0
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
for (reshared_dealing, prior_vk) in resharing_dealings.iter().zip(recovered_partials.iter()) {
|
||||
for (reshared_dealing, prior_vk) in resharing_dealings.values().zip(recovered_partials.iter()) {
|
||||
reshared_dealing
|
||||
.verify(¶ms, threshold, &receivers, Some(*prior_vk))
|
||||
.unwrap();
|
||||
@@ -228,9 +237,9 @@ fn full_threshold_secret_resharing() {
|
||||
} = try_recover_verification_keys(&resharing_dealings, threshold, &receivers).unwrap();
|
||||
|
||||
let mut reshared_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = resharing_dealings
|
||||
.iter()
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
@@ -279,9 +288,12 @@ fn full_threshold_secret_resharing_left_party() {
|
||||
let first_dealings = node_indices
|
||||
.iter()
|
||||
.map(|&dealer_index| {
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0
|
||||
(
|
||||
dealer_index,
|
||||
Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// recover verification keys
|
||||
let RecoveredVerificationKeys {
|
||||
@@ -290,9 +302,9 @@ fn full_threshold_secret_resharing_left_party() {
|
||||
} = try_recover_verification_keys(&first_dealings, threshold, &receivers).unwrap();
|
||||
|
||||
let mut derived_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = first_dealings
|
||||
.iter()
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
@@ -323,20 +335,23 @@ fn full_threshold_secret_resharing_left_party() {
|
||||
.iter()
|
||||
.zip(derived_secrets.iter().take(2))
|
||||
.map(|(&dealer_index, prior_secret)| {
|
||||
Dealing::create(
|
||||
&mut rng,
|
||||
¶ms,
|
||||
(
|
||||
dealer_index,
|
||||
threshold,
|
||||
&receivers,
|
||||
Some(*prior_secret),
|
||||
Dealing::create(
|
||||
&mut rng,
|
||||
¶ms,
|
||||
dealer_index,
|
||||
threshold,
|
||||
&receivers,
|
||||
Some(*prior_secret),
|
||||
)
|
||||
.0,
|
||||
)
|
||||
.0
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
for (reshared_dealing, prior_vk) in resharing_dealings
|
||||
.iter()
|
||||
.values()
|
||||
.zip(recovered_partials.iter().take(2))
|
||||
{
|
||||
reshared_dealing
|
||||
@@ -351,9 +366,9 @@ fn full_threshold_secret_resharing_left_party() {
|
||||
} = try_recover_verification_keys(&resharing_dealings, threshold, &receivers).unwrap();
|
||||
|
||||
let mut reshared_secrets = Vec::new();
|
||||
for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = resharing_dealings
|
||||
.iter()
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
|
||||
@@ -99,7 +99,9 @@ impl SecretKey {
|
||||
Self { x, ys }
|
||||
}
|
||||
|
||||
pub fn into_raw(&self) -> (Scalar, Vec<Scalar>) {
|
||||
/// Extract the Scalar copy of the underlying secrets.
|
||||
/// The caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized
|
||||
pub fn hazmat_to_raw(&self) -> (Scalar, Vec<Scalar>) {
|
||||
(self.x, self.ys.clone())
|
||||
}
|
||||
|
||||
|
||||
@@ -228,10 +228,11 @@ pub fn check_vk_pairing(
|
||||
|
||||
// safety: we made an explicit check for if the length of the slice is 0, thus unwrap here is fine
|
||||
#[allow(clippy::unwrap_used)]
|
||||
if &vk.alpha != *dkg_values.last().as_ref().unwrap() {
|
||||
if &vk.alpha != *dkg_values.first().as_ref().unwrap() {
|
||||
return false;
|
||||
}
|
||||
if dkg_values
|
||||
let dkg_betas = &dkg_values[1..];
|
||||
if dkg_betas
|
||||
.iter()
|
||||
.zip(vk.beta_g2.iter())
|
||||
.any(|(dkg_beta, vk_beta)| dkg_beta != vk_beta)
|
||||
@@ -329,8 +330,9 @@ mod tests {
|
||||
let params = setup(2).unwrap();
|
||||
let keypair = keygen(¶ms);
|
||||
let vk = keypair.verification_key();
|
||||
let mut dkg_values = vk.beta_g2.clone();
|
||||
dkg_values.push(vk.alpha);
|
||||
|
||||
let mut dkg_values = vec![vk.alpha];
|
||||
dkg_values.append(&mut vk.beta_g2.clone());
|
||||
assert!(check_vk_pairing(¶ms, &dkg_values, vk));
|
||||
}
|
||||
|
||||
|
||||
Generated
+6
-3
@@ -1220,12 +1220,13 @@ dependencies = [
|
||||
"cw-controllers",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"cw2",
|
||||
"cw4",
|
||||
"cw4-group",
|
||||
"lazy_static",
|
||||
"nym-coconut-dkg-common",
|
||||
"nym-group-contract-common",
|
||||
"rusty-fork",
|
||||
"semver",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
@@ -1237,6 +1238,8 @@ dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-utils",
|
||||
"cw2",
|
||||
"cw4",
|
||||
"nym-contracts-common",
|
||||
"nym-multisig-contract-common",
|
||||
]
|
||||
@@ -1879,9 +1882,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.17"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
|
||||
checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
|
||||
@@ -48,5 +48,6 @@ cw3 = "=1.1.0"
|
||||
cw3-fixed-multisig = "=1.1.0"
|
||||
cw4 = "=1.1.0"
|
||||
cw20 = "=1.1.0"
|
||||
semver = "1.0.21"
|
||||
|
||||
thiserror = "1.0.48"
|
||||
|
||||
@@ -20,15 +20,16 @@ cosmwasm-std = { workspace = true }
|
||||
cosmwasm-storage = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
cw-controllers = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw4 = { workspace = true }
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
semver = { workspace = true, default-features = false }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
cw-multi-test = { workspace = true }
|
||||
cw4-group = { path = "../multisig/cw4-group" }
|
||||
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract" }
|
||||
lazy_static = "1.4"
|
||||
rusty-fork = "0.3"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"group_addr",
|
||||
"key_size",
|
||||
"mix_denom",
|
||||
"multisig_addr"
|
||||
],
|
||||
@@ -15,6 +16,12 @@
|
||||
"group_addr": {
|
||||
"type": "string"
|
||||
},
|
||||
"key_size": {
|
||||
"description": "Specifies the number of elements in the derived keys",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"mix_denom": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -84,6 +91,19 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ExecuteMsg",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"initiate_dkg"
|
||||
],
|
||||
"properties": {
|
||||
"initiate_dkg": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -95,6 +115,7 @@
|
||||
"required": [
|
||||
"announce_address",
|
||||
"bte_key_with_proof",
|
||||
"identity_key",
|
||||
"resharing"
|
||||
],
|
||||
"properties": {
|
||||
@@ -104,6 +125,9 @@
|
||||
"bte_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"identity_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"resharing": {
|
||||
"type": "boolean"
|
||||
}
|
||||
@@ -122,12 +146,12 @@
|
||||
"commit_dealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealing_bytes",
|
||||
"dealing",
|
||||
"resharing"
|
||||
],
|
||||
"properties": {
|
||||
"dealing_bytes": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
"dealing": {
|
||||
"$ref": "#/definitions/PartialContractDealing"
|
||||
},
|
||||
"resharing": {
|
||||
"type": "boolean"
|
||||
@@ -227,6 +251,24 @@
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"PartialContractDealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data",
|
||||
"index"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
},
|
||||
"index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -234,6 +276,19 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryMsg",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_state"
|
||||
],
|
||||
"properties": {
|
||||
"get_state": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -352,6 +407,39 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_dealing_status"
|
||||
],
|
||||
"properties": {
|
||||
"get_dealing_status": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"type": "string"
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -361,10 +449,47 @@
|
||||
"get_dealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"idx"
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"idx": {
|
||||
"dealer": {
|
||||
"type": "string"
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_dealings"
|
||||
],
|
||||
"properties": {
|
||||
"get_dealings": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"type": "string"
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
@@ -379,9 +504,38 @@
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"string",
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_verification_key"
|
||||
],
|
||||
"properties": {
|
||||
"get_verification_key": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"epoch_id",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -425,6 +579,20 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Gets the stored contract version information that's required by the CW2 spec interface for migrations.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_cw2_contract_version"
|
||||
],
|
||||
"properties": {
|
||||
"get_cw2_contract_version": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -436,6 +604,26 @@
|
||||
},
|
||||
"sudo": null,
|
||||
"responses": {
|
||||
"get_c_w2_contract_version": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ContractVersion",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"contract",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"contract": {
|
||||
"description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"get_current_dealers": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PagedDealerResponse",
|
||||
@@ -480,7 +668,8 @@
|
||||
"address",
|
||||
"announce_address",
|
||||
"assigned_index",
|
||||
"bte_public_key_with_proof"
|
||||
"bte_public_key_with_proof",
|
||||
"ed25519_identity"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
@@ -496,6 +685,9 @@
|
||||
},
|
||||
"bte_public_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"ed25519_identity": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -508,7 +700,6 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"epoch_id",
|
||||
"finish_timestamp",
|
||||
"state",
|
||||
"time_configuration"
|
||||
],
|
||||
@@ -519,7 +710,14 @@
|
||||
"minimum": 0.0
|
||||
},
|
||||
"finish_timestamp": {
|
||||
"$ref": "#/definitions/Timestamp"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Timestamp"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/definitions/EpochState"
|
||||
@@ -535,6 +733,7 @@
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"waiting_initialisation",
|
||||
"in_progress"
|
||||
]
|
||||
},
|
||||
@@ -744,7 +943,8 @@
|
||||
"address",
|
||||
"announce_address",
|
||||
"assigned_index",
|
||||
"bte_public_key_with_proof"
|
||||
"bte_public_key_with_proof",
|
||||
"ed25519_identity"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
@@ -760,6 +960,9 @@
|
||||
},
|
||||
"bte_public_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"ed25519_identity": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -776,34 +979,36 @@
|
||||
},
|
||||
"get_dealing": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PagedDealingsResponse",
|
||||
"title": "DealingResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealings",
|
||||
"per_page"
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ContractDealing"
|
||||
}
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
|
||||
"dealing": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -812,21 +1017,91 @@
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"ContractDealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealing"
|
||||
"ContractSafeBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_dealing_status": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "DealingStatusResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"dealing_submitted",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"dealing_submitted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_dealings": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PagedDealingsResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealings",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"dealings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PartialContractDealing"
|
||||
}
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"dealing": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"ContractSafeBytes": {
|
||||
"type": "array",
|
||||
@@ -835,6 +1110,24 @@
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"PartialContractDealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data",
|
||||
"index"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
},
|
||||
"index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -921,7 +1214,8 @@
|
||||
"address",
|
||||
"announce_address",
|
||||
"assigned_index",
|
||||
"bte_public_key_with_proof"
|
||||
"bte_public_key_with_proof",
|
||||
"ed25519_identity"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
@@ -937,6 +1231,124 @@
|
||||
},
|
||||
"bte_public_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"ed25519_identity": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_state": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "State",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"group_addr",
|
||||
"key_size",
|
||||
"mix_denom",
|
||||
"multisig_addr"
|
||||
],
|
||||
"properties": {
|
||||
"group_addr": {
|
||||
"$ref": "#/definitions/Cw4Contract"
|
||||
},
|
||||
"key_size": {
|
||||
"description": "Specifies the number of elements in the derived keys",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"mix_denom": {
|
||||
"type": "string"
|
||||
},
|
||||
"multisig_addr": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"Cw4Contract": {
|
||||
"description": "Cw4Contract is a wrapper around Addr that provides a lot of helpers for working with cw4 contracts\n\nIf you wish to persist this, convert to Cw4CanonicalContract via .canonical()",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_verification_key": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "VkShareResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"epoch_id",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"share": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ContractVKShare"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"ContractVKShare": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"announce_address",
|
||||
"epoch_id",
|
||||
"node_index",
|
||||
"owner",
|
||||
"share",
|
||||
"verified"
|
||||
],
|
||||
"properties": {
|
||||
"announce_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_index": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"share": {
|
||||
"type": "string"
|
||||
},
|
||||
"verified": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ExecuteMsg",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"initiate_dkg"
|
||||
],
|
||||
"properties": {
|
||||
"initiate_dkg": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -13,6 +26,7 @@
|
||||
"required": [
|
||||
"announce_address",
|
||||
"bte_key_with_proof",
|
||||
"identity_key",
|
||||
"resharing"
|
||||
],
|
||||
"properties": {
|
||||
@@ -22,6 +36,9 @@
|
||||
"bte_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"identity_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"resharing": {
|
||||
"type": "boolean"
|
||||
}
|
||||
@@ -40,12 +57,12 @@
|
||||
"commit_dealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealing_bytes",
|
||||
"dealing",
|
||||
"resharing"
|
||||
],
|
||||
"properties": {
|
||||
"dealing_bytes": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
"dealing": {
|
||||
"$ref": "#/definitions/PartialContractDealing"
|
||||
},
|
||||
"resharing": {
|
||||
"type": "boolean"
|
||||
@@ -145,6 +162,24 @@
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"PartialContractDealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data",
|
||||
"index"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
},
|
||||
"index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"group_addr",
|
||||
"key_size",
|
||||
"mix_denom",
|
||||
"multisig_addr"
|
||||
],
|
||||
@@ -11,6 +12,12 @@
|
||||
"group_addr": {
|
||||
"type": "string"
|
||||
},
|
||||
"key_size": {
|
||||
"description": "Specifies the number of elements in the derived keys",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"mix_denom": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,19 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "QueryMsg",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_state"
|
||||
],
|
||||
"properties": {
|
||||
"get_state": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -120,6 +133,39 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_dealing_status"
|
||||
],
|
||||
"properties": {
|
||||
"get_dealing_status": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"type": "string"
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
@@ -129,10 +175,47 @@
|
||||
"get_dealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"idx"
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"idx": {
|
||||
"dealer": {
|
||||
"type": "string"
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_dealings"
|
||||
],
|
||||
"properties": {
|
||||
"get_dealings": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"type": "string"
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
@@ -147,9 +230,38 @@
|
||||
},
|
||||
"start_after": {
|
||||
"type": [
|
||||
"string",
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_verification_key"
|
||||
],
|
||||
"properties": {
|
||||
"get_verification_key": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"epoch_id",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -193,6 +305,20 @@
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"description": "Gets the stored contract version information that's required by the CW2 spec interface for migrations.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"get_cw2_contract_version"
|
||||
],
|
||||
"properties": {
|
||||
"get_cw2_contract_version": {
|
||||
"type": "object",
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ContractVersion",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"contract",
|
||||
"version"
|
||||
],
|
||||
"properties": {
|
||||
"contract": {
|
||||
"description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -42,7 +42,8 @@
|
||||
"address",
|
||||
"announce_address",
|
||||
"assigned_index",
|
||||
"bte_public_key_with_proof"
|
||||
"bte_public_key_with_proof",
|
||||
"ed25519_identity"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
@@ -58,6 +59,9 @@
|
||||
},
|
||||
"bte_public_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"ed25519_identity": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"type": "object",
|
||||
"required": [
|
||||
"epoch_id",
|
||||
"finish_timestamp",
|
||||
"state",
|
||||
"time_configuration"
|
||||
],
|
||||
@@ -15,7 +14,14 @@
|
||||
"minimum": 0.0
|
||||
},
|
||||
"finish_timestamp": {
|
||||
"$ref": "#/definitions/Timestamp"
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Timestamp"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"$ref": "#/definitions/EpochState"
|
||||
@@ -31,6 +37,7 @@
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"waiting_initialisation",
|
||||
"in_progress"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"address",
|
||||
"announce_address",
|
||||
"assigned_index",
|
||||
"bte_public_key_with_proof"
|
||||
"bte_public_key_with_proof",
|
||||
"ed25519_identity"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
@@ -48,6 +49,9 @@
|
||||
},
|
||||
"bte_public_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"ed25519_identity": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PagedDealingsResponse",
|
||||
"title": "DealingResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealings",
|
||||
"per_page"
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/ContractDealing"
|
||||
}
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"per_page": {
|
||||
"type": "integer",
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
|
||||
"dealing": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -36,22 +38,6 @@
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"ContractDealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealing"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"dealing": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ContractSafeBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "DealingStatusResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealing_index",
|
||||
"dealing_submitted",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"dealing_index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"dealing_submitted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "PagedDealingsResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dealer",
|
||||
"dealings",
|
||||
"epoch_id"
|
||||
],
|
||||
"properties": {
|
||||
"dealer": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"dealings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/PartialContractDealing"
|
||||
}
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"start_next_after": {
|
||||
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"ContractSafeBytes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"PartialContractDealing": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data",
|
||||
"index"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/definitions/ContractSafeBytes"
|
||||
},
|
||||
"index": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,8 @@
|
||||
"address",
|
||||
"announce_address",
|
||||
"assigned_index",
|
||||
"bte_public_key_with_proof"
|
||||
"bte_public_key_with_proof",
|
||||
"ed25519_identity"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
@@ -58,6 +59,9 @@
|
||||
},
|
||||
"bte_public_key_with_proof": {
|
||||
"type": "string"
|
||||
},
|
||||
"ed25519_identity": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "State",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"group_addr",
|
||||
"key_size",
|
||||
"mix_denom",
|
||||
"multisig_addr"
|
||||
],
|
||||
"properties": {
|
||||
"group_addr": {
|
||||
"$ref": "#/definitions/Cw4Contract"
|
||||
},
|
||||
"key_size": {
|
||||
"description": "Specifies the number of elements in the derived keys",
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"mix_denom": {
|
||||
"type": "string"
|
||||
},
|
||||
"multisig_addr": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"Cw4Contract": {
|
||||
"description": "Cw4Contract is a wrapper around Addr that provides a lot of helpers for working with cw4 contracts\n\nIf you wish to persist this, convert to Cw4CanonicalContract via .canonical()",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Addr"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "VkShareResponse",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"epoch_id",
|
||||
"owner"
|
||||
],
|
||||
"properties": {
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"share": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ContractVKShare"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"Addr": {
|
||||
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
|
||||
"type": "string"
|
||||
},
|
||||
"ContractVKShare": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"announce_address",
|
||||
"epoch_id",
|
||||
"node_index",
|
||||
"owner",
|
||||
"share",
|
||||
"verified"
|
||||
],
|
||||
"properties": {
|
||||
"announce_address": {
|
||||
"type": "string"
|
||||
},
|
||||
"epoch_id": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"node_index": {
|
||||
"type": "integer",
|
||||
"format": "uint64",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/definitions/Addr"
|
||||
},
|
||||
"share": {
|
||||
"type": "string"
|
||||
},
|
||||
"verified": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
// 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::queries::{query_dealing, query_dealing_status, query_dealings_paged};
|
||||
use crate::dealings::transactions::try_commit_dealings;
|
||||
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 +25,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 +40,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 +58,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 +86,23 @@ 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::CommitDealing { dealing, resharing } => {
|
||||
try_commit_dealings(deps, info, dealing, resharing)
|
||||
}
|
||||
ExecuteMsg::CommitVerificationKeyShare { share, resharing } => {
|
||||
try_commit_verification_key_share(deps, env, info, share, resharing)
|
||||
}
|
||||
@@ -97,6 +117,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 +132,70 @@ 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::GetDealingStatus {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
} => to_binary(&query_dealing_status(
|
||||
deps,
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
)?)?,
|
||||
QueryMsg::GetDealing {
|
||||
idx,
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
} => to_binary(&query_dealing(deps, epoch_id, dealer, dealing_index)?)?,
|
||||
QueryMsg::GetDealings {
|
||||
epoch_id,
|
||||
dealer,
|
||||
limit,
|
||||
start_after,
|
||||
} => to_binary(&query_dealings_paged(deps, idx, start_after, limit)?)?,
|
||||
} => to_binary(&query_dealings_paged(
|
||||
deps,
|
||||
epoch_id,
|
||||
dealer,
|
||||
start_after,
|
||||
limit,
|
||||
)?)?,
|
||||
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,8 +207,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::types::NodeIndex;
|
||||
use nym_coconut_dkg_common::msg::ExecuteMsg::{InitiateDkg, RegisterDealer};
|
||||
use nym_coconut_dkg_common::types::{NodeIndex, DEFAULT_DEALINGS};
|
||||
use nym_group_contract_common::msg::InstantiateMsg as GroupInstantiateMsg;
|
||||
|
||||
fn instantiate_with_group(app: &mut App, members: &[Addr]) -> Addr {
|
||||
@@ -178,6 +245,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 +281,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 +304,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 +319,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 +334,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 +351,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(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,44 +2,74 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::dealings::storage;
|
||||
use crate::dealings::storage::DEALINGS_BYTES;
|
||||
use cosmwasm_std::{Deps, Order, StdResult};
|
||||
use crate::dealings::storage::StoredDealing;
|
||||
use cosmwasm_std::{Deps, StdResult};
|
||||
use cw_storage_plus::Bound;
|
||||
use nym_coconut_dkg_common::dealer::{ContractDealing, PagedDealingsResponse};
|
||||
use nym_coconut_dkg_common::types::TOTAL_DEALINGS;
|
||||
use nym_coconut_dkg_common::dealer::{
|
||||
DealingResponse, DealingStatusResponse, PagedDealingsResponse,
|
||||
};
|
||||
use nym_coconut_dkg_common::types::{DealingIndex, EpochId};
|
||||
|
||||
// this does almost the same as query_dealing but doesn't return the actual dealing to make it easier on the validator
|
||||
// so it wouldn't need to deal with the deserialization
|
||||
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 dealing_submitted = StoredDealing::exists(deps.storage, epoch_id, &dealer, dealing_index);
|
||||
|
||||
Ok(DealingStatusResponse {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
dealing_submitted,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_dealing(
|
||||
deps: Deps<'_>,
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
dealing_index: DealingIndex,
|
||||
) -> StdResult<DealingResponse> {
|
||||
let dealer = deps.api.addr_validate(&dealer)?;
|
||||
let dealing = StoredDealing::read(deps.storage, epoch_id, &dealer, dealing_index);
|
||||
|
||||
Ok(DealingResponse {
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealing_index,
|
||||
dealing,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn query_dealings_paged(
|
||||
deps: Deps<'_>,
|
||||
idx: u64,
|
||||
start_after: Option<String>,
|
||||
epoch_id: EpochId,
|
||||
dealer: String,
|
||||
start_after: Option<DealingIndex>,
|
||||
limit: Option<u32>,
|
||||
) -> StdResult<PagedDealingsResponse> {
|
||||
let limit = limit
|
||||
.unwrap_or(storage::DEALINGS_PAGE_DEFAULT_LIMIT)
|
||||
.min(storage::DEALINGS_PAGE_MAX_LIMIT) as usize;
|
||||
.min(storage::DEALINGS_PAGE_MAX_LIMIT);
|
||||
|
||||
let idx = idx as usize;
|
||||
if idx >= TOTAL_DEALINGS {
|
||||
return Ok(PagedDealingsResponse::new(vec![], limit, None));
|
||||
}
|
||||
let dealer = deps.api.addr_validate(&dealer)?;
|
||||
let start = start_after.map(Bound::exclusive);
|
||||
|
||||
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)))
|
||||
let dealings = StoredDealing::prefix_range(deps.storage, (epoch_id, &dealer), start)
|
||||
.take(limit as usize)
|
||||
.collect::<StdResult<Vec<_>>>()?;
|
||||
|
||||
let start_next_after = dealings.last().map(|dealing| dealing.dealer.clone());
|
||||
let start_next_after = dealings.last().map(|dealing| dealing.index);
|
||||
|
||||
Ok(PagedDealingsResponse::new(
|
||||
epoch_id,
|
||||
dealer,
|
||||
dealings,
|
||||
limit,
|
||||
start_next_after,
|
||||
))
|
||||
}
|
||||
@@ -48,148 +78,229 @@ pub fn query_dealings_paged(
|
||||
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::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<'_>, 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();
|
||||
});
|
||||
fn fill_dealings(deps: DepsMut<'_>, epoch: EpochId, dealers: usize, key_size: u32) {
|
||||
for i in 0..dealers {
|
||||
let dealer = Addr::unchecked(format!("dealer{i}"));
|
||||
for dealing_index in 0..key_size {
|
||||
StoredDealing::save(
|
||||
deps.storage,
|
||||
epoch,
|
||||
&dealer,
|
||||
PartialContractDealing {
|
||||
index: dealing_index,
|
||||
data: dealing_bytes_fixture(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn 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() {
|
||||
fn test_query_dealing() {
|
||||
let mut deps = init_contract();
|
||||
|
||||
fill_dealings(deps.as_mut(), 1);
|
||||
let bad_address = "FOOMP".to_string();
|
||||
assert!(query_dealing(deps.as_ref(), 0, bad_address, 0).is_err());
|
||||
|
||||
let per_page = 2;
|
||||
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());
|
||||
|
||||
for idx in 0..TOTAL_DEALINGS as u64 {
|
||||
let page1 =
|
||||
query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap();
|
||||
// insert the dealing
|
||||
let dealing = partial_dealing_fixture();
|
||||
StoredDealing::save(
|
||||
deps.as_mut().storage,
|
||||
0,
|
||||
&Addr::unchecked("foo"),
|
||||
dealing.clone(),
|
||||
);
|
||||
|
||||
// page should have 1 result on it
|
||||
assert_eq!(1, page1.dealings.len());
|
||||
let retrieved = query_dealing(deps.as_ref(), 0, "foo".to_string(), 0).unwrap();
|
||||
assert_eq!(retrieved.epoch_id, 0);
|
||||
assert_eq!(retrieved.dealing_index, dealing.index);
|
||||
assert_eq!(retrieved.dealer, Addr::unchecked("foo"));
|
||||
assert_eq!(retrieved.dealing.unwrap(), dealing.data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_dealing_status() {
|
||||
let mut deps = init_contract();
|
||||
|
||||
let bad_address = "FOOMP".to_string();
|
||||
assert!(query_dealing_status(deps.as_ref(), 0, bad_address, 0).is_err());
|
||||
|
||||
let empty = query_dealing_status(deps.as_ref(), 0, "foo".to_string(), 0).unwrap();
|
||||
assert_eq!(empty.epoch_id, 0);
|
||||
assert_eq!(empty.dealing_index, 0);
|
||||
assert_eq!(empty.dealer, Addr::unchecked("foo"));
|
||||
assert!(!empty.dealing_submitted);
|
||||
|
||||
// insert the dealing
|
||||
let dealing = partial_dealing_fixture();
|
||||
StoredDealing::save(
|
||||
deps.as_mut().storage,
|
||||
0,
|
||||
&Addr::unchecked("foo"),
|
||||
dealing.clone(),
|
||||
);
|
||||
|
||||
let retrieved = query_dealing_status(deps.as_ref(), 0, "foo".to_string(), 0).unwrap();
|
||||
assert_eq!(retrieved.epoch_id, 0);
|
||||
assert_eq!(retrieved.dealing_index, dealing.index);
|
||||
assert_eq!(retrieved.dealer, Addr::unchecked("foo"));
|
||||
assert!(retrieved.dealing_submitted)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod query_dealings {
|
||||
use super::*;
|
||||
use nym_coconut_dkg_common::types::DEFAULT_DEALINGS;
|
||||
|
||||
#[test]
|
||||
fn dealings_empty_on_init() {
|
||||
let deps = init_contract();
|
||||
let all_dealings = StoredDealing::unchecked_all_entries(&deps.storage);
|
||||
assert!(all_dealings.is_empty())
|
||||
}
|
||||
|
||||
// save another
|
||||
fill_dealings(deps.as_mut(), 2);
|
||||
#[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 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());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fill_dealings(deps.as_mut(), 3);
|
||||
#[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 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());
|
||||
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();
|
||||
|
||||
// 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());
|
||||
assert_eq!(DEALINGS_PAGE_DEFAULT_LIMIT, page1.dealings.len() as u32);
|
||||
}
|
||||
}
|
||||
|
||||
fill_dealings(deps.as_mut(), 4);
|
||||
#[test]
|
||||
fn dealings_paged_retrieval_has_max_limit() {
|
||||
let mut deps = init_contract();
|
||||
fill_dealings(deps.as_mut(), 0, 10, DEFAULT_DEALINGS as u32);
|
||||
|
||||
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();
|
||||
// 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();
|
||||
|
||||
// now we have 2 pages, with 2 results on the second page
|
||||
assert_eq!(2, page2.dealings.len());
|
||||
// we default to a decent sized upper bound instead
|
||||
let expected_limit = DEALINGS_PAGE_MAX_LIMIT;
|
||||
assert_eq!(expected_limit, page1.dealings.len() as u32);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dealings_pagination_works() {
|
||||
let mut deps = init_contract();
|
||||
|
||||
fill_dealings(deps.as_mut(), 0, 10, 1);
|
||||
let per_page = 2;
|
||||
|
||||
for dealer in 0..10 {
|
||||
let dealer = format!("dealer{dealer}");
|
||||
let page1 =
|
||||
query_dealings_paged(deps.as_ref(), 0, dealer, None, Option::from(per_page))
|
||||
.unwrap();
|
||||
|
||||
// page should have 1 result on it
|
||||
assert_eq!(1, page1.dealings.len());
|
||||
}
|
||||
|
||||
// save another
|
||||
fill_dealings(deps.as_mut(), 1, 10, 2);
|
||||
|
||||
for dealer in 0..10 {
|
||||
let dealer = format!("dealer{dealer}");
|
||||
// page1 should have 2 results on it
|
||||
let page1 =
|
||||
query_dealings_paged(deps.as_ref(), 1, dealer, None, Option::from(per_page))
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.dealings.len());
|
||||
}
|
||||
|
||||
fill_dealings(deps.as_mut(), 3, 10, 3);
|
||||
|
||||
for dealer in 0..10 {
|
||||
let dealer = format!("dealer{dealer}");
|
||||
// page1 still has 2 results
|
||||
let page1 = query_dealings_paged(
|
||||
deps.as_ref(),
|
||||
3,
|
||||
dealer.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(2, page1.dealings.len());
|
||||
|
||||
// retrieving the next page should start after the last key on this page
|
||||
let start_after = page1.start_next_after.unwrap();
|
||||
let page2 = query_dealings_paged(
|
||||
deps.as_ref(),
|
||||
3,
|
||||
dealer,
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, page2.dealings.len());
|
||||
}
|
||||
|
||||
fill_dealings(deps.as_mut(), 4, 10, 4);
|
||||
|
||||
for dealer in 0..10 {
|
||||
let dealer = format!("dealer{dealer}");
|
||||
let page1 = query_dealings_paged(
|
||||
deps.as_ref(),
|
||||
4,
|
||||
dealer.clone(),
|
||||
None,
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
let start_after = page1.start_next_after.unwrap();
|
||||
let page2 = query_dealings_paged(
|
||||
deps.as_ref(),
|
||||
4,
|
||||
dealer,
|
||||
Option::from(start_after),
|
||||
Option::from(per_page),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// now we have 2 pages, with 2 results on the second page
|
||||
assert_eq!(2, page2.dealings.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,297 @@
|
||||
// Copyright 2022 - 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 cosmwasm_std::{Addr, Order, Record, StdResult, Storage};
|
||||
use cw_storage_plus::{Bound, Key, KeyDeserialize, Path, Prefix, Prefixer, PrimaryKey};
|
||||
use nym_coconut_dkg_common::types::{
|
||||
ContractDealing, ContractSafeBytes, DealingIndex, EpochId, PartialContractDealing,
|
||||
};
|
||||
|
||||
pub(crate) const DEALINGS_PAGE_MAX_LIMIT: u32 = 2;
|
||||
pub(crate) const DEALINGS_PAGE_DEFAULT_LIMIT: u32 = 1;
|
||||
|
||||
type DealingKey<'a> = &'a Addr;
|
||||
type Dealer<'a> = &'a Addr;
|
||||
|
||||
// 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.
|
||||
// dealings are stored in a multilevel map with the following hierarchy:
|
||||
// - epoch-id:
|
||||
// - issuer-address:
|
||||
// - dealing 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!
|
||||
|
||||
// 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) struct StoredDealing;
|
||||
|
||||
// part of `StoredDealing` to make existence lookup cheaper
|
||||
// TODO: do it later since we need to chunk the dealings anyway
|
||||
// pub(crate) struct UNIMPLEMENTED_DealingLookup;
|
||||
|
||||
impl StoredDealing {
|
||||
const NAMESPACE: &'static [u8] = b"dealing";
|
||||
|
||||
fn deserialize_dealing_record(kv: Record) -> StdResult<(DealingIndex, ContractDealing)> {
|
||||
let (k, v) = kv;
|
||||
let index = <DealingIndex as KeyDeserialize>::from_vec(k)?;
|
||||
let data = ContractSafeBytes(v);
|
||||
|
||||
Ok((index, data))
|
||||
}
|
||||
|
||||
fn storage_key(
|
||||
epoch_id: EpochId,
|
||||
dealer: Dealer,
|
||||
dealing_index: DealingIndex,
|
||||
) -> Path<Vec<u8>> {
|
||||
// just replicate the behaviour from `Map::key`
|
||||
let key = (epoch_id, dealer, dealing_index);
|
||||
Path::new(
|
||||
Self::NAMESPACE,
|
||||
&key.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn prefix(prefix: (EpochId, Dealer)) -> Prefix<DealingIndex, ContractSafeBytes, DealingIndex> {
|
||||
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,
|
||||
) -> bool {
|
||||
StoredDealing::storage_key(epoch_id, dealer, dealing_index).has(storage)
|
||||
}
|
||||
|
||||
pub(crate) fn save(
|
||||
storage: &mut dyn Storage,
|
||||
epoch_id: EpochId,
|
||||
dealer: Dealer,
|
||||
dealing: PartialContractDealing,
|
||||
) {
|
||||
// NOTE: we're storing bytes directly here!
|
||||
let storage_key = StoredDealing::storage_key(epoch_id, dealer, dealing.index);
|
||||
storage.set(&storage_key, dealing.data.as_slice());
|
||||
}
|
||||
|
||||
pub(crate) fn read(
|
||||
storage: &dyn Storage,
|
||||
epoch_id: EpochId,
|
||||
dealer: Dealer,
|
||||
dealing_index: DealingIndex,
|
||||
) -> Option<ContractDealing> {
|
||||
let storage_key = StoredDealing::storage_key(epoch_id, dealer, dealing_index);
|
||||
let raw_dealing = storage.get(&storage_key);
|
||||
raw_dealing.map(ContractSafeBytes)
|
||||
}
|
||||
|
||||
pub(crate) fn prefix_range<'a>(
|
||||
storage: &'a dyn Storage,
|
||||
prefix: (EpochId, Dealer),
|
||||
start: Option<Bound<DealingIndex>>,
|
||||
) -> impl Iterator<Item = StdResult<PartialContractDealing>> + 'a {
|
||||
Self::prefix(prefix)
|
||||
.range(storage, start, None, Order::Ascending)
|
||||
.map(|maybe_record| maybe_record.map(Into::into))
|
||||
}
|
||||
|
||||
// iterate over all values, only to be used in tests due to the amount of data being returned
|
||||
#[cfg(test)]
|
||||
pub(crate) fn unchecked_all_entries(
|
||||
storage: &dyn Storage,
|
||||
) -> Vec<((EpochId, Addr, DealingIndex), ContractDealing)> {
|
||||
type StorageKey<'a> = (EpochId, Dealer<'a>, DealingIndex);
|
||||
|
||||
let empty_prefix: Prefix<StorageKey, ContractDealing, 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,
|
||||
) -> ContractDealing {
|
||||
ContractSafeBytes(
|
||||
format!("{epoch_id},{dealer},{dealing_index}")
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saving_dealing() {
|
||||
let mut deps = init_contract();
|
||||
|
||||
// make sure to check all combinations of epoch id, dealer address and dealing index to ensure nothing overlaps
|
||||
let epochs = [54, 423, 754];
|
||||
let dealers = [
|
||||
Addr::unchecked("dealer1"),
|
||||
Addr::unchecked("dealer2"),
|
||||
Addr::unchecked("dealer3"),
|
||||
Addr::unchecked("dealer4"),
|
||||
Addr::unchecked("dealer5"),
|
||||
];
|
||||
let dealing_indices = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
|
||||
for epoch_id in &epochs {
|
||||
for dealer in &dealers {
|
||||
for dealing_index in &dealing_indices {
|
||||
assert!(!StoredDealing::exists(
|
||||
&deps.storage,
|
||||
*epoch_id,
|
||||
dealer,
|
||||
*dealing_index
|
||||
));
|
||||
|
||||
StoredDealing::save(
|
||||
deps.as_mut().storage,
|
||||
*epoch_id,
|
||||
dealer,
|
||||
PartialContractDealing {
|
||||
index: *dealing_index,
|
||||
data: dealing_data(*epoch_id, dealer, *dealing_index),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let all: HashMap<_, _> = StoredDealing::unchecked_all_entries(&deps.storage)
|
||||
.into_iter()
|
||||
.collect();
|
||||
assert_eq!(
|
||||
all.len(),
|
||||
epochs.len() * dealers.len() * dealing_indices.len()
|
||||
);
|
||||
|
||||
for epoch_id in &epochs {
|
||||
for dealer in &dealers {
|
||||
for dealing_index in &dealing_indices {
|
||||
assert!(StoredDealing::exists(
|
||||
&deps.storage,
|
||||
*epoch_id,
|
||||
dealer,
|
||||
*dealing_index
|
||||
));
|
||||
|
||||
let content =
|
||||
StoredDealing::read(&deps.storage, *epoch_id, dealer, *dealing_index)
|
||||
.unwrap();
|
||||
let expected = dealing_data(*epoch_id, dealer, *dealing_index);
|
||||
assert_eq!(expected, content);
|
||||
assert_eq!(
|
||||
&expected,
|
||||
all.get(&(*epoch_id, dealer.clone(), *dealing_index))
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iterating_over_dealings() {
|
||||
let mut deps = init_contract();
|
||||
|
||||
let epochs = [54, 423, 754];
|
||||
let dealers = [
|
||||
Addr::unchecked("dealer1"),
|
||||
Addr::unchecked("dealer2"),
|
||||
Addr::unchecked("dealer3"),
|
||||
Addr::unchecked("dealer4"),
|
||||
Addr::unchecked("dealer5"),
|
||||
];
|
||||
let dealing_indices = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
|
||||
for epoch_id in &epochs {
|
||||
for dealer in &dealers {
|
||||
for dealing_index in &dealing_indices {
|
||||
StoredDealing::save(
|
||||
deps.as_mut().storage,
|
||||
*epoch_id,
|
||||
dealer,
|
||||
PartialContractDealing {
|
||||
index: *dealing_index,
|
||||
data: dealing_data(*epoch_id, dealer, *dealing_index),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remember, we're not testing the iterator implementation
|
||||
|
||||
// nothing under epoch 0
|
||||
let dealings =
|
||||
StoredDealing::prefix_range(&deps.storage, (0, &dealers[0]), None).collect::<Vec<_>>();
|
||||
assert!(dealings.is_empty());
|
||||
|
||||
// nothing for dealer "foo"
|
||||
let foo = Addr::unchecked("foo");
|
||||
let dealings =
|
||||
StoredDealing::prefix_range(&deps.storage, (epochs[0], &foo), None).collect::<Vec<_>>();
|
||||
assert!(dealings.is_empty());
|
||||
|
||||
let all = StoredDealing::prefix_range(&deps.storage, (epochs[0], &dealers[0]), None)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(all.len(), dealing_indices.len());
|
||||
|
||||
for (i, dealing) in all.iter().enumerate() {
|
||||
let expected = dealing_data(epochs[0], &dealers[0], dealing_indices[i]);
|
||||
assert_eq!(expected, dealing.as_ref().unwrap().data);
|
||||
assert_eq!(dealing_indices[i], dealing.as_ref().unwrap().index);
|
||||
}
|
||||
|
||||
// for sanity sake, check another dealer with different epoch
|
||||
let all_other = StoredDealing::prefix_range(&deps.storage, (epochs[2], &dealers[3]), None)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(all_other.len(), dealing_indices.len());
|
||||
|
||||
for (i, dealing) in all_other.iter().enumerate() {
|
||||
let expected = dealing_data(epochs[2], &dealers[3], dealing_indices[i]);
|
||||
assert_eq!(expected, dealing.as_ref().unwrap().data);
|
||||
assert_eq!(dealing_indices[i], dealing.as_ref().unwrap().index);
|
||||
}
|
||||
|
||||
let without_first = StoredDealing::prefix_range(
|
||||
&deps.storage,
|
||||
(epochs[0], &dealers[0]),
|
||||
Some(Bound::exclusive(dealing_indices[0])),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(&all[1..], without_first);
|
||||
|
||||
let mid = StoredDealing::prefix_range(
|
||||
&deps.storage,
|
||||
(epochs[0], &dealers[0]),
|
||||
Some(Bound::inclusive(dealing_indices[3])),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(&all[3..], mid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
// 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::StoredDealing;
|
||||
use crate::epoch_state::storage::{CURRENT_EPOCH, INITIAL_REPLACEMENT_DATA};
|
||||
use crate::epoch_state::utils::check_epoch_state;
|
||||
use crate::error::ContractError;
|
||||
use crate::state::storage::STATE;
|
||||
use cosmwasm_std::{DepsMut, MessageInfo, Response};
|
||||
use nym_coconut_dkg_common::types::{ContractSafeBytes, EpochState};
|
||||
use nym_coconut_dkg_common::types::{EpochState, PartialContractDealing};
|
||||
|
||||
pub fn try_commit_dealings(
|
||||
deps: DepsMut<'_>,
|
||||
info: MessageInfo,
|
||||
dealing_bytes: ContractSafeBytes,
|
||||
dealing: PartialContractDealing,
|
||||
resharing: bool,
|
||||
) -> Result<Response, ContractError> {
|
||||
check_epoch_state(deps.storage, EpochState::DealingExchange { resharing })?;
|
||||
@@ -32,47 +33,65 @@ pub fn try_commit_dealings(
|
||||
return Err(ContractError::NotAnInitialDealer);
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
let state = STATE.load(deps.storage)?;
|
||||
let epoch = CURRENT_EPOCH.load(deps.storage)?;
|
||||
|
||||
// check if the index is in range without doing expensive storage reads
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
|
||||
Err(ContractError::AlreadyCommitted {
|
||||
commitment: String::from("dealing"),
|
||||
})
|
||||
// check if this dealer has already committed this particular dealing
|
||||
if StoredDealing::exists(deps.storage, epoch.epoch_id, &info.sender, dealing.index) {
|
||||
return Err(ContractError::DealingAlreadyCommitted {
|
||||
epoch_id: epoch.epoch_id,
|
||||
dealer: info.sender,
|
||||
index: dealing.index,
|
||||
});
|
||||
}
|
||||
|
||||
StoredDealing::save(deps.storage, epoch.epoch_id, &info.sender, dealing);
|
||||
|
||||
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, DEFAULT_DEALINGS,
|
||||
};
|
||||
|
||||
#[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();
|
||||
try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap();
|
||||
|
||||
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), false)
|
||||
.unwrap_err();
|
||||
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::default().to_string(),
|
||||
current_state: EpochState::PublicKeySubmission { resharing: false }.to_string(),
|
||||
expected_state: EpochState::DealingExchange { resharing: false }.to_string()
|
||||
}
|
||||
);
|
||||
@@ -84,13 +103,14 @@ pub(crate) mod tests {
|
||||
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();
|
||||
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,
|
||||
};
|
||||
@@ -114,8 +134,8 @@ pub(crate) mod tests {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), true)
|
||||
.unwrap_err();
|
||||
let ret =
|
||||
try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), true).unwrap_err();
|
||||
assert_eq!(ret, ContractError::NotAnInitialDealer);
|
||||
|
||||
INITIAL_REPLACEMENT_DATA
|
||||
@@ -125,18 +145,60 @@ pub(crate) mod tests {
|
||||
})
|
||||
.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();
|
||||
// back to 'normal' mode
|
||||
CURRENT_EPOCH
|
||||
.update::<_, ContractError>(deps.as_mut().storage, |mut epoch| {
|
||||
epoch.state = EpochState::DealingExchange { resharing: false };
|
||||
Ok(epoch)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// dealing out of range
|
||||
let ret = try_commit_dealings(
|
||||
deps.as_mut(),
|
||||
info.clone(),
|
||||
PartialContractDealing {
|
||||
index: 42,
|
||||
data: ContractSafeBytes(vec![1, 2, 3]),
|
||||
},
|
||||
false,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
ret,
|
||||
ContractError::AlreadyCommitted {
|
||||
commitment: String::from("dealing"),
|
||||
ContractError::DealingOutOfRange {
|
||||
epoch_id: 0,
|
||||
dealer: info.sender.clone(),
|
||||
index: 42,
|
||||
key_size: DEFAULT_DEALINGS as u32,
|
||||
}
|
||||
);
|
||||
|
||||
// 'good' dealing
|
||||
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false);
|
||||
assert!(ret.is_ok());
|
||||
|
||||
// duplicate dealing
|
||||
let ret =
|
||||
try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false).unwrap_err();
|
||||
assert_eq!(
|
||||
ret,
|
||||
ContractError::DealingAlreadyCommitted {
|
||||
epoch_id: 0,
|
||||
dealer: info.sender.clone(),
|
||||
index: 0,
|
||||
}
|
||||
);
|
||||
|
||||
// same index, but next epoch
|
||||
CURRENT_EPOCH
|
||||
.update::<_, ContractError>(deps.as_mut().storage, |mut epoch| {
|
||||
epoch.epoch_id += 1;
|
||||
Ok(epoch)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false);
|
||||
assert!(ret.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,22 +27,29 @@ pub(crate) fn query_initial_dealers(
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use super::*;
|
||||
use crate::support::tests::helpers::init_contract;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use crate::epoch_state::transactions::try_initiate_dkg;
|
||||
use crate::support::tests::helpers::{init_contract, ADMIN_ADDRESS};
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use nym_coconut_dkg_common::types::{EpochState, TimeConfiguration};
|
||||
|
||||
#[test]
|
||||
fn query_state() {
|
||||
let mut deps = init_contract();
|
||||
let epoch = query_current_epoch(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(epoch.state, EpochState::WaitingInitialisation);
|
||||
assert_eq!(epoch.finish_timestamp, None);
|
||||
|
||||
let env = mock_env();
|
||||
try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap();
|
||||
|
||||
let epoch = query_current_epoch(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(
|
||||
epoch.state,
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
mock_env()
|
||||
.block
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block
|
||||
.time
|
||||
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs)
|
||||
);
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::dealers::storage::{current_dealers, past_dealers};
|
||||
use crate::dealings::storage::DEALINGS_BYTES;
|
||||
use crate::epoch_state::storage::{CURRENT_EPOCH, INITIAL_REPLACEMENT_DATA, THRESHOLD};
|
||||
use crate::epoch_state::utils::check_epoch_state;
|
||||
use crate::error::ContractError;
|
||||
use crate::state::STATE;
|
||||
use crate::state::storage::{DKG_ADMIN, STATE};
|
||||
use crate::verification_key_shares::storage::verified_dealers;
|
||||
use cosmwasm_std::{Addr, Deps, DepsMut, Env, Order, Response, Storage};
|
||||
use cosmwasm_std::{Addr, Deps, DepsMut, Env, MessageInfo, Order, Response, Storage};
|
||||
use nym_coconut_dkg_common::types::{Epoch, EpochState, InitialReplacementData};
|
||||
|
||||
fn reset_epoch_state(storage: &mut dyn Storage) -> Result<(), ContractError> {
|
||||
fn reset_dkg_state(storage: &mut dyn Storage) -> Result<(), ContractError> {
|
||||
THRESHOLD.remove(storage);
|
||||
let dealers: Vec<_> = current_dealers()
|
||||
.keys(storage, None, None, Order::Ascending)
|
||||
@@ -19,15 +18,6 @@ fn reset_epoch_state(storage: &mut dyn Storage) -> Result<(), ContractError> {
|
||||
|
||||
for dealer_addr in dealers {
|
||||
let details = current_dealers().load(storage, &dealer_addr)?;
|
||||
for dealings in DEALINGS_BYTES {
|
||||
let dealing_keys: Vec<_> = dealings
|
||||
.keys(storage, None, None, Order::Ascending)
|
||||
.flatten()
|
||||
.collect();
|
||||
for key in dealing_keys {
|
||||
dealings.remove(storage, &key);
|
||||
}
|
||||
}
|
||||
current_dealers().remove(storage, &dealer_addr)?;
|
||||
past_dealers().save(storage, &dealer_addr, &details)?;
|
||||
}
|
||||
@@ -80,18 +70,43 @@ fn replacement_threshold_surpassed(deps: &DepsMut<'_>) -> Result<bool, ContractE
|
||||
Ok(removed_dealer_count >= replacement_threshold)
|
||||
}
|
||||
|
||||
pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Response, ContractError> {
|
||||
pub(crate) fn try_initiate_dkg(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
) -> Result<Response, ContractError> {
|
||||
// only the admin is allowed to kick start the process
|
||||
DKG_ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
|
||||
|
||||
let epoch = CURRENT_EPOCH.load(deps.storage)?;
|
||||
if epoch.finish_timestamp > env.block.time {
|
||||
return Err(ContractError::EarlyEpochStateAdvancement(
|
||||
epoch
|
||||
.finish_timestamp
|
||||
.minus_seconds(env.block.time.seconds())
|
||||
.seconds(),
|
||||
));
|
||||
if !matches!(epoch.state, EpochState::WaitingInitialisation) {
|
||||
return Err(ContractError::AlreadyInitialised);
|
||||
}
|
||||
|
||||
// the first exchange won't involve resharing
|
||||
let initial_state = EpochState::PublicKeySubmission { resharing: false };
|
||||
let initial_epoch = Epoch::new(initial_state, 0, epoch.time_configuration, env.block.time);
|
||||
CURRENT_EPOCH.save(deps.storage, &initial_epoch)?;
|
||||
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Response, ContractError> {
|
||||
let current_epoch = CURRENT_EPOCH.load(deps.storage)?;
|
||||
if current_epoch.state == EpochState::WaitingInitialisation {
|
||||
return Err(ContractError::WaitingInitialisation);
|
||||
}
|
||||
|
||||
if let Some(finish_timestamp) = current_epoch.finish_timestamp {
|
||||
if finish_timestamp > env.block.time {
|
||||
return Err(ContractError::EarlyEpochStateAdvancement(
|
||||
finish_timestamp
|
||||
.minus_seconds(env.block.time.seconds())
|
||||
.seconds(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let next_epoch = if let Some(state) = current_epoch.state.next() {
|
||||
// We are during DKG process
|
||||
let mut new_state = state;
|
||||
@@ -123,6 +138,8 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
|
||||
} else if dealers_eq_members(&deps)? {
|
||||
// The dealer set hasn't changed, so we only extend the finish timestamp
|
||||
// The epoch remains the same, as we use it as key for storing VKs
|
||||
|
||||
// TODO: change that behaviour in the following PR
|
||||
Epoch::new(
|
||||
current_epoch.state,
|
||||
current_epoch.epoch_id,
|
||||
@@ -139,6 +156,7 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
|
||||
// ... in reshare mode
|
||||
if INITIAL_REPLACEMENT_DATA.may_load(deps.storage)?.is_some() {
|
||||
INITIAL_REPLACEMENT_DATA.update::<_, ContractError>(deps.storage, |mut data| {
|
||||
// TODO: FIXME: for second reshare the added set of dealers won't be allowed to participate
|
||||
data.initial_height = env.block.height;
|
||||
Ok(data)
|
||||
})?;
|
||||
@@ -152,7 +170,7 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
|
||||
|
||||
EpochState::PublicKeySubmission { resharing: true }
|
||||
};
|
||||
reset_epoch_state(deps.storage)?;
|
||||
reset_dkg_state(deps.storage)?;
|
||||
Epoch::new(
|
||||
state,
|
||||
current_epoch.epoch_id + 1,
|
||||
@@ -174,7 +192,7 @@ pub(crate) fn try_surpassed_threshold(
|
||||
let threshold = THRESHOLD.load(deps.storage)?;
|
||||
let dealers = verified_dealers(deps.storage)?;
|
||||
if dealers_still_active(&deps.as_ref(), dealers.into_iter())? < threshold as usize {
|
||||
reset_epoch_state(deps.storage)?;
|
||||
reset_dkg_state(deps.storage)?;
|
||||
CURRENT_EPOCH.update::<_, ContractError>(deps.storage, |epoch| {
|
||||
Ok(Epoch::new(
|
||||
EpochState::default(),
|
||||
@@ -193,20 +211,19 @@ pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::error::ContractError::EarlyEpochStateAdvancement;
|
||||
use crate::support::tests::fixtures::{dealer_details_fixture, vk_share_fixture};
|
||||
use crate::support::tests::helpers::{init_contract, GROUP_MEMBERS};
|
||||
use crate::support::tests::helpers::{init_contract, ADMIN_ADDRESS, GROUP_MEMBERS};
|
||||
use crate::verification_key_shares::storage::vk_shares;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::Addr;
|
||||
use cw4::Member;
|
||||
use nym_coconut_dkg_common::types::{
|
||||
ContractSafeBytes, DealerDetails, EpochState, TimeConfiguration,
|
||||
};
|
||||
use cw_controllers::AdminError;
|
||||
use nym_coconut_dkg_common::types::{DealerDetails, EpochState, TimeConfiguration};
|
||||
use rusty_fork::rusty_fork_test;
|
||||
|
||||
// Because of the global variable handling group, we need individual process for each test
|
||||
|
||||
rusty_fork_test! {
|
||||
// Using values from the DKG document
|
||||
// Using values from the DKG document
|
||||
#[test]
|
||||
fn threshold_surpassed() {
|
||||
let mut deps = init_contract();
|
||||
@@ -217,14 +234,18 @@ pub(crate) mod tests {
|
||||
|
||||
for n in [10, 25, 50, 100] {
|
||||
let dealers: Vec<_> = (0..n).map(dealer_details_fixture).collect();
|
||||
let shares: Vec<_> = (0..n).map(|idx| vk_share_fixture(&format!("owner{}", idx), 0)).collect();
|
||||
let shares: Vec<_> = (0..n)
|
||||
.map(|idx| vk_share_fixture(&format!("owner{}", idx), 0))
|
||||
.collect();
|
||||
let initial_dealers = dealers.iter().map(|d| d.address.clone()).collect();
|
||||
let data = InitialReplacementData {
|
||||
initial_dealers,
|
||||
initial_height: 1,
|
||||
};
|
||||
for share in shares {
|
||||
vk_shares().save(deps.as_mut().storage, (&share.owner, 0), &share).unwrap();
|
||||
vk_shares()
|
||||
.save(deps.as_mut().storage, (&share.owner, 0), &share)
|
||||
.unwrap();
|
||||
}
|
||||
for f in [two_thirds, three_fourths, ninty_pc] {
|
||||
let threshold = f(n);
|
||||
@@ -361,7 +382,8 @@ pub(crate) mod tests {
|
||||
fn advance_state() {
|
||||
let mut deps = init_contract();
|
||||
let mut env = mock_env();
|
||||
{
|
||||
|
||||
{
|
||||
let mut group = GROUP_MEMBERS.lock().unwrap();
|
||||
|
||||
group.push((
|
||||
@@ -394,13 +416,21 @@ pub(crate) mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
// can't advance the state if dkg hasn't been initiated
|
||||
assert_eq!(
|
||||
advance_epoch_state(deps.as_mut(), env.clone()).unwrap_err(),
|
||||
ContractError::WaitingInitialisation
|
||||
);
|
||||
|
||||
try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap();
|
||||
|
||||
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(
|
||||
epoch.state,
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block
|
||||
.time
|
||||
.plus_seconds(epoch.time_configuration.public_key_submission_time_secs)
|
||||
@@ -424,7 +454,8 @@ pub(crate) mod tests {
|
||||
);
|
||||
|
||||
// setup dealer details
|
||||
let all_shares: [_; 4] = std::array::from_fn(|i| vk_share_fixture(&format!("owner{}", i + 1), 0));
|
||||
let all_shares: [_; 4] =
|
||||
std::array::from_fn(|i| vk_share_fixture(&format!("owner{}", i + 1), 0));
|
||||
for share in all_shares.iter() {
|
||||
vk_shares()
|
||||
.save(deps.as_mut().storage, (&share.owner, 0), share)
|
||||
@@ -441,7 +472,10 @@ pub(crate) mod tests {
|
||||
.may_load(&deps.storage)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
env.block.time = env.block.time.plus_seconds(epoch.time_configuration.public_key_submission_time_secs);
|
||||
env.block.time = env
|
||||
.block
|
||||
.time
|
||||
.plus_seconds(epoch.time_configuration.public_key_submission_time_secs);
|
||||
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
|
||||
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(
|
||||
@@ -449,7 +483,7 @@ pub(crate) mod tests {
|
||||
EpochState::DealingExchange { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block
|
||||
.time
|
||||
.plus_seconds(epoch.time_configuration.dealing_exchange_time_secs)
|
||||
@@ -472,7 +506,7 @@ pub(crate) mod tests {
|
||||
EpochState::VerificationKeySubmission { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block.time.plus_seconds(
|
||||
epoch
|
||||
.time_configuration
|
||||
@@ -499,7 +533,7 @@ pub(crate) mod tests {
|
||||
EpochState::VerificationKeyValidation { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block.time.plus_seconds(
|
||||
epoch
|
||||
.time_configuration
|
||||
@@ -526,7 +560,7 @@ pub(crate) mod tests {
|
||||
EpochState::VerificationKeyFinalization { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block.time.plus_seconds(
|
||||
epoch
|
||||
.time_configuration
|
||||
@@ -548,7 +582,7 @@ pub(crate) mod tests {
|
||||
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
|
||||
assert_eq!(epoch.state, EpochState::InProgress);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp,
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block
|
||||
.time
|
||||
.plus_seconds(epoch.time_configuration.in_progress_time_secs)
|
||||
@@ -614,7 +648,9 @@ pub(crate) mod tests {
|
||||
|
||||
let all_details: [_; 4] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 2));
|
||||
for details in all_details.iter() {
|
||||
past_dealers().remove(deps.as_mut().storage, &details.address).unwrap();
|
||||
past_dealers()
|
||||
.remove(deps.as_mut().storage, &details.address)
|
||||
.unwrap();
|
||||
current_dealers()
|
||||
.save(deps.as_mut().storage, &details.address, details)
|
||||
.unwrap();
|
||||
@@ -622,9 +658,15 @@ pub(crate) mod tests {
|
||||
for times in [
|
||||
epoch.time_configuration.public_key_submission_time_secs,
|
||||
epoch.time_configuration.dealing_exchange_time_secs,
|
||||
epoch.time_configuration.verification_key_submission_time_secs,
|
||||
epoch.time_configuration.verification_key_validation_time_secs,
|
||||
epoch.time_configuration.verification_key_finalization_time_secs,
|
||||
epoch
|
||||
.time_configuration
|
||||
.verification_key_submission_time_secs,
|
||||
epoch
|
||||
.time_configuration
|
||||
.verification_key_validation_time_secs,
|
||||
epoch
|
||||
.time_configuration
|
||||
.verification_key_finalization_time_secs,
|
||||
] {
|
||||
env.block.time = env.block.time.plus_seconds(times);
|
||||
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
|
||||
@@ -634,7 +676,7 @@ pub(crate) mod tests {
|
||||
let mut share = vk_share_fixture(&format!("owner{}", i + 1), 1);
|
||||
share.verified = i % 2 == 0;
|
||||
share
|
||||
});
|
||||
});
|
||||
for share in all_shares.iter() {
|
||||
vk_shares()
|
||||
.save(deps.as_mut().storage, (&share.owner, 0), share)
|
||||
@@ -670,6 +712,8 @@ pub(crate) mod tests {
|
||||
fn surpass_threshold() {
|
||||
let mut deps = init_contract();
|
||||
let mut env = mock_env();
|
||||
try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap();
|
||||
|
||||
let time_configuration = TimeConfiguration::default();
|
||||
{
|
||||
let mut group = GROUP_MEMBERS.lock().unwrap();
|
||||
@@ -701,13 +745,13 @@ pub(crate) mod tests {
|
||||
assert_eq!(
|
||||
ret,
|
||||
ContractError::IncorrectEpochState {
|
||||
current_state: EpochState::default().to_string(),
|
||||
current_state: EpochState::PublicKeySubmission { resharing: false }.to_string(),
|
||||
expected_state: EpochState::InProgress.to_string()
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
let all_shares: [_; 3] = std::array::from_fn(|i| vk_share_fixture(&format!("owner{}", i + 1), 0));
|
||||
let all_shares: [_; 3] =
|
||||
std::array::from_fn(|i| vk_share_fixture(&format!("owner{}", i + 1), 0));
|
||||
for share in all_shares.iter() {
|
||||
vk_shares()
|
||||
.save(deps.as_mut().storage, (&share.owner, 0), share)
|
||||
@@ -719,7 +763,8 @@ pub(crate) mod tests {
|
||||
.save(deps.as_mut().storage, &details.address, details)
|
||||
.unwrap();
|
||||
}
|
||||
let all_shares: [_; 3] = std::array::from_fn(|i| vk_share_fixture(&format!("owner{}", i + 1), 0));
|
||||
let all_shares: [_; 3] =
|
||||
std::array::from_fn(|i| vk_share_fixture(&format!("owner{}", i + 1), 0));
|
||||
for share in all_shares.iter() {
|
||||
vk_shares()
|
||||
.save(deps.as_mut().storage, (&share.owner, share.epoch_id), share)
|
||||
@@ -778,6 +823,46 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialising_dkg() {
|
||||
let mut deps = init_contract();
|
||||
let env = mock_env();
|
||||
|
||||
let initial_epoch_info = CURRENT_EPOCH.load(&deps.storage).unwrap();
|
||||
assert!(initial_epoch_info.finish_timestamp.is_none());
|
||||
|
||||
// can only be executed by the admin
|
||||
let res = try_initiate_dkg(deps.as_mut(), env.clone(), mock_info("not an admin", &[]))
|
||||
.unwrap_err();
|
||||
assert_eq!(ContractError::Admin(AdminError::NotAdmin {}), res);
|
||||
|
||||
let res = try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[]));
|
||||
assert!(res.is_ok());
|
||||
|
||||
// can't be initialised more than once
|
||||
let res = try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[]))
|
||||
.unwrap_err();
|
||||
assert_eq!(ContractError::AlreadyInitialised, res);
|
||||
|
||||
// sets the correct epoch data
|
||||
let epoch = CURRENT_EPOCH.load(&deps.storage).unwrap();
|
||||
assert_eq!(epoch.epoch_id, 0);
|
||||
assert_eq!(
|
||||
epoch.state,
|
||||
EpochState::PublicKeySubmission { resharing: false }
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.time_configuration,
|
||||
initial_epoch_info.time_configuration
|
||||
);
|
||||
assert_eq!(
|
||||
epoch.finish_timestamp.unwrap(),
|
||||
env.block
|
||||
.time
|
||||
.plus_seconds(epoch.time_configuration.public_key_submission_time_secs)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_state() {
|
||||
let mut deps = init_contract();
|
||||
@@ -788,27 +873,12 @@ pub(crate) mod tests {
|
||||
current_dealers()
|
||||
.save(deps.as_mut().storage, &details.address, details)
|
||||
.unwrap();
|
||||
for dealings in DEALINGS_BYTES {
|
||||
dealings
|
||||
.save(
|
||||
deps.as_mut().storage,
|
||||
&details.address,
|
||||
&ContractSafeBytes(vec![1, 2, 3]),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
reset_epoch_state(deps.as_mut().storage).unwrap();
|
||||
reset_dkg_state(deps.as_mut().storage).unwrap();
|
||||
|
||||
assert!(THRESHOLD.may_load(&deps.storage).unwrap().is_none());
|
||||
for details in all_details {
|
||||
for dealings in DEALINGS_BYTES {
|
||||
assert!(dealings
|
||||
.may_load(&deps.storage, &details.address)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
assert!(current_dealers()
|
||||
.may_load(deps.as_mut().storage, &details.address)
|
||||
.unwrap()
|
||||
@@ -826,6 +896,7 @@ pub(crate) mod tests {
|
||||
fn verify_threshold() {
|
||||
let mut deps = init_contract();
|
||||
let mut env = mock_env();
|
||||
try_initiate_dkg(deps.as_mut(), env.clone(), mock_info(ADMIN_ADDRESS, &[])).unwrap();
|
||||
|
||||
assert!(THRESHOLD.may_load(deps.as_mut().storage).unwrap().is_none());
|
||||
|
||||
@@ -838,6 +909,7 @@ pub(crate) mod tests {
|
||||
&DealerDetails {
|
||||
address: address.clone(),
|
||||
bte_public_key_with_proof: "bte_public_key_with_proof".to_string(),
|
||||
ed25519_identity: "identity".to_string(),
|
||||
announce_address: "127.0.0.1".to_string(),
|
||||
assigned_index: i,
|
||||
},
|
||||
|
||||
@@ -33,14 +33,14 @@ pub(crate) mod test {
|
||||
let mut deps = init_contract();
|
||||
let env = mock_env();
|
||||
|
||||
for fixed_state in EpochState::default().all_until(EpochState::InProgress) {
|
||||
for fixed_state in EpochState::first().all_until(EpochState::InProgress) {
|
||||
CURRENT_EPOCH
|
||||
.save(
|
||||
deps.as_mut().storage,
|
||||
&Epoch::new(fixed_state, 0, TimeConfiguration::default(), env.block.time),
|
||||
)
|
||||
.unwrap();
|
||||
for against_state in EpochState::default().all_until(EpochState::InProgress) {
|
||||
for against_state in EpochState::first().all_until(EpochState::InProgress) {
|
||||
let ret = check_epoch_state(deps.as_mut().storage, against_state);
|
||||
if fixed_state == against_state {
|
||||
assert!(ret.is_ok());
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// 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::types::{DealingIndex, EpochId};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Custom errors for contract failure conditions.
|
||||
@@ -14,6 +15,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 },
|
||||
|
||||
@@ -43,9 +50,34 @@ pub enum ContractError {
|
||||
#[error("This sender is not a dealer for the current resharing epoch")]
|
||||
NotAnInitialDealer,
|
||||
|
||||
#[error(
|
||||
"Dealer {dealer} has already committed dealing for epoch {epoch_id} with index {index}"
|
||||
)]
|
||||
DealingAlreadyCommitted {
|
||||
epoch_id: EpochId,
|
||||
dealer: Addr,
|
||||
index: DealingIndex,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"Dealer {dealer} has attempted to commit dealing for epoch {epoch_id} with index {index} while the key size is set to {key_size}"
|
||||
)]
|
||||
DealingOutOfRange {
|
||||
epoch_id: EpochId,
|
||||
dealer: Addr,
|
||||
index: DealingIndex,
|
||||
key_size: u32,
|
||||
},
|
||||
|
||||
#[error("This dealer has already committed {commitment}")]
|
||||
AlreadyCommitted { commitment: String },
|
||||
|
||||
#[error("No verification key committed for owner {owner}")]
|
||||
NoCommitForOwner { owner: String },
|
||||
|
||||
#[error("failed to parse {value} into a valid SemVer version: {error_message}")]
|
||||
SemVerFailure {
|
||||
value: String,
|
||||
error_message: String,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::Addr;
|
||||
use cw4::Cw4Contract;
|
||||
use cw_controllers::Admin;
|
||||
use cw_storage_plus::Item;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// unique items
|
||||
pub const STATE: Item<State> = Item::new("state");
|
||||
pub const MULTISIG: Admin = Admin::new("multisig");
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||
pub struct State {
|
||||
pub mix_denom: String,
|
||||
pub multisig_addr: Addr,
|
||||
pub group_addr: Cw4Contract,
|
||||
}
|
||||
pub mod queries;
|
||||
pub mod storage;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::state::storage::STATE;
|
||||
use cosmwasm_std::{StdResult, Storage};
|
||||
use nym_coconut_dkg_common::types::State;
|
||||
|
||||
pub(crate) fn query_state(storage: &dyn Storage) -> StdResult<State> {
|
||||
STATE.load(storage)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cw_controllers::Admin;
|
||||
use cw_storage_plus::Item;
|
||||
use nym_coconut_dkg_common::types::State;
|
||||
|
||||
// unique items
|
||||
pub const DKG_ADMIN: Admin = Admin::new("dkg-admin");
|
||||
|
||||
pub const STATE: Item<State> = Item::new("state");
|
||||
|
||||
pub const MULTISIG: Admin = Admin::new("multisig");
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::types::ContractSafeBytes;
|
||||
use nym_coconut_dkg_common::types::{ContractSafeBytes, PartialContractDealing};
|
||||
use nym_coconut_dkg_common::verification_key::ContractVKShare;
|
||||
|
||||
pub const TEST_MIX_DENOM: &str = "unym";
|
||||
@@ -20,13 +20,21 @@ 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 {
|
||||
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,9 +9,8 @@ use cosmwasm_std::{
|
||||
QuerierResult, SystemResult, WasmQuery,
|
||||
};
|
||||
use cw4::{Cw4QueryMsg, Member, MemberListResponse, MemberResponse};
|
||||
use lazy_static::lazy_static;
|
||||
use nym_coconut_dkg_common::msg::InstantiateMsg;
|
||||
use nym_coconut_dkg_common::types::DealerDetails;
|
||||
use nym_coconut_dkg_common::types::{DealerDetails, DEFAULT_DEALINGS};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use super::fixtures::TEST_MIX_DENOM;
|
||||
@@ -20,9 +19,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 +30,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 +85,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,7 +6,7 @@ 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 nym_coconut_dkg_common::types::EpochState;
|
||||
@@ -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),
|
||||
@@ -91,9 +92,9 @@ 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 cw_controllers::AdminError;
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
@@ -103,6 +104,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 +125,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 +153,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 +169,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 +199,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,6 +230,8 @@ 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 multisig_info = mock_info(MULTISIG_CONTRACT, &[]);
|
||||
@@ -233,7 +242,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,6 +289,8 @@ mod tests {
|
||||
fn 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 owner = Addr::unchecked("owner");
|
||||
let info = mock_info(owner.as_ref(), &[]);
|
||||
let share = "share".to_string();
|
||||
@@ -300,6 +311,7 @@ mod tests {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ use cw4::Member;
|
||||
use cw_multi_test::Executor;
|
||||
use cw_utils::{Duration, Threshold};
|
||||
use nym_coconut_dkg_common::msg::ExecuteMsg::{
|
||||
AdvanceEpochState, CommitVerificationKeyShare, RegisterDealer,
|
||||
AdvanceEpochState, CommitVerificationKeyShare, InitiateDkg, RegisterDealer,
|
||||
};
|
||||
use nym_coconut_dkg_common::msg::InstantiateMsg as DkgInstantiateMsg;
|
||||
use nym_coconut_dkg_common::msg::QueryMsg::GetVerificationKeys;
|
||||
@@ -75,6 +75,7 @@ fn dkg_proposal() {
|
||||
multisig_addr: multisig_contract_addr.to_string(),
|
||||
time_configuration: None,
|
||||
mix_denom: TEST_COIN_DENOM.to_string(),
|
||||
key_size: 5,
|
||||
};
|
||||
let coconut_dkg_contract_addr = app
|
||||
.instantiate_contract(
|
||||
@@ -99,11 +100,20 @@ fn dkg_proposal() {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
app.execute_contract(
|
||||
Addr::unchecked(OWNER),
|
||||
coconut_dkg_contract_addr.clone(),
|
||||
&InitiateDkg {},
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
app.execute_contract(
|
||||
Addr::unchecked(MEMBER1),
|
||||
coconut_dkg_contract_addr.clone(),
|
||||
&RegisterDealer {
|
||||
bte_key_with_proof: "bte_key_with_proof".to_string(),
|
||||
identity_key: "identity".to_string(),
|
||||
announce_address: "127.0.0.1:8000".to_string(),
|
||||
resharing: false,
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@ bs58 = "0.4.0"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
time = { version = "0.3", features = ["macros"] }
|
||||
semver = { version = "1.0.16", default-features = false }
|
||||
semver = { workspace = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.2"
|
||||
|
||||
@@ -20,7 +20,7 @@ cw-utils = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" }
|
||||
nym-name-service-common = { path = "../../common/cosmwasm-smart-contracts/name-service" }
|
||||
semver = { version = "1.0.16", default-features = false }
|
||||
semver = { workspace = true, default-features = false }
|
||||
serde = { version = "1.0.155", default-features = false, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ cw-utils = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" }
|
||||
nym-service-provider-directory-common = { path = "../../common/cosmwasm-smart-contracts/service-provider-directory" }
|
||||
semver = { version = "1.0.16", default-features = false }
|
||||
semver = { workspace = true, default-features = false }
|
||||
serde = { version = "1.0.155", default-features = false, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ cw-storage-plus = { workspace = true, features = ["iterator"] }
|
||||
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
thiserror ={ workspace = true }
|
||||
semver = { version = "1.0.16", default-features = false }
|
||||
semver = { workspace = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.3.1"
|
||||
|
||||
@@ -125,3 +125,7 @@ sqlx = { workspace = true, features = [
|
||||
[dev-dependencies]
|
||||
cw3 = { workspace = true }
|
||||
cw-utils = { workspace = true }
|
||||
rand_chacha = "0.3"
|
||||
rand_chacha_02 = { package = "rand_chacha", version = "0.2" }
|
||||
sha2 = "0.9"
|
||||
|
||||
|
||||
@@ -58,7 +58,10 @@ pub async fn post_blind_sign(
|
||||
|
||||
// check if we have the signing key available
|
||||
debug!("checking if we actually have coconut keys derived...");
|
||||
let keypair_guard = state.coconut_keypair.get().await;
|
||||
let maybe_keypair_guard = state.coconut_keypair.get().await;
|
||||
let Some(keypair_guard) = maybe_keypair_guard.as_ref() else {
|
||||
return Err(CoconutError::KeyPairNotDerivedYet);
|
||||
};
|
||||
let Some(signing_key) = keypair_guard.as_ref() else {
|
||||
return Err(CoconutError::KeyPairNotDerivedYet);
|
||||
};
|
||||
@@ -75,7 +78,7 @@ pub async fn post_blind_sign(
|
||||
|
||||
// produce the partial signature
|
||||
debug!("producing the partial credential");
|
||||
let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.secret_key())?;
|
||||
let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.keys.secret_key())?;
|
||||
|
||||
// store the information locally
|
||||
debug!("storing the issued credential in the database");
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
// 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, DealingStatusResponse};
|
||||
use nym_coconut_dkg_common::types::{
|
||||
EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData,
|
||||
DealingIndex, EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData,
|
||||
PartialContractDealing, 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,20 +19,41 @@ 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_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_dealings(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
dealer: &str,
|
||||
) -> Result<Vec<PartialContractDealing>>;
|
||||
|
||||
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<()>;
|
||||
@@ -40,12 +62,13 @@ pub trait Client {
|
||||
async fn register_dealer(
|
||||
&self,
|
||||
bte_key: EncodedBTEPublicKeyWithProof,
|
||||
identity_key: IdentityKey,
|
||||
announce_address: String,
|
||||
resharing: bool,
|
||||
) -> Result<ExecuteResult>;
|
||||
async fn submit_dealing(
|
||||
&self,
|
||||
dealing_bytes: ContractSafeBytes,
|
||||
dealing: PartialContractDealing,
|
||||
resharing: bool,
|
||||
) -> Result<ExecuteResult>;
|
||||
async fn submit_verification_key_share(
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -3,14 +3,15 @@
|
||||
|
||||
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::types::{
|
||||
EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, NodeIndex,
|
||||
DealingIndex, EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, NodeIndex,
|
||||
PartialContractDealing, 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 +22,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 +35,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 +75,43 @@ impl DkgClient {
|
||||
self.inner.get_current_dealers().await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_dealing_status(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
dealing_index: DealingIndex,
|
||||
) -> Result<bool, CoconutError> {
|
||||
let address = self.inner.address().await.to_string();
|
||||
|
||||
self.inner
|
||||
.get_dealing_status(epoch_id, address, dealing_index)
|
||||
.await
|
||||
.map(|r| r.dealing_submitted)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_dealings(
|
||||
&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<Vec<PartialContractDealing>, CoconutError> {
|
||||
self.inner.get_dealings(epoch_id, &dealer).await
|
||||
}
|
||||
|
||||
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 +121,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 +144,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 {
|
||||
@@ -131,10 +167,10 @@ impl DkgClient {
|
||||
|
||||
pub(crate) async fn submit_dealing(
|
||||
&self,
|
||||
dealing_bytes: ContractSafeBytes,
|
||||
dealing: PartialContractDealing,
|
||||
resharing: bool,
|
||||
) -> Result<(), CoconutError> {
|
||||
self.inner.submit_dealing(dealing_bytes, resharing).await?;
|
||||
self.inner.submit_dealing(dealing, resharing).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -143,20 +179,9 @@ impl DkgClient {
|
||||
share: VerificationKeyShare,
|
||||
resharing: bool,
|
||||
) -> Result<ExecuteResult, CoconutError> {
|
||||
let mut ret = self
|
||||
.inner
|
||||
self.inner
|
||||
.submit_verification_key_share(share.clone(), resharing)
|
||||
.await;
|
||||
for _ in 0..Self::RETRIES {
|
||||
if let Ok(res) = ret {
|
||||
return Ok(res);
|
||||
}
|
||||
ret = self
|
||||
.inner
|
||||
.submit_verification_key_share(share.clone(), resharing)
|
||||
.await;
|
||||
}
|
||||
ret
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn vote_verification_key_share(
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub(crate) enum ComplaintReason {
|
||||
MalformedBTEPublicKey,
|
||||
InvalidBTEPublicKey,
|
||||
MissingDealing,
|
||||
MalformedDealing,
|
||||
DealingVerificationError,
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::client::DkgClient;
|
||||
use crate::coconut::dkg::state::{ConsistentState, PersistentState, State};
|
||||
use crate::coconut::dkg::verification_key::{
|
||||
verification_key_finalization, verification_key_validation,
|
||||
};
|
||||
use crate::coconut::dkg::{
|
||||
dealing::dealing_exchange, public_key::public_key_submission,
|
||||
verification_key::verification_key_submission,
|
||||
};
|
||||
use crate::coconut::keypair::KeyPair as CoconutKeyPair;
|
||||
use crate::nyxd;
|
||||
use crate::support::config;
|
||||
use anyhow::{bail, Result};
|
||||
use nym_coconut_dkg_common::types::EpochState;
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use nym_task::{TaskClient, TaskManager};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::time::interval;
|
||||
|
||||
pub(crate) fn init_keypair(config: &config::CoconutSigner) -> Result<()> {
|
||||
let mut rng = OsRng;
|
||||
let dkg_params = nym_dkg::bte::setup();
|
||||
let kp = DkgKeyPair::new(&dkg_params, &mut rng);
|
||||
nym_pemstore::store_keypair(
|
||||
&kp,
|
||||
&nym_pemstore::KeyPairPath::new(
|
||||
&config.storage_paths.decryption_key_path,
|
||||
&config.storage_paths.public_key_with_proof_path,
|
||||
),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) struct DkgController<R> {
|
||||
dkg_client: DkgClient,
|
||||
secret_key_path: PathBuf,
|
||||
verification_key_path: PathBuf,
|
||||
state: State,
|
||||
rng: R,
|
||||
polling_rate: Duration,
|
||||
}
|
||||
|
||||
impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
|
||||
pub(crate) async fn new(
|
||||
config: &config::CoconutSigner,
|
||||
nyxd_client: nyxd::Client,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
rng: R,
|
||||
) -> Result<Self> {
|
||||
let Some(announce_address) = &config.announce_address else {
|
||||
bail!("can't start a DKG controller without specifying an announce address!")
|
||||
};
|
||||
|
||||
let dkg_keypair = nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new(
|
||||
&config.storage_paths.decryption_key_path,
|
||||
&config.storage_paths.public_key_with_proof_path,
|
||||
))?;
|
||||
if let Ok(coconut_keypair_value) =
|
||||
nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new(
|
||||
&config.storage_paths.secret_key_path,
|
||||
&config.storage_paths.verification_key_path,
|
||||
))
|
||||
{
|
||||
coconut_keypair.set(Some(coconut_keypair_value)).await;
|
||||
}
|
||||
let persistent_state =
|
||||
PersistentState::load_from_file(&config.storage_paths.dkg_persistent_state_path)
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(DkgController {
|
||||
dkg_client: DkgClient::new(nyxd_client),
|
||||
secret_key_path: config.storage_paths.secret_key_path.clone(),
|
||||
verification_key_path: config.storage_paths.verification_key_path.clone(),
|
||||
state: State::new(
|
||||
config.storage_paths.dkg_persistent_state_path.clone(),
|
||||
persistent_state,
|
||||
announce_address.clone(),
|
||||
dkg_keypair,
|
||||
coconut_keypair,
|
||||
),
|
||||
rng,
|
||||
polling_rate: config.debug.dkg_contract_polling_rate,
|
||||
})
|
||||
}
|
||||
|
||||
async fn dump_persistent_state(&self) {
|
||||
if !self.state.coconut_keypair_is_some().await {
|
||||
// Delete the files just in case the process is killed before the new keys are generated
|
||||
std::fs::remove_file(&self.secret_key_path).ok();
|
||||
std::fs::remove_file(&self.verification_key_path).ok();
|
||||
}
|
||||
let persistent_state = PersistentState::from(&self.state);
|
||||
if let Err(err) = persistent_state.save_to_file(self.state.persistent_state_path()) {
|
||||
warn!("Could not backup the state for this iteration: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_epoch_state(&mut self) {
|
||||
match self.dkg_client.get_current_epoch().await {
|
||||
Err(err) => warn!("Could not get current epoch state {err}"),
|
||||
Ok(epoch) => {
|
||||
if self
|
||||
.dkg_client
|
||||
.group_member()
|
||||
.await
|
||||
.map(|resp| resp.weight.is_none())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
debug!("Not a member of the group, DKG won't be run");
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self.state.is_consistent(epoch.state).await {
|
||||
debug!("Epoch state is corrupted - {err}. Awaiting for a DKG restart.");
|
||||
} else {
|
||||
let ret = match epoch.state {
|
||||
EpochState::PublicKeySubmission { resharing } => {
|
||||
public_key_submission(&self.dkg_client, &mut self.state, resharing)
|
||||
.await
|
||||
}
|
||||
EpochState::DealingExchange { resharing } => {
|
||||
dealing_exchange(
|
||||
&self.dkg_client,
|
||||
&mut self.state,
|
||||
self.rng.clone(),
|
||||
resharing,
|
||||
)
|
||||
.await
|
||||
}
|
||||
EpochState::VerificationKeySubmission { resharing } => {
|
||||
let keypair_path = nym_pemstore::KeyPairPath::new(
|
||||
self.secret_key_path.clone(),
|
||||
self.verification_key_path.clone(),
|
||||
);
|
||||
verification_key_submission(
|
||||
&self.dkg_client,
|
||||
&mut self.state,
|
||||
&keypair_path,
|
||||
resharing,
|
||||
)
|
||||
.await
|
||||
}
|
||||
EpochState::VerificationKeyValidation { resharing } => {
|
||||
verification_key_validation(
|
||||
&self.dkg_client,
|
||||
&mut self.state,
|
||||
resharing,
|
||||
)
|
||||
.await
|
||||
}
|
||||
EpochState::VerificationKeyFinalization { resharing } => {
|
||||
verification_key_finalization(
|
||||
&self.dkg_client,
|
||||
&mut self.state,
|
||||
resharing,
|
||||
)
|
||||
.await
|
||||
}
|
||||
// Just wait, in case we need to redo dkg at some point
|
||||
EpochState::InProgress => {
|
||||
self.state.set_was_in_progress();
|
||||
// We're dumping state here so that we don't do it uselessly during the
|
||||
// long InProgress state
|
||||
self.dump_persistent_state().await;
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
if let Err(err) = ret {
|
||||
warn!("Could not handle this iteration for the epoch state: {err}");
|
||||
} else if epoch.state != EpochState::InProgress {
|
||||
self.dump_persistent_state().await;
|
||||
}
|
||||
}
|
||||
if let Ok(current_timestamp) =
|
||||
SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)
|
||||
{
|
||||
if current_timestamp.as_secs() >= epoch.finish_timestamp.seconds() {
|
||||
// We try advancing the epoch state, on a best-effort basis
|
||||
info!("DKG: Trying to advance the epoch");
|
||||
self.dkg_client.advance_epoch_state().await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run(mut self, mut shutdown: TaskClient) {
|
||||
let mut interval = interval(self.polling_rate);
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => self.handle_epoch_state().await,
|
||||
_ = shutdown.recv() => {
|
||||
trace!("DkgController: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: can we make it non-async? it seems we'd have to modify `coconut_keypair.set(coconut_keypair_value)` in new
|
||||
// could we do it?
|
||||
pub(crate) async fn start(
|
||||
config: &config::CoconutSigner,
|
||||
nyxd_client: nyxd::Client,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
rng: R,
|
||||
shutdown: &TaskManager,
|
||||
) -> Result<()>
|
||||
where
|
||||
R: Sync + Send + 'static,
|
||||
{
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
let dkg_controller = DkgController::new(config, nyxd_client, coconut_keypair, rng).await?;
|
||||
tokio::spawn(async move { dkg_controller.run(shutdown_listener).await });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::dealing::DealingGenerationError;
|
||||
use crate::coconut::dkg::key_derivation::KeyDerivationError;
|
||||
use crate::coconut::dkg::key_finalization::KeyFinalizationError;
|
||||
use crate::coconut::dkg::key_validation::KeyValidationError;
|
||||
use crate::coconut::dkg::public_key::PublicKeySubmissionError;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DkgError {
|
||||
#[error("failed to persist local state to disk at path {}: {source}", path.display())]
|
||||
StatePersistenceFailure {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: CoconutError,
|
||||
},
|
||||
|
||||
#[error("failed to query for the current DKG epoch state: {source}")]
|
||||
EpochQueryFailure {
|
||||
#[source]
|
||||
source: CoconutError,
|
||||
},
|
||||
|
||||
#[error("failed to query the CW4 group contract for the membership status: {source}")]
|
||||
GroupQueryFailure {
|
||||
#[source]
|
||||
source: CoconutError,
|
||||
},
|
||||
|
||||
#[error("this API is currently not member of the DKG group and thus can't participate in the process")]
|
||||
NotInGroup,
|
||||
|
||||
#[error("failed to submit public keys to the DKG contract: {source}")]
|
||||
PublicKeySubmissionFailure {
|
||||
#[source]
|
||||
source: PublicKeySubmissionError,
|
||||
},
|
||||
|
||||
#[error("failed to submit DKG dealings to the DKG contract: {source}")]
|
||||
DealingExchangeFailure {
|
||||
#[source]
|
||||
source: DealingGenerationError,
|
||||
},
|
||||
|
||||
#[error("failed to submit verification keys to the DKG contract: {source}")]
|
||||
VerificationKeySubmissionFailure {
|
||||
#[source]
|
||||
source: KeyDerivationError,
|
||||
},
|
||||
|
||||
#[error("failed to validate verification keys in the DKG contract: {source}")]
|
||||
VerificationKeyValidationFailure {
|
||||
#[source]
|
||||
source: KeyValidationError,
|
||||
},
|
||||
|
||||
#[error("failed to finalize verification keys in the DKG contract: {source}")]
|
||||
VerificationKeyFinalizationFailure {
|
||||
#[source]
|
||||
source: KeyFinalizationError,
|
||||
},
|
||||
|
||||
#[error("failed to advance the DKG state: {source}")]
|
||||
StateAdvancementFailure {
|
||||
#[source]
|
||||
source: CoconutError,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::client::Client;
|
||||
use crate::coconut::keys::KeyPairWithEpoch;
|
||||
use crate::support::{config, nyxd};
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use nym_coconut_dkg_common::types::{EpochId, EpochState};
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::path::Path;
|
||||
use thiserror::__private::AsDisplay;
|
||||
|
||||
pub(crate) fn init_bte_keypair<R: RngCore + CryptoRng>(
|
||||
rng: &mut R,
|
||||
config: &config::CoconutSigner,
|
||||
) -> anyhow::Result<()> {
|
||||
let dkg_params = nym_dkg::bte::setup();
|
||||
let kp = DkgKeyPair::new(&dkg_params, rng);
|
||||
nym_pemstore::store_keypair(
|
||||
&kp,
|
||||
&nym_pemstore::KeyPairPath::new(
|
||||
&config.storage_paths.decryption_key_path,
|
||||
&config.storage_paths.public_key_with_proof_path,
|
||||
),
|
||||
)
|
||||
.context("DKG BTE keypair store failure")
|
||||
}
|
||||
|
||||
pub(crate) fn load_bte_keypair(config: &config::CoconutSigner) -> anyhow::Result<DkgKeyPair> {
|
||||
nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new(
|
||||
&config.storage_paths.decryption_key_path,
|
||||
&config.storage_paths.public_key_with_proof_path,
|
||||
))
|
||||
.context("bte keypair load failure")
|
||||
}
|
||||
|
||||
pub(crate) fn load_coconut_keypair_if_exists(
|
||||
config: &config::CoconutSigner,
|
||||
) -> anyhow::Result<Option<KeyPairWithEpoch>> {
|
||||
if !config.storage_paths.coconut_key_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
nym_pemstore::load_key(&config.storage_paths.coconut_key_path)
|
||||
.context("coconut key load failure")
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
// the keys can be considered valid if they were generated for the current dkg epoch
|
||||
// and we're either in the "in progress" or "key finalization" states of the DKG
|
||||
pub(crate) async fn can_validate_coconut_keys(
|
||||
nyxd_client: &nyxd::Client,
|
||||
issued_for: EpochId,
|
||||
) -> anyhow::Result<bool> {
|
||||
// validate the keys if they were generated for the current dkg epoch
|
||||
// and we're either in the "in progress" or "key finalization" states of the DKG
|
||||
let current_dkg_epoch = nyxd_client.get_current_epoch().await?;
|
||||
if issued_for != current_dkg_epoch.epoch_id {
|
||||
warn!("managed to load coconut keys, but they were generated for epoch {issued_for}. The current epoch is {}. the keys won't be used for credential issuance", current_dkg_epoch.epoch_id);
|
||||
Ok(false)
|
||||
} else if !matches!(
|
||||
current_dkg_epoch.state,
|
||||
EpochState::InProgress | EpochState::VerificationKeyFinalization { .. }
|
||||
) {
|
||||
warn!("managed to load coconut keys, but the current DKG epoch is at {}. the keys won't (yet) be used for credential issuance", current_dkg_epoch.state);
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn persist_coconut_keypair<P: AsRef<Path>>(
|
||||
keys: &KeyPairWithEpoch,
|
||||
store_path: P,
|
||||
) -> anyhow::Result<()> {
|
||||
nym_pemstore::store_key(keys, store_path).context("coconut key store failure")
|
||||
}
|
||||
|
||||
pub(crate) fn archive_coconut_keypair<P: AsRef<Path>>(
|
||||
store_path: P,
|
||||
epoch_id: EpochId,
|
||||
) -> anyhow::Result<()> {
|
||||
let store_path = store_path.as_ref();
|
||||
if !store_path.exists() {
|
||||
bail!("coconut key does not exist at {}", store_path.as_display())
|
||||
}
|
||||
|
||||
let dir = store_path
|
||||
.parent()
|
||||
.ok_or(anyhow!("the coconut key does not have a valid parent"))?;
|
||||
let filename = store_path
|
||||
.file_name()
|
||||
.ok_or(anyhow!("the coconut key does not have a valid filename"))?
|
||||
.to_str()
|
||||
.ok_or(anyhow!("the coconut key filename is not valid UTF8"))?;
|
||||
let archive_path = dir.join(format!("epoch-{epoch_id}-{filename}.archived"));
|
||||
std::fs::rename(store_path, archive_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::client::DkgClient;
|
||||
use crate::coconut::dkg::controller::error::DkgError;
|
||||
use crate::coconut::dkg::state::{PersistentState, State};
|
||||
use crate::coconut::keys::KeyPair as CoconutKeyPair;
|
||||
use crate::nyxd;
|
||||
use crate::support::config;
|
||||
use anyhow::{bail, Result};
|
||||
use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState};
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use nym_task::{TaskClient, TaskManager};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::{CryptoRng, Rng, RngCore};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::time::interval;
|
||||
|
||||
mod error;
|
||||
pub(crate) mod keys;
|
||||
|
||||
pub(crate) struct DkgController<R = OsRng> {
|
||||
pub(crate) dkg_client: DkgClient,
|
||||
pub(crate) coconut_key_path: PathBuf,
|
||||
pub(crate) state: State,
|
||||
pub(super) rng: R,
|
||||
polling_rate: Duration,
|
||||
}
|
||||
|
||||
impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
|
||||
pub(crate) fn new(
|
||||
config: &config::CoconutSigner,
|
||||
nyxd_client: nyxd::Client,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
dkg_keypair: DkgKeyPair,
|
||||
identity_key: identity::PublicKey,
|
||||
rng: R,
|
||||
) -> Result<Self> {
|
||||
let Some(announce_address) = &config.announce_address else {
|
||||
bail!("can't start a DKG controller without specifying an announce address!")
|
||||
};
|
||||
|
||||
let persistent_state = PersistentState::load_from_file(
|
||||
&config.storage_paths.dkg_persistent_state_path,
|
||||
).unwrap_or_else(|err| {
|
||||
warn!("could not load an existing persistent state from the file. a fresh state will be used: {err}");
|
||||
Default::default()
|
||||
});
|
||||
|
||||
Ok(DkgController {
|
||||
dkg_client: DkgClient::new(nyxd_client),
|
||||
coconut_key_path: config.storage_paths.coconut_key_path.clone(),
|
||||
state: State::new(
|
||||
config.storage_paths.dkg_persistent_state_path.clone(),
|
||||
persistent_state,
|
||||
announce_address.clone(),
|
||||
dkg_keypair,
|
||||
identity_key,
|
||||
coconut_keypair,
|
||||
),
|
||||
rng,
|
||||
polling_rate: config.debug.dkg_contract_polling_rate,
|
||||
})
|
||||
}
|
||||
|
||||
fn persist_state(&self) -> Result<(), DkgError> {
|
||||
let persistent_state = PersistentState::from(&self.state);
|
||||
let save_path = self.state.persistent_state_path();
|
||||
persistent_state.save_to_file(save_path).map_err(|source| {
|
||||
DkgError::StatePersistenceFailure {
|
||||
path: save_path.to_path_buf(),
|
||||
source,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn current_epoch(&self) -> Result<Epoch, DkgError> {
|
||||
self.dkg_client
|
||||
.get_current_epoch()
|
||||
.await
|
||||
.map_err(|source| DkgError::EpochQueryFailure { source })
|
||||
}
|
||||
|
||||
async fn ensure_group_member(&self) -> Result<(), DkgError> {
|
||||
let membership_response = self
|
||||
.dkg_client
|
||||
.group_member()
|
||||
.await
|
||||
.map_err(|source| DkgError::GroupQueryFailure { source })?;
|
||||
|
||||
debug!("CW4 membership response: {membership_response:?}");
|
||||
|
||||
// make sure we are a voting member, i.e. have a non-zero weight
|
||||
if let Some(weight) = membership_response.weight {
|
||||
if weight == 0 {
|
||||
return Err(DkgError::NotInGroup);
|
||||
}
|
||||
} else {
|
||||
return Err(DkgError::NotInGroup);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_awaiting_initialisation(&mut self) -> Result<(), DkgError> {
|
||||
info!("DKG hasn't been initialised yet - nothing to do");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_key_submission(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), DkgError> {
|
||||
debug!("DKG: public key submission (resharing: {resharing})");
|
||||
self.public_key_submission(epoch_id, resharing)
|
||||
.await
|
||||
.map_err(|source| DkgError::PublicKeySubmissionFailure { source })?;
|
||||
self.persist_state()
|
||||
}
|
||||
|
||||
async fn handle_dealing_exchange(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), DkgError> {
|
||||
debug!("DKG: dealing exchange (resharing: {resharing})");
|
||||
self.dealing_exchange(epoch_id, resharing)
|
||||
.await
|
||||
.map_err(|source| DkgError::DealingExchangeFailure { source })?;
|
||||
self.persist_state()
|
||||
}
|
||||
|
||||
async fn handle_verification_key_submission(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), DkgError> {
|
||||
debug!("DKG: verification key submission (resharing: {resharing})");
|
||||
self.verification_key_submission(epoch_id, resharing)
|
||||
.await
|
||||
.map_err(|source| DkgError::VerificationKeySubmissionFailure { source })?;
|
||||
self.persist_state()
|
||||
}
|
||||
|
||||
async fn handle_verification_key_validation(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), DkgError> {
|
||||
debug!("DKG: verification key validation (resharing: {resharing})");
|
||||
|
||||
self.verification_key_validation(epoch_id)
|
||||
.await
|
||||
.map_err(|source| DkgError::VerificationKeyValidationFailure { source })?;
|
||||
self.persist_state()
|
||||
}
|
||||
|
||||
async fn handle_verification_key_finalization(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), DkgError> {
|
||||
debug!("DKG: verification key finalization (resharing: {resharing})");
|
||||
|
||||
self.verification_key_finalization(epoch_id)
|
||||
.await
|
||||
.map_err(|source| DkgError::VerificationKeyFinalizationFailure { source })?;
|
||||
self.persist_state()
|
||||
}
|
||||
|
||||
async fn handle_in_progress(&mut self, epoch_id: EpochId) -> Result<(), DkgError> {
|
||||
debug!("DKG: epoch in progress");
|
||||
|
||||
let Ok(state) = self.state.in_progress_state(epoch_id) else {
|
||||
// we probably just started up the api while the DKG has already finished and we're waiting for new round to join
|
||||
debug!("the DKG has finished without our participation");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if !state.entered {
|
||||
info!("this is the first time this node is in the in progress state - going to clear state from the PREVIOUS epoch...");
|
||||
// if we finished dkg for epoch 123, we no longer care about anything from epoch 122
|
||||
// (but keep track of data from 123 for the future reference)
|
||||
self.state.clear_previous_epoch(epoch_id);
|
||||
|
||||
// SAFETY: we just accessed this item in an immutable way, thus it MUST exist so the unwrap is fine
|
||||
self.state.in_progress_state_mut(epoch_id).unwrap().entered = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_advance_dkg_state(&mut self) -> Result<(), DkgError> {
|
||||
// We try advancing the epoch state, on a best-effort basis
|
||||
info!("DKG: Trying to advance the epoch");
|
||||
self.dkg_client
|
||||
.advance_epoch_state()
|
||||
.await
|
||||
.map_err(|source| DkgError::StateAdvancementFailure { source })
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_epoch_state(&mut self) -> Result<(), DkgError> {
|
||||
self.ensure_group_member().await?;
|
||||
|
||||
let epoch = self.current_epoch().await?;
|
||||
|
||||
match epoch.state {
|
||||
EpochState::WaitingInitialisation => self.handle_awaiting_initialisation().await?,
|
||||
EpochState::PublicKeySubmission { resharing } => {
|
||||
self.handle_key_submission(epoch.epoch_id, resharing)
|
||||
.await?
|
||||
}
|
||||
EpochState::DealingExchange { resharing } => {
|
||||
self.handle_dealing_exchange(epoch.epoch_id, resharing)
|
||||
.await?
|
||||
}
|
||||
EpochState::VerificationKeySubmission { resharing } => {
|
||||
self.handle_verification_key_submission(epoch.epoch_id, resharing)
|
||||
.await?
|
||||
}
|
||||
EpochState::VerificationKeyValidation { resharing } => {
|
||||
self.handle_verification_key_validation(epoch.epoch_id, resharing)
|
||||
.await?
|
||||
}
|
||||
EpochState::VerificationKeyFinalization { resharing } => {
|
||||
self.handle_verification_key_finalization(epoch.epoch_id, resharing)
|
||||
.await?
|
||||
}
|
||||
// Just wait, in case we need to redo dkg at some point
|
||||
EpochState::InProgress => self.handle_in_progress(epoch.epoch_id).await?,
|
||||
};
|
||||
|
||||
// add a bit of variance so that all apis wouldn't attempt to trigger it at the same time
|
||||
let variance = self.rng.gen_range(0..=60);
|
||||
if let Some(epoch_finish) = epoch.finish_timestamp {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
if now.unix_timestamp() > epoch_finish.seconds() as i64 + variance {
|
||||
// TODO: make sure to not overload validator in case its running slow
|
||||
// i.e. send it once at most every X seconds
|
||||
self.try_advance_dkg_state().await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn run(mut self, mut shutdown: TaskClient) {
|
||||
let mut interval = interval(self.polling_rate);
|
||||
|
||||
// sometimes when the process is running behind, the ticker resolves multiple times in quick succession
|
||||
// so explicitly track those instances and make sure we don't overload the validator with contract calls
|
||||
let mut last_polled = OffsetDateTime::now_utc();
|
||||
let mut last_tick_duration = Default::default();
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let tick_duration = now - last_polled;
|
||||
last_polled = now;
|
||||
|
||||
if tick_duration < self.polling_rate {
|
||||
warn!("it seems the process is running behind. The current tick rate is lower than the polling rate. rate: {:?}, current tick: {}, previous tick: {}", self.polling_rate, tick_duration, last_tick_duration);
|
||||
last_tick_duration = tick_duration;
|
||||
continue
|
||||
}
|
||||
last_tick_duration = tick_duration;
|
||||
|
||||
if let Err(err) = self.handle_epoch_state().await {
|
||||
error!("failed to update the DKG state: {err}")
|
||||
}
|
||||
}
|
||||
_ = shutdown.recv() => {
|
||||
trace!("DkgController: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start(
|
||||
config: &config::CoconutSigner,
|
||||
nyxd_client: nyxd::Client,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
dkg_bte_keypair: DkgKeyPair,
|
||||
identity_key: identity::PublicKey,
|
||||
rng: R,
|
||||
shutdown: &TaskManager,
|
||||
) -> Result<()>
|
||||
where
|
||||
R: Sync + Send + 'static,
|
||||
{
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
let dkg_controller = DkgController::new(
|
||||
config,
|
||||
nyxd_client,
|
||||
coconut_keypair,
|
||||
dkg_bte_keypair,
|
||||
identity_key,
|
||||
rng,
|
||||
)?;
|
||||
tokio::spawn(async move { dkg_controller.run(shutdown_listener).await });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl DkgController {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn default_test_mock(
|
||||
dkg_client: DkgClient,
|
||||
state: State,
|
||||
) -> DkgController<rand_chacha::ChaCha20Rng> {
|
||||
DkgController {
|
||||
dkg_client,
|
||||
coconut_key_path: Default::default(),
|
||||
state,
|
||||
rng: crate::coconut::tests::fixtures::test_rng([1u8; 32]),
|
||||
polling_rate: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn test_mock(
|
||||
rng: rand_chacha::ChaCha20Rng,
|
||||
dkg_client: DkgClient,
|
||||
state: State,
|
||||
coconut_key_path: PathBuf,
|
||||
) -> DkgController<rand_chacha::ChaCha20Rng> {
|
||||
DkgController {
|
||||
dkg_client,
|
||||
coconut_key_path,
|
||||
state,
|
||||
rng,
|
||||
polling_rate: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+636
-317
@@ -1,348 +1,667 @@
|
||||
// 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::{ConsistentState, State};
|
||||
use crate::coconut::dkg;
|
||||
use crate::coconut::dkg::controller::keys::archive_coconut_keypair;
|
||||
use crate::coconut::dkg::controller::DkgController;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::keys::KeyPairWithEpoch;
|
||||
use log::debug;
|
||||
use nym_coconut_dkg_common::types::TOTAL_DEALINGS;
|
||||
use nym_contracts_common::dealings::ContractSafeBytes;
|
||||
use nym_dkg::bte::setup;
|
||||
use nym_dkg::Dealing;
|
||||
use rand::RngCore;
|
||||
use std::collections::VecDeque;
|
||||
use zeroize::Zeroize;
|
||||
use nym_coconut_dkg_common::types::{
|
||||
ContractDealing, DealingIndex, EpochId, PartialContractDealing,
|
||||
};
|
||||
use nym_dkg::{Dealing, Scalar};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) async fn dealing_exchange(
|
||||
dkg_client: &DkgClient,
|
||||
state: &mut State,
|
||||
rng: impl RngCore + Clone,
|
||||
resharing: bool,
|
||||
) -> Result<(), CoconutError> {
|
||||
if state.receiver_index().is_some() {
|
||||
debug!("Receiver index was set previously, nothing to do");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let dealers = dkg_client.get_current_dealers().await?;
|
||||
let threshold = dkg_client.get_current_epoch_threshold().await?;
|
||||
let initial_dealers = dkg_client
|
||||
.get_initial_dealers()
|
||||
.await?
|
||||
.map(|d| d.initial_dealers)
|
||||
.unwrap_or_default();
|
||||
let own_address = dkg_client.get_address().await.as_ref().to_string();
|
||||
state.set_dealers(dealers);
|
||||
state.set_threshold(threshold);
|
||||
let receivers = state.current_dealers_by_idx();
|
||||
let dealer_index = state.node_index_value()?;
|
||||
let receiver_index = receivers
|
||||
.keys()
|
||||
.position(|node_index| *node_index == dealer_index);
|
||||
|
||||
let prior_resharing_secrets = if let Some(mut keypair) = state.take_coconut_keypair().await {
|
||||
// Double check that we are in resharing mode
|
||||
if resharing {
|
||||
let sk = keypair.secret_key();
|
||||
if sk.size() + 1 != TOTAL_DEALINGS {
|
||||
return Err(CoconutError::CorruptedCoconutKeyPair);
|
||||
}
|
||||
|
||||
let (x, mut scalars) = sk.into_raw();
|
||||
|
||||
// We can now erase the keypair from memory
|
||||
debug!("Removing coconut keypair from memory");
|
||||
keypair.zeroize();
|
||||
scalars.push(x);
|
||||
scalars
|
||||
} else {
|
||||
log::warn!("Coconut key hasn't been reset in memory. The state might be corrupt");
|
||||
vec![]
|
||||
}
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let mut prior_resharing_secrets = VecDeque::from(prior_resharing_secrets);
|
||||
if !resharing || initial_dealers.iter().any(|d| *d == own_address) {
|
||||
let params = setup();
|
||||
for _ in 0..TOTAL_DEALINGS {
|
||||
debug!(
|
||||
"Submitting dealing for indexes {:?} with resharing: {}",
|
||||
receivers.keys().collect::<Vec<_>>(),
|
||||
prior_resharing_secrets.front().is_some()
|
||||
);
|
||||
let (dealing, _) = Dealing::create(
|
||||
rng.clone(),
|
||||
¶ms,
|
||||
dealer_index,
|
||||
state.threshold()?,
|
||||
&receivers,
|
||||
prior_resharing_secrets.pop_front(),
|
||||
);
|
||||
dkg_client
|
||||
.submit_dealing(ContractSafeBytes::from(&dealing), resharing)
|
||||
.await?;
|
||||
}
|
||||
} else {
|
||||
debug!("Nothing to do, waiting for initial dealers to submit dealings");
|
||||
}
|
||||
|
||||
info!("DKG: Finished dealing exchange");
|
||||
state.set_receiver_index(receiver_index);
|
||||
|
||||
Ok(())
|
||||
enum DealingGeneration {
|
||||
Fresh { number: u32 },
|
||||
Resharing { prior_secrets: Vec<Scalar> },
|
||||
}
|
||||
|
||||
impl Debug for DealingGeneration {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
DealingGeneration::Fresh { number } => f
|
||||
.debug_struct("DealingGeneration::Fresh")
|
||||
.field("number", number)
|
||||
.finish(),
|
||||
DealingGeneration::Resharing { prior_secrets } => f
|
||||
.debug_struct("DealingGeneration::Resharing")
|
||||
.field("number", &prior_secrets.len())
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DealingGenerationError {
|
||||
#[error(transparent)]
|
||||
CoconutError(#[from] CoconutError),
|
||||
|
||||
#[error("can't complete dealing exchange without registering public keys")]
|
||||
IncompletePublicKeyRegistration,
|
||||
|
||||
#[error("contract state failure - the DKG threshold is unavailable even though dealing exchange has been initiated")]
|
||||
UnavailableContractThreshold,
|
||||
|
||||
#[error("could not establish receiver index for epoch {epoch_id} even though we're a dealer!")]
|
||||
UnavailableReceiverIndex { epoch_id: EpochId },
|
||||
|
||||
#[error("failed to archive coconut key for epoch {epoch_id} using path {}: {source}", path.display())]
|
||||
KeyArchiveFailure {
|
||||
epoch_id: EpochId,
|
||||
path: PathBuf,
|
||||
|
||||
// I hate that we're using anyhow error source here, but changing that would require bigger refactoring
|
||||
#[source]
|
||||
source: anyhow::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl<R: RngCore + CryptoRng> DkgController<R> {
|
||||
async fn generate_dealings(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
spec: DealingGeneration,
|
||||
) -> Result<HashMap<DealingIndex, Dealing>, DealingGenerationError> {
|
||||
let threshold = self.dkg_client.get_current_epoch_threshold().await?.ok_or(
|
||||
CoconutError::UnrecoverableState {
|
||||
reason: String::from("Threshold should have been set"),
|
||||
},
|
||||
)?;
|
||||
|
||||
let dealer_index = self
|
||||
.state
|
||||
.registration_state(epoch_id)?
|
||||
.assigned_index
|
||||
.ok_or(CoconutError::UnrecoverableState {
|
||||
reason: String::from("Node index should have been set"),
|
||||
})?;
|
||||
|
||||
// in our case every dealer is also a receiver
|
||||
|
||||
// ASSUMPTION: all dealers see the same contract data, i.e. if one fails to decode and verify the receiver's key,
|
||||
// all of them will
|
||||
let filtered_receivers = self.state.valid_epoch_receivers_keys(epoch_id)?;
|
||||
|
||||
let dbg_receivers = filtered_receivers.keys().collect::<Vec<_>>();
|
||||
debug!("generating dealings with threshold {threshold} for receivers: {dbg_receivers:?} with the following spec: {spec:?}. Our index is {dealer_index}");
|
||||
|
||||
let mut dealings = HashMap::new();
|
||||
match spec {
|
||||
DealingGeneration::Fresh { number } => {
|
||||
for i in 0..number {
|
||||
let dealing = Dealing::create(
|
||||
&mut self.rng,
|
||||
dkg::params(),
|
||||
dealer_index,
|
||||
threshold,
|
||||
&filtered_receivers,
|
||||
None,
|
||||
);
|
||||
dealings.insert(i as DealingIndex, dealing.0);
|
||||
}
|
||||
}
|
||||
DealingGeneration::Resharing { prior_secrets } => {
|
||||
for (i, secret) in prior_secrets.into_iter().enumerate() {
|
||||
let dealing = Dealing::create(
|
||||
&mut self.rng,
|
||||
dkg::params(),
|
||||
dealer_index,
|
||||
threshold,
|
||||
&filtered_receivers,
|
||||
Some(secret),
|
||||
);
|
||||
dealings.insert(i as DealingIndex, dealing.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update the state with the dealing information
|
||||
self.state
|
||||
.dealing_exchange_state_mut(epoch_id)?
|
||||
.generated_dealings = dealings.clone();
|
||||
|
||||
Ok(dealings)
|
||||
}
|
||||
|
||||
async fn generate_fresh_dealings(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
number: u32,
|
||||
) -> Result<HashMap<DealingIndex, Dealing>, DealingGenerationError> {
|
||||
self.generate_dealings(epoch_id, DealingGeneration::Fresh { number })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn generate_resharing_dealings(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
prior_secrets: Vec<Scalar>,
|
||||
) -> Result<HashMap<DealingIndex, Dealing>, DealingGenerationError> {
|
||||
self.generate_dealings(epoch_id, DealingGeneration::Resharing { prior_secrets })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn resubmit_pregenerated_dealings(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), DealingGenerationError> {
|
||||
let dealing_state = self.state.dealing_exchange_state(epoch_id)?;
|
||||
|
||||
for (dealing_index, dealing) in &dealing_state.generated_dealings {
|
||||
// check which dealing is actually present on the chain (some might have gotten stuck in the mempool for quite a while)
|
||||
let dealing_submitted = self
|
||||
.dkg_client
|
||||
.get_dealing_status(epoch_id, *dealing_index)
|
||||
.await?;
|
||||
|
||||
if dealing_submitted {
|
||||
warn!("we have already submitted dealing {dealing_index} before - we probably crashed or the chain timed out!");
|
||||
continue;
|
||||
}
|
||||
warn!(
|
||||
"we have already generated dealing {dealing_index} before, but failed to submit it"
|
||||
);
|
||||
let contract_dealing =
|
||||
PartialContractDealing::new(*dealing_index, ContractDealing::from(dealing));
|
||||
|
||||
self.dkg_client
|
||||
.submit_dealing(contract_dealing, resharing)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check whether this dealer can participate in the resharing
|
||||
/// by looking into the contract and ensuring it's in the list of initial dealers for this epoch
|
||||
async fn can_reshare(&self) -> Result<bool, DealingGenerationError> {
|
||||
let Some(initial_data) = self.dkg_client.get_initial_dealers().await? else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let address = self.dkg_client.get_address().await;
|
||||
Ok(initial_data
|
||||
.initial_dealers
|
||||
.iter()
|
||||
.any(|d| d.as_str() == address.as_ref()))
|
||||
}
|
||||
|
||||
/// Deal with the dealing generation case where the system requests resharing
|
||||
/// and this node contains an already derived coconut keypair from some previous epoch.
|
||||
async fn handle_resharing_with_prior_key(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
expected_key_size: u32,
|
||||
old_keypair: KeyPairWithEpoch,
|
||||
) -> Result<(), DealingGenerationError> {
|
||||
// make sure we're allowed to participate in resharing
|
||||
if !self.can_reshare().await? {
|
||||
// we have to wait for other dealers to give us the dealings (hopefully)
|
||||
warn!("we we have an existing coconut keypair, but we're not allowed to participate in resharing");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// EDGE CASE:
|
||||
// make sure our keypair is from strictly the previous epoch
|
||||
// because our node might have been offline for multiple epochs and while we do have a coconut keypair,
|
||||
// it could be outdated and we can't use it for resharing
|
||||
let previous = epoch_id.saturating_sub(1);
|
||||
if old_keypair.issued_for_epoch != previous {
|
||||
warn!("our existing coconut keypair has been generated for an distant epoch ({} vs expected {previous} for resharing)", old_keypair.issued_for_epoch);
|
||||
// don't participate in resharing
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// EDGE CASE:
|
||||
// we have changed the key size (because we wanted to add new attribute to credentials)
|
||||
// in this instance we can't reuse our key and have to generate brand new dealings
|
||||
if expected_key_size != 1 + old_keypair.keys.secret_key().size() as u32 {
|
||||
warn!("our existing coconut keypair has different size than the currently expected value ({expected_key_size} vs {})", old_keypair.keys.secret_key().size() as u32);
|
||||
self.generate_fresh_dealings(epoch_id, expected_key_size)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// generate resharing dealings
|
||||
let prior_secrets = old_keypair.hazmat_into_secrets();
|
||||
// safety:
|
||||
// the prior secrets will be immediately converted into `Polynomial` with the specified coefficient
|
||||
// that does implement `ZeroizeOnDrop`
|
||||
self.generate_resharing_dealings(epoch_id, prior_secrets)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Second step of the DKG process during which the nym api will generate appropriate [Dealing] for
|
||||
/// other parties as indicated by public key registration from the previous step.
|
||||
///
|
||||
/// Before submitting any dealings to the system, the node will persist them locally so that if any failure
|
||||
/// occurs, it will be possible to recover.
|
||||
pub(crate) async fn dealing_exchange(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), DealingGenerationError> {
|
||||
let dealing_state = self.state.dealing_exchange_state(epoch_id)?;
|
||||
|
||||
// check if we have already submitted the dealings
|
||||
if dealing_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 all the dealings for this epoch");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !self.state.registration_state(epoch_id)?.completed() {
|
||||
return Err(DealingGenerationError::IncompletePublicKeyRegistration);
|
||||
}
|
||||
|
||||
// FAILURE CASE:
|
||||
// check if we have already generated the dealings, but they failed to get sent to the contract for whatever reason
|
||||
if !dealing_state.generated_dealings.is_empty() {
|
||||
debug!("we have already generated the dealings for this epoch");
|
||||
self.resubmit_pregenerated_dealings(epoch_id, resharing)
|
||||
.await?;
|
||||
|
||||
// if we managed to resubmit the dealings (i.e. we didn't return an error),
|
||||
// it means the state is complete now.
|
||||
info!("DKG: resubmitted previously generated dealings - finished dealing exchange");
|
||||
self.state.dealing_exchange_state_mut(epoch_id)?.completed = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// we don't have any prior information - grab, parse and cache it since we will need it in next steps
|
||||
// and it's not going to change during the epoch
|
||||
let dealers = self.dkg_client.get_current_dealers().await?;
|
||||
|
||||
// EDGE CASE:
|
||||
// if there are no receivers(dealers) in this epoch for some reason,
|
||||
// don't attempt to generate dealings as this will fail with a panic
|
||||
if dealers.is_empty() {
|
||||
warn!("there are no active dealers/receivers to generate dealings for");
|
||||
self.state.dealing_exchange_state_mut(epoch_id)?.completed = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.state.dkg_state_mut(epoch_id)?.set_dealers(dealers);
|
||||
|
||||
// obtain our dealer index to correctly set receiver index (used for share decryption)
|
||||
let dealer_index = self.state.assigned_index(epoch_id)?;
|
||||
|
||||
// update internally used threshold value which should have been available after all dealers registered
|
||||
let Some(threshold) = self.dkg_client.get_current_epoch_threshold().await? else {
|
||||
// if we're in the dealing exchange phase, the threshold must have been already established
|
||||
return Err(DealingGenerationError::UnavailableContractThreshold);
|
||||
};
|
||||
self.state
|
||||
.key_derivation_state_mut(epoch_id)?
|
||||
.expected_threshold = Some(threshold);
|
||||
|
||||
// establish our receiver index
|
||||
let sorted_dealers = &self.state.dkg_state(epoch_id)?.dealing_exchange.dealers;
|
||||
let Some(receiver_index) = sorted_dealers.keys().position(|idx| idx == &dealer_index)
|
||||
else {
|
||||
// this branch should be impossible as `dealing_exchange` should never be called unless we're actually a dealer
|
||||
error!("could not establish receiver index for epoch {epoch_id} even though we're a dealer!");
|
||||
return Err(DealingGenerationError::UnavailableReceiverIndex { epoch_id });
|
||||
};
|
||||
self.state
|
||||
.dealing_exchange_state_mut(epoch_id)?
|
||||
.receiver_index = Some(receiver_index);
|
||||
|
||||
// get the expected key size which will determine the number of dealings we need to construct
|
||||
let contract_state = self.dkg_client.get_contract_state().await?;
|
||||
let expected_key_size = contract_state.key_size;
|
||||
|
||||
// there are few cases to cover here based on the resharing status and presence of coconut keypair:
|
||||
// - resharing + we have a key => we should use the prior secrets for the resharing dealings generation
|
||||
// - resharing + we don't have a key => we are a new party that joined the existing setup and we have to wait for others to give us the shares
|
||||
// - no resharing + we have a key => whole DKG has been restarted (probably enough new parties joined / old parties left) to trigger it
|
||||
// - no resharing + we don't have a key => either as above (but we're a new party) or it's the very first instance of the DKG
|
||||
if let Some(old_keypair) = self.state.take_coconut_keypair().await {
|
||||
let keypair_epoch = old_keypair.issued_for_epoch;
|
||||
|
||||
if resharing {
|
||||
debug!("resharing + prior key");
|
||||
self.handle_resharing_with_prior_key(epoch_id, expected_key_size, old_keypair)
|
||||
.await?;
|
||||
} else {
|
||||
debug!("no resharing + prior key");
|
||||
self.generate_fresh_dealings(epoch_id, expected_key_size)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// EDGE CASE:
|
||||
// make sure to persist the state after possibly generating the resharing dealings as we're going to be archiving the keypair
|
||||
// (so we won't be able to create resharing dealings again if we crashed since we won't be able to load the keys)
|
||||
self.state.persist()?;
|
||||
// archive the keypair
|
||||
if let Err(source) = archive_coconut_keypair(&self.coconut_key_path, keypair_epoch) {
|
||||
return Err(DealingGenerationError::KeyArchiveFailure {
|
||||
epoch_id,
|
||||
path: self.coconut_key_path.clone(),
|
||||
source,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// sure, the if statements could be collapsed, but i prefer to explicitly repeat the block for readability
|
||||
if resharing {
|
||||
debug!("resharing + no prior key -> nothing to do");
|
||||
if self.can_reshare().await? {
|
||||
warn!("this dealer was expected to participate in resharing but it doesn't have any prior keys to use");
|
||||
}
|
||||
} else {
|
||||
debug!("no resharing + no prior key");
|
||||
self.generate_fresh_dealings(epoch_id, expected_key_size)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
let dealings = &self
|
||||
.state
|
||||
.dealing_exchange_state(epoch_id)?
|
||||
.generated_dealings;
|
||||
let total = dealings.len();
|
||||
|
||||
// if we have generated any dealings persist the state in case we crash so that we would still have the data on hand
|
||||
// for resubmission upon getting back up
|
||||
if total > 0 {
|
||||
self.state.persist()?;
|
||||
}
|
||||
|
||||
for (i, (dealing_index, dealing)) in dealings.iter().enumerate() {
|
||||
let i = i + 1;
|
||||
debug!("submitting dealing index {dealing_index} ({i}/{total})");
|
||||
|
||||
let contract_dealing =
|
||||
PartialContractDealing::new(*dealing_index, ContractDealing::from(dealing));
|
||||
|
||||
self.dkg_client
|
||||
.submit_dealing(contract_dealing, resharing)
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.state.dealing_exchange_state_mut(epoch_id)?.completed = true;
|
||||
info!("DKG: Finished dealing exchange");
|
||||
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::complaints::ComplaintReason;
|
||||
use crate::coconut::dkg::state::PersistentState;
|
||||
use crate::coconut::tests::DummyClient;
|
||||
use crate::coconut::KeyPair;
|
||||
use cosmwasm_std::Addr;
|
||||
use crate::coconut::dkg::state::registration::KeyRejectionReason;
|
||||
use crate::coconut::keys::KeyPair;
|
||||
use crate::coconut::tests::fixtures::{
|
||||
dealers_fixtures, test_rng, TestingDkgControllerBuilder,
|
||||
};
|
||||
use crate::coconut::tests::helpers::unchecked_decode_bte_key;
|
||||
use nym_coconut::{ttp_keygen, Parameters};
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::types::InitialReplacementData;
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use nym_dkg::bte::{Params, PublicKeyWithProof};
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use rand::rngs::OsRng;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use url::Url;
|
||||
use nym_dkg::bte::PublicKeyWithProof;
|
||||
|
||||
const TEST_VALIDATORS_ADDRESS: [&str; 4] = [
|
||||
"n1aq9kakfgwqcufr23lsv644apavcntrsqsk4yus",
|
||||
"n1s9l3xr4g0rglvk4yctktmck3h4eq0gp6z2e20v",
|
||||
"n19kl4py32vsk297dm93ezem992cdyzdy4zuc2x6",
|
||||
"n1jfrs6cmw9t7dv0x8cgny6geunzjh56n2s89fkv",
|
||||
];
|
||||
#[tokio::test]
|
||||
async fn exchange_dealing() -> anyhow::Result<()> {
|
||||
let mut rng = test_rng([69u8; 32]);
|
||||
let dealers = dealers_fixtures(&mut rng, 4);
|
||||
let self_dealer = dealers[0].clone();
|
||||
|
||||
fn insert_dealers(
|
||||
params: &Params,
|
||||
dealer_details_db: &Arc<RwLock<HashMap<String, (DealerDetails, bool)>>>,
|
||||
) -> Vec<DkgKeyPair> {
|
||||
let mut keypairs = vec![];
|
||||
for (idx, addr) in TEST_VALIDATORS_ADDRESS.iter().enumerate() {
|
||||
let keypair = DkgKeyPair::new(params, OsRng);
|
||||
let bte_public_key_with_proof =
|
||||
bs58::encode(&keypair.public_key().to_bytes()).into_string();
|
||||
keypairs.push(keypair);
|
||||
dealer_details_db.write().unwrap().insert(
|
||||
addr.to_string(),
|
||||
let mut controller = TestingDkgControllerBuilder::default()
|
||||
.with_threshold(2)
|
||||
.with_dealers(dealers.clone())
|
||||
.with_as_dealer(self_dealer.clone())
|
||||
.build()
|
||||
.await;
|
||||
|
||||
let epoch = controller.dkg_client.get_current_epoch().await?.epoch_id;
|
||||
let key_size = controller.dkg_client.get_contract_state().await?.key_size;
|
||||
|
||||
// initial state
|
||||
assert!(controller
|
||||
.state
|
||||
.dealing_exchange_state(epoch)?
|
||||
.dealers
|
||||
.is_empty());
|
||||
assert!(controller
|
||||
.state
|
||||
.dealing_exchange_state(epoch)?
|
||||
.generated_dealings
|
||||
.is_empty());
|
||||
|
||||
// exchange
|
||||
let res = controller.dealing_exchange(epoch, false).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
let expected_dealers = dealers
|
||||
.iter()
|
||||
.map(|d| {
|
||||
(
|
||||
DealerDetails {
|
||||
address: Addr::unchecked(*addr),
|
||||
bte_public_key_with_proof,
|
||||
announce_address: format!("localhost:80{}", idx),
|
||||
assigned_index: (idx + 1) as u64,
|
||||
},
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
keypairs
|
||||
}
|
||||
d.assigned_index,
|
||||
unchecked_decode_bte_key(&d.bte_public_key_with_proof),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let dealers = controller
|
||||
.state
|
||||
.dealing_exchange_state(epoch)?
|
||||
.dealers
|
||||
.values()
|
||||
.map(|p| (p.assigned_index, p.unwrap_key()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn exchange_dealing() {
|
||||
let self_index = 2;
|
||||
let dealer_details_db = Arc::new(RwLock::new(HashMap::new()));
|
||||
let dealings_db = Arc::new(RwLock::new(HashMap::new()));
|
||||
let threshold_db = Arc::new(RwLock::new(Some(2)));
|
||||
let dkg_client = DkgClient::new(
|
||||
DummyClient::new(AccountId::from_str(TEST_VALIDATORS_ADDRESS[0]).unwrap())
|
||||
.with_dealer_details(&dealer_details_db)
|
||||
.with_dealings(&dealings_db)
|
||||
.with_threshold(&threshold_db),
|
||||
);
|
||||
let params = setup();
|
||||
let mut state = State::new(
|
||||
PathBuf::default(),
|
||||
PersistentState::default(),
|
||||
Url::parse("localhost:8000").unwrap(),
|
||||
DkgKeyPair::new(¶ms, OsRng),
|
||||
KeyPair::new(),
|
||||
);
|
||||
state.set_node_index(Some(self_index));
|
||||
let keypairs = insert_dealers(¶ms, &dealer_details_db);
|
||||
|
||||
dealing_exchange(&dkg_client, &mut state, OsRng, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
state.current_dealers_by_idx().values().collect::<Vec<_>>(),
|
||||
keypairs
|
||||
.iter()
|
||||
.map(|k| k.public_key().public_key())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(state.threshold().unwrap(), 2);
|
||||
assert_eq!(state.receiver_index().unwrap(), 1);
|
||||
let dealings = dealings_db
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(TEST_VALIDATORS_ADDRESS[0])
|
||||
.unwrap()
|
||||
let generated_dealings = controller
|
||||
.state
|
||||
.dealing_exchange_state(epoch)?
|
||||
.generated_dealings
|
||||
.clone();
|
||||
assert_eq!(dealings.len(), TOTAL_DEALINGS);
|
||||
|
||||
dealing_exchange(&dkg_client, &mut state, OsRng, false)
|
||||
.await
|
||||
assert_eq!(expected_dealers, dealers);
|
||||
assert_eq!(key_size as usize, generated_dealings.len());
|
||||
|
||||
// also make sure the fake chain state contains our dealings (since we submitted them)
|
||||
let chain_state = controller.chain_state.lock().unwrap();
|
||||
|
||||
let submitted_dealings = chain_state
|
||||
.dkg_contract
|
||||
.dealings
|
||||
.get(&epoch)
|
||||
.unwrap()
|
||||
.get(self_dealer.address.as_str())
|
||||
.unwrap();
|
||||
let new_dealings = dealings_db
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(TEST_VALIDATORS_ADDRESS[0])
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert_eq!(dealings, new_dealings);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn invalid_bte_proof_dealing_posted() {
|
||||
let self_index = 2;
|
||||
let dealer_details_db = Arc::new(RwLock::new(HashMap::new()));
|
||||
let dealings_db = Arc::new(RwLock::new(HashMap::new()));
|
||||
let threshold_db = Arc::new(RwLock::new(Some(2)));
|
||||
let dkg_client = DkgClient::new(
|
||||
DummyClient::new(AccountId::from_str(TEST_VALIDATORS_ADDRESS[0]).unwrap())
|
||||
.with_dealer_details(&dealer_details_db)
|
||||
.with_dealings(&dealings_db)
|
||||
.with_threshold(&threshold_db),
|
||||
);
|
||||
let params = setup();
|
||||
let mut state = State::new(
|
||||
PathBuf::default(),
|
||||
PersistentState::default(),
|
||||
Url::parse("localhost:8000").unwrap(),
|
||||
DkgKeyPair::new(¶ms, OsRng),
|
||||
KeyPair::new(),
|
||||
);
|
||||
state.set_node_index(Some(self_index));
|
||||
insert_dealers(¶ms, &dealer_details_db);
|
||||
|
||||
dealer_details_db
|
||||
.write()
|
||||
.unwrap()
|
||||
.entry(TEST_VALIDATORS_ADDRESS[1].to_string())
|
||||
.and_modify(|details| {
|
||||
let mut bytes = bs58::decode(details.0.bte_public_key_with_proof.clone())
|
||||
.into_vec()
|
||||
.unwrap();
|
||||
// Find another value for last byte that still deserializes to a public key with proof
|
||||
let initial_byte = *bytes.last_mut().unwrap();
|
||||
loop {
|
||||
let last_byte = bytes.last_mut().unwrap();
|
||||
let (ret, _) = last_byte.overflowing_add(1);
|
||||
*last_byte = ret;
|
||||
// stop when we find that value, or if we do a full round trip of u8 values
|
||||
// and can't find one, in which case this test is invalid
|
||||
if PublicKeyWithProof::try_from_bytes(&bytes).is_ok() || ret == initial_byte {
|
||||
break;
|
||||
}
|
||||
}
|
||||
details.0.bte_public_key_with_proof = bs58::encode(&bytes).into_string();
|
||||
});
|
||||
|
||||
dealing_exchange(&dkg_client, &mut state, OsRng, false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
*state
|
||||
.all_dealers()
|
||||
.get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[1]))
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap_err(),
|
||||
ComplaintReason::InvalidBTEPublicKey
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn resharing_exchange_dealing() {
|
||||
let self_index = 2;
|
||||
let dealer_details_db = Arc::new(RwLock::new(HashMap::new()));
|
||||
let dealings_db = Arc::new(RwLock::new(HashMap::new()));
|
||||
let threshold_db = Arc::new(RwLock::new(Some(3)));
|
||||
let initial_dealers_db = Arc::new(RwLock::new(Some(InitialReplacementData {
|
||||
initial_dealers: vec![Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])],
|
||||
initial_height: 100,
|
||||
})));
|
||||
let dkg_client = DkgClient::new(
|
||||
DummyClient::new(
|
||||
AccountId::from_str("n1vxkywf9g4cg0k2dehanzwzz64jw782qm0kuynf").unwrap(),
|
||||
for submitted_dealing in submitted_dealings {
|
||||
let dealing = Dealing::try_from_bytes(submitted_dealing.data.as_slice())?;
|
||||
assert_eq!(
|
||||
generated_dealings.get(&submitted_dealing.index).unwrap(),
|
||||
&dealing
|
||||
)
|
||||
.with_dealer_details(&dealer_details_db)
|
||||
.with_dealings(&dealings_db)
|
||||
.with_threshold(&threshold_db)
|
||||
.with_initial_dealers_db(&initial_dealers_db),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_bte_proof_dealing_posted() -> anyhow::Result<()> {
|
||||
let mut rng = test_rng([69u8; 32]);
|
||||
let mut dealers = dealers_fixtures(&mut rng, 4);
|
||||
let self_dealer = dealers[0].clone();
|
||||
|
||||
// malform key of one of the dealers, but in such a way that it still deserializes correctly
|
||||
let bad_dealer_addr = dealers[1].address.clone();
|
||||
let mut bytes = bs58::decode(&dealers[1].bte_public_key_with_proof).into_vec()?;
|
||||
let initial_byte = *bytes.last_mut().unwrap();
|
||||
loop {
|
||||
let last_byte = bytes.last_mut().unwrap();
|
||||
let (ret, _) = last_byte.overflowing_add(1);
|
||||
*last_byte = ret;
|
||||
// stop when we find that value, or if we do a full round trip of u8 values
|
||||
// and can't find one, in which case this test is invalid
|
||||
if PublicKeyWithProof::try_from_bytes(&bytes).is_ok() {
|
||||
break;
|
||||
}
|
||||
if ret == initial_byte {
|
||||
panic!("did not find a valid byte")
|
||||
}
|
||||
}
|
||||
dealers[1].bte_public_key_with_proof = bs58::encode(&bytes).into_string();
|
||||
|
||||
let mut controller = TestingDkgControllerBuilder::default()
|
||||
.with_threshold(2)
|
||||
.with_dealers(dealers.clone())
|
||||
.with_as_dealer(self_dealer.clone())
|
||||
.build()
|
||||
.await;
|
||||
|
||||
let epoch = controller.dkg_client.get_current_epoch().await?.epoch_id;
|
||||
|
||||
// exchange
|
||||
let res = controller.dealing_exchange(epoch, false).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
let bad_dealer = controller
|
||||
.state
|
||||
.dealing_exchange_state(epoch)?
|
||||
.dealers
|
||||
.values()
|
||||
.find(|d| d.address == bad_dealer_addr)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
KeyRejectionReason::InvalidBTEPublicKey,
|
||||
bad_dealer.unwrap_rejection()
|
||||
);
|
||||
let params = setup();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resharing_outside_initial_set() -> anyhow::Result<()> {
|
||||
let mut rng = test_rng([69u8; 32]);
|
||||
let dealers = dealers_fixtures(&mut rng, 4);
|
||||
let self_dealer = dealers[0].clone();
|
||||
|
||||
let epoch = 0;
|
||||
|
||||
let mut keys = ttp_keygen(&Parameters::new(4).unwrap(), 3, 4).unwrap();
|
||||
let coconut_keypair = KeyPair::new();
|
||||
coconut_keypair.set(Some(keys.pop().unwrap())).await;
|
||||
coconut_keypair
|
||||
.set(KeyPairWithEpoch::new(keys.pop().unwrap(), epoch))
|
||||
.await;
|
||||
|
||||
let mut state = State::new(
|
||||
PathBuf::default(),
|
||||
PersistentState::default(),
|
||||
Url::parse("localhost:8000").unwrap(),
|
||||
DkgKeyPair::new(¶ms, OsRng),
|
||||
coconut_keypair.clone(),
|
||||
);
|
||||
state.set_node_index(Some(self_index));
|
||||
let keypairs = insert_dealers(¶ms, &dealer_details_db);
|
||||
let mut controller = TestingDkgControllerBuilder::default()
|
||||
.with_threshold(3)
|
||||
.with_dealers(dealers.clone())
|
||||
.with_as_dealer(self_dealer.clone())
|
||||
.with_keypair(coconut_keypair)
|
||||
.with_initial_epoch_id(epoch)
|
||||
.build()
|
||||
.await;
|
||||
|
||||
dealing_exchange(&dkg_client, &mut state, OsRng, true)
|
||||
.await
|
||||
.unwrap();
|
||||
let res = controller.dealing_exchange(epoch, true).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
assert_eq!(
|
||||
state.current_dealers_by_idx().values().collect::<Vec<_>>(),
|
||||
keypairs
|
||||
.iter()
|
||||
.map(|k| k.public_key().public_key())
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(state.threshold().unwrap(), 3);
|
||||
assert_eq!(state.receiver_index().unwrap(), 1);
|
||||
let addr = dkg_client.get_address().await;
|
||||
assert!(dealings_db.read().unwrap().get(addr.as_ref()).is_none());
|
||||
let expected_dealers = dealers
|
||||
.iter()
|
||||
.map(|d| {
|
||||
(
|
||||
d.assigned_index,
|
||||
unchecked_decode_bte_key(&d.bte_public_key_with_proof),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let dealers = controller
|
||||
.state
|
||||
.dealing_exchange_state(epoch)?
|
||||
.dealers
|
||||
.values()
|
||||
.map(|p| (p.assigned_index, p.unwrap_key()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut state = State::new(
|
||||
PathBuf::default(),
|
||||
PersistentState::default(),
|
||||
Url::parse("localhost:8000").unwrap(),
|
||||
DkgKeyPair::new(¶ms, OsRng),
|
||||
coconut_keypair,
|
||||
);
|
||||
state.set_node_index(Some(self_index));
|
||||
// Use a client that is in the initial dealers set
|
||||
let dkg_client = DkgClient::new(
|
||||
DummyClient::new(AccountId::from_str(TEST_VALIDATORS_ADDRESS[0]).unwrap())
|
||||
.with_dealer_details(&dealer_details_db)
|
||||
.with_dealings(&dealings_db)
|
||||
.with_threshold(&threshold_db)
|
||||
.with_initial_dealers_db(&initial_dealers_db),
|
||||
);
|
||||
|
||||
dealing_exchange(&dkg_client, &mut state, OsRng, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let dealings = dealings_db
|
||||
.read()
|
||||
.unwrap()
|
||||
.get(TEST_VALIDATORS_ADDRESS[0])
|
||||
.unwrap()
|
||||
let generated_dealings = controller
|
||||
.state
|
||||
.dealing_exchange_state(epoch)?
|
||||
.generated_dealings
|
||||
.clone();
|
||||
assert_eq!(dealings.len(), TOTAL_DEALINGS);
|
||||
|
||||
assert_eq!(expected_dealers, dealers);
|
||||
|
||||
// no dealings submitted for the epoch, because we're not an initial dealer
|
||||
assert!(generated_dealings.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resharing_inside_initial_set() -> anyhow::Result<()> {
|
||||
let mut rng = test_rng([69u8; 32]);
|
||||
let dealers = dealers_fixtures(&mut rng, 4);
|
||||
let self_dealer = dealers[0].clone();
|
||||
|
||||
let epoch = 0;
|
||||
|
||||
let mut keys = ttp_keygen(&Parameters::new(4).unwrap(), 3, 4).unwrap();
|
||||
let coconut_keypair = KeyPair::new();
|
||||
coconut_keypair
|
||||
.set(KeyPairWithEpoch::new(keys.pop().unwrap(), epoch))
|
||||
.await;
|
||||
|
||||
let initial_dealers = InitialReplacementData {
|
||||
initial_dealers: vec![self_dealer.address.clone()],
|
||||
initial_height: 100,
|
||||
};
|
||||
|
||||
let mut controller = TestingDkgControllerBuilder::default()
|
||||
.with_threshold(3)
|
||||
.with_dealers(dealers.clone())
|
||||
.with_as_dealer(self_dealer.clone())
|
||||
.with_keypair(coconut_keypair)
|
||||
.with_initial_epoch_id(epoch)
|
||||
.with_initial_dealers(initial_dealers)
|
||||
.build()
|
||||
.await;
|
||||
|
||||
let key_size = controller.dkg_client.get_contract_state().await?.key_size;
|
||||
|
||||
let res = controller.dealing_exchange(epoch, true).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
let expected_dealers = dealers
|
||||
.iter()
|
||||
.map(|d| {
|
||||
(
|
||||
d.assigned_index,
|
||||
unchecked_decode_bte_key(&d.bte_public_key_with_proof),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let dealers = controller
|
||||
.state
|
||||
.dealing_exchange_state(epoch)?
|
||||
.dealers
|
||||
.values()
|
||||
.map(|p| (p.assigned_index, p.unwrap_key()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let generated_dealings = controller
|
||||
.state
|
||||
.dealing_exchange_state(epoch)?
|
||||
.generated_dealings
|
||||
.clone();
|
||||
|
||||
assert_eq!(expected_dealers, dealers);
|
||||
|
||||
// now we have dealings
|
||||
assert_eq!(key_size as usize, generated_dealings.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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 cosmwasm_std::Addr;
|
||||
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<(Addr, 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<Addr, 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,977 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg;
|
||||
use crate::coconut::dkg::controller::keys::persist_coconut_keypair;
|
||||
use crate::coconut::dkg::controller::DkgController;
|
||||
use crate::coconut::dkg::state::key_derivation::{DealerRejectionReason, DerivationFailure};
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::keys::KeyPairWithEpoch;
|
||||
use crate::coconut::state::bandwidth_voucher_params;
|
||||
use cosmwasm_std::Addr;
|
||||
use log::debug;
|
||||
use nym_coconut::{check_vk_pairing, Base58, SecretKey, VerificationKey};
|
||||
use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID;
|
||||
use nym_coconut_dkg_common::types::{DealingIndex, EpochId, NodeIndex, PartialContractDealing};
|
||||
use nym_coconut_interface::KeyPair as CoconutKeyPair;
|
||||
use nym_dkg::{
|
||||
bte::{self, decrypt_share},
|
||||
combine_shares, try_recover_verification_keys, Dealing,
|
||||
};
|
||||
use nym_validator_client::nyxd::cosmwasm_client::logs::{find_attribute, Log};
|
||||
use nym_validator_client::nyxd::Hash;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use rocket::form::validate::Contains;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::ops::Deref;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KeyDerivationError {
|
||||
#[error(transparent)]
|
||||
CoconutError(#[from] CoconutError),
|
||||
|
||||
#[error("can't complete key derivation without dealing exchange")]
|
||||
IncompleteDealingExchange,
|
||||
|
||||
#[error("the initial, zeroth, epoch is set to be in resharing mode - this is illegal and should have been impossible!")]
|
||||
ZerothEpochResharing,
|
||||
|
||||
#[error("could not recover our own proposal id from the submitted share")]
|
||||
UnrecoverableProposalId,
|
||||
|
||||
#[error("failed to persist the generated keys to disk: {source}")]
|
||||
KeyPersistenceFailure { source: anyhow::Error },
|
||||
|
||||
#[error("the state file has been tampered with - key generation state is marked as complete, but proposal id is not set")]
|
||||
TamperedStateNoProposal,
|
||||
|
||||
#[error("the state file has been tampered with - key generation state is marked as complete, but the key doesn't exist")]
|
||||
TamperedStateNoKeys,
|
||||
|
||||
#[error("the state file has been tampered with - we're in the middle of DKG for epoch {current_epoch}, but we loaded keys for epoch {keys_epoch}")]
|
||||
TamperedStateWrongEpochKeys {
|
||||
current_epoch: EpochId,
|
||||
keys_epoch: EpochId,
|
||||
},
|
||||
|
||||
#[error("did not derive partial key for ourselves (receiver: {receiver_index})")]
|
||||
NoSelfPartialKey { receiver_index: usize },
|
||||
|
||||
#[error("did not find the proposal id attribute in the transaction events. looked for event '{event_type}' and attribute {attribute_key}' in tx {tx_hash}")]
|
||||
MissingProposalIdAttribute {
|
||||
tx_hash: Hash,
|
||||
event_type: String,
|
||||
attribute_key: String,
|
||||
},
|
||||
|
||||
#[error("the retrieved proposal id ('{raw}') could not be parsed into a number")]
|
||||
UnparsableProposalId { raw: String },
|
||||
}
|
||||
|
||||
impl<R: RngCore + CryptoRng> DkgController<R> {
|
||||
fn verified_dealer_dealings(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
dealer: &Addr,
|
||||
epoch_receivers: &BTreeMap<NodeIndex, bte::PublicKey>,
|
||||
raw_dealings: Vec<PartialContractDealing>,
|
||||
prior_public_key: Option<VerificationKey>,
|
||||
) -> Result<Result<Vec<(DealingIndex, Dealing)>, DealerRejectionReason>, KeyDerivationError>
|
||||
{
|
||||
let threshold = self.state.threshold(epoch_id)?;
|
||||
|
||||
// extract G2 elements from the old verification key of the dealer for checking the resharing dealings
|
||||
let prior_public_components = match prior_public_key {
|
||||
Some(vk) => {
|
||||
if vk.beta_g2().len() != raw_dealings.len().saturating_sub(1) {
|
||||
return Ok(Err(DealerRejectionReason::LastEpochKeyOfWrongSize {
|
||||
key_size: vk.beta_g2().len() + 1,
|
||||
expected: raw_dealings.len(),
|
||||
}));
|
||||
}
|
||||
|
||||
let mut prior = HashMap::new();
|
||||
prior.insert(0, *vk.alpha());
|
||||
for (i, beta) in vk.beta_g2().iter().enumerate() {
|
||||
// element 1, 2, ...
|
||||
prior.insert((i + 1) as DealingIndex, *beta);
|
||||
}
|
||||
|
||||
Some(prior)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut temp_verified = Vec::with_capacity(raw_dealings.len());
|
||||
// make sure ALL of them verify correctly, we can't have a situation where dealing 2 is valid but dealing 3 is not
|
||||
for raw_dealing in raw_dealings {
|
||||
let index = raw_dealing.index;
|
||||
|
||||
// recover the actual dealing from its submitted bytes representation
|
||||
let dealing = match Dealing::try_from_bytes(&raw_dealing.data) {
|
||||
Ok(dealing) => dealing,
|
||||
Err(err) => {
|
||||
warn!("failed to recover dealing {index} from {dealer}: {err}");
|
||||
return Ok(Err(DealerRejectionReason::MalformedDealing {
|
||||
index,
|
||||
err_msg: err.to_string(),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
let prior_public = prior_public_components
|
||||
.as_ref()
|
||||
.and_then(|p| p.get(&index).copied());
|
||||
|
||||
// make sure the cryptographic material embedded inside is actually valid
|
||||
if let Err(err) =
|
||||
dealing.verify(dkg::params(), threshold, epoch_receivers, prior_public)
|
||||
{
|
||||
warn!("dealing {index} from {dealer} is invalid: {err}");
|
||||
return Ok(Err(DealerRejectionReason::InvalidDealing {
|
||||
index,
|
||||
err_msg: err.to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
temp_verified.push((index, dealing))
|
||||
}
|
||||
|
||||
Ok(Ok(temp_verified))
|
||||
}
|
||||
|
||||
fn blacklist_dealer(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
dealer: Addr,
|
||||
reason: DealerRejectionReason,
|
||||
) -> Result<(), KeyDerivationError> {
|
||||
self.state
|
||||
.key_derivation_state_mut(epoch_id)?
|
||||
.rejected_dealers
|
||||
.insert(dealer, reason);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_old_verification_key(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
dealer: &Addr,
|
||||
) -> Result<Option<VerificationKey>, KeyDerivationError> {
|
||||
let Some(previous_epoch) = epoch_id.checked_sub(1) else {
|
||||
return Err(KeyDerivationError::ZerothEpochResharing);
|
||||
};
|
||||
|
||||
let Some(share) = self
|
||||
.dkg_client
|
||||
.get_verification_key_share(previous_epoch, dealer)
|
||||
.await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if !share.verified {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// SAFETY:
|
||||
// since this share appears as 'verified' on the chain, it means the consensus of dealers confirmed its validity
|
||||
// and thus they must have been able to parse it, so the unwrap/expect here is fine
|
||||
Ok(Some(
|
||||
VerificationKey::try_from_bs58(&share.share)
|
||||
.expect("failed to deserialize VERIFIED key"),
|
||||
))
|
||||
}
|
||||
|
||||
/// Attempt to retrieve valid dealings submitted this epoch.
|
||||
///
|
||||
/// For each dealer that submitted a valid public key, query its dealings.
|
||||
/// Then for each of those dealings, make sure they're cryptographically consistent
|
||||
pub(crate) async fn get_valid_dealings(
|
||||
&mut self,
|
||||
epoch_receivers: &BTreeMap<NodeIndex, bte::PublicKey>,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<BTreeMap<DealingIndex, BTreeMap<NodeIndex, Dealing>>, KeyDerivationError> {
|
||||
let expected_key_size = self.dkg_client.get_contract_state().await?.key_size;
|
||||
|
||||
let mut valid_dealings: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::new();
|
||||
|
||||
// given at MOST we'll have like 50 entries here, iterating over entire vector for lookup is fine
|
||||
let initial_dealers = self
|
||||
.dkg_client
|
||||
.get_initial_dealers()
|
||||
.await?
|
||||
.map(|i| i.initial_dealers)
|
||||
.unwrap_or_default();
|
||||
|
||||
// for every valid dealer in this epoch, obtain its dealings
|
||||
for (dealer, dealer_index) in self.state.valid_epoch_receivers(epoch_id)? {
|
||||
// note: if we're in resharing mode, the contract itself will forbid submission of dealings from
|
||||
// parties that were not "initial" dealers, so we don't have to worry about it
|
||||
|
||||
// TODO: introduce caching here in case we crash because those queries are EXPENSIVE
|
||||
let raw_dealings = self
|
||||
.dkg_client
|
||||
.get_dealings(epoch_id, dealer.to_string())
|
||||
.await?;
|
||||
|
||||
if raw_dealings.is_empty() {
|
||||
// we might be in resharing mode and this dealer was not in "initial" set.
|
||||
// in that case we don't expect any dealings
|
||||
if resharing && !initial_dealers.contains(&dealer) {
|
||||
continue;
|
||||
}
|
||||
self.blacklist_dealer(epoch_id, dealer, DealerRejectionReason::NoDealingsProvided)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// no point in verifying any dealings if we don't have all of them
|
||||
if raw_dealings.len() != expected_key_size as usize {
|
||||
self.blacklist_dealer(
|
||||
epoch_id,
|
||||
dealer,
|
||||
DealerRejectionReason::InsufficientNumberOfDealingsProvided {
|
||||
got: raw_dealings.len(),
|
||||
expected: expected_key_size as usize,
|
||||
},
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// if this is resharing DKG, get the public key of this dealer from the previous epoch
|
||||
// and use it for dealing(s) verification
|
||||
let old_public_key = if resharing {
|
||||
// OPTIMIZATION:
|
||||
// rather than explicitly querying for the key, lookup the state from the previous epoch and reconstruct the key
|
||||
let Some(key) = self.get_old_verification_key(epoch_id, &dealer).await? else {
|
||||
self.blacklist_dealer(
|
||||
epoch_id,
|
||||
dealer,
|
||||
DealerRejectionReason::MissingVerifiedLastEpochKey,
|
||||
)?;
|
||||
continue;
|
||||
};
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// parse and validate the received dealings
|
||||
match self.verified_dealer_dealings(
|
||||
epoch_id,
|
||||
&dealer,
|
||||
epoch_receivers,
|
||||
raw_dealings,
|
||||
old_public_key,
|
||||
)? {
|
||||
Ok(verified_dealings) => {
|
||||
// if we managed to verify ALL the dealings from this dealer, insert them into the map
|
||||
for (dealing_index, dealing) in verified_dealings {
|
||||
valid_dealings
|
||||
.entry(dealing_index)
|
||||
.or_default()
|
||||
.insert(dealer_index, dealing);
|
||||
}
|
||||
}
|
||||
Err(reason) => {
|
||||
self.blacklist_dealer(epoch_id, dealer, reason)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(valid_dealings)
|
||||
}
|
||||
|
||||
fn derive_partial_keypair(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
epoch_receivers: BTreeMap<NodeIndex, bte::PublicKey>,
|
||||
dealings: BTreeMap<DealingIndex, BTreeMap<NodeIndex, Dealing>>,
|
||||
) -> Result<Result<KeyPairWithEpoch, DerivationFailure>, KeyDerivationError> {
|
||||
debug!("attempting to derive coconut keypair for epoch {epoch_id}");
|
||||
|
||||
let threshold = self.state.threshold(epoch_id)?;
|
||||
let receiver_index = self.state.receiver_index(epoch_id)?;
|
||||
|
||||
// TODO: make sure that each receiver received its dealings
|
||||
|
||||
// SAFETY:
|
||||
// we have ensured before calling this function that the dealings map is non-empty
|
||||
// and has exactly 'expected key size' number of entries;
|
||||
// furthermore each entry has the same number of sub-entries (ALL dealings from given dealer must be valid)
|
||||
//
|
||||
// SAFETY2:
|
||||
// dealing indexing starts from 0, so accessing 0th element is fine
|
||||
if dealings[&0].len() < threshold as usize {
|
||||
// make sure we have sufficient number of dealings to derive keys for the provided threshold,
|
||||
// otherwise we can't perform the lagrangian interpolation
|
||||
error!("we don't have enough dealings for key derivation");
|
||||
return Ok(Err(DerivationFailure::InsufficientNumberOfDealings {
|
||||
available: dealings[&0].len(),
|
||||
threshold,
|
||||
}));
|
||||
}
|
||||
|
||||
let all_dealers = dealings[&0].keys().copied().collect::<Vec<_>>();
|
||||
|
||||
let mut derived_x = None;
|
||||
let mut derived_secrets = Vec::new();
|
||||
|
||||
let total = dealings.len();
|
||||
|
||||
// for every part of the key
|
||||
for (dealing_index, dealings) in dealings {
|
||||
let human_index = dealing_index + 1;
|
||||
debug!("recovering part {human_index}/{total} of the keys");
|
||||
|
||||
debug!("recovering the partial verification keys");
|
||||
let recovered =
|
||||
match try_recover_verification_keys(&dealings, threshold, &epoch_receivers) {
|
||||
Ok(keys) => keys,
|
||||
Err(err) => {
|
||||
error!("failed to derive partial keys for index {dealing_index}: {err}");
|
||||
return Ok(Err(DerivationFailure::KeyRecoveryFailure {
|
||||
dealing_index,
|
||||
err_msg: err.to_string(),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
self.state
|
||||
.key_derivation_state_mut(epoch_id)?
|
||||
.derived_partials
|
||||
.insert(dealing_index, recovered);
|
||||
|
||||
debug!("decrypting received shares");
|
||||
|
||||
// for every received share of the key
|
||||
let mut shares = Vec::with_capacity(dealings.len());
|
||||
for (dealer_index, dealing) in dealings.into_iter() {
|
||||
// attempt to decrypt our portion
|
||||
let dk = self.state.dkg_keypair().private_key();
|
||||
let share = match decrypt_share(dk, receiver_index, &dealing.ciphertexts, None) {
|
||||
Ok(share) => share,
|
||||
Err(err) => {
|
||||
error!("failed to decrypt share {human_index}/{total} generated from dealer {dealer_index}: {err} - can't generate the full key");
|
||||
return Ok(Err(DerivationFailure::ShareDecryptionFailure {
|
||||
dealing_index,
|
||||
dealer_index,
|
||||
err_msg: err.to_string(),
|
||||
}));
|
||||
}
|
||||
};
|
||||
shares.push(share)
|
||||
}
|
||||
|
||||
debug!("combining the shares into part {human_index}/{total} of the epoch key");
|
||||
|
||||
// SAFETY: combining shares can only fail if we have different number shares and indices
|
||||
// however, we returned an explicit error if decryption of any share failed and thus we know those values must match
|
||||
let secret = combine_shares(shares, &all_dealers).unwrap();
|
||||
if derived_x.is_none() {
|
||||
derived_x = Some(secret)
|
||||
} else {
|
||||
derived_secrets.push(secret)
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY:
|
||||
// we know we had a non-empty map of dealings and thus, at the very least, we must have derived a single secret
|
||||
// (i.e. the x-element)
|
||||
let sk = SecretKey::create_from_raw(derived_x.unwrap(), derived_secrets);
|
||||
let derived_vk = sk.verification_key(bandwidth_voucher_params());
|
||||
|
||||
// make the key we derived out of the decrypted shares matches the partial key
|
||||
// (cryptographically there shouldn't be any reason for the mismatch,
|
||||
// but programmatically we might have accidentally used wrong index or something, so this is a good sanity check)
|
||||
let derived_partial = self
|
||||
.state
|
||||
.key_derivation_state(epoch_id)?
|
||||
.derived_partials_for(receiver_index)
|
||||
.ok_or(KeyDerivationError::NoSelfPartialKey { receiver_index })?;
|
||||
|
||||
if !check_vk_pairing(bandwidth_voucher_params(), &derived_partial, &derived_vk) {
|
||||
// can't do anything, we got all dealings, we derived all keys, but somehow they don't match
|
||||
error!("our derived key does not match the expected partials!");
|
||||
return Ok(Err(DerivationFailure::MismatchedPartialKey));
|
||||
}
|
||||
|
||||
Ok(Ok(KeyPairWithEpoch::new(
|
||||
CoconutKeyPair::from_keys(sk, derived_vk),
|
||||
epoch_id,
|
||||
)))
|
||||
}
|
||||
|
||||
async fn submit_partial_verification_key(
|
||||
&self,
|
||||
key: &VerificationKey,
|
||||
resharing: bool,
|
||||
) -> Result<u64, KeyDerivationError> {
|
||||
fn extract_proposal_id_from_logs(
|
||||
logs: &[Log],
|
||||
tx_hash: Hash,
|
||||
) -> Result<u64, KeyDerivationError> {
|
||||
let event_type = "wasm";
|
||||
let attribute_key = DKG_PROPOSAL_ID;
|
||||
let proposal_attribute = find_attribute(logs, event_type, attribute_key).ok_or(
|
||||
KeyDerivationError::MissingProposalIdAttribute {
|
||||
tx_hash,
|
||||
event_type: event_type.to_string(),
|
||||
attribute_key: attribute_key.to_string(),
|
||||
},
|
||||
)?;
|
||||
|
||||
proposal_attribute
|
||||
.value
|
||||
.parse()
|
||||
.map_err(|_| KeyDerivationError::UnparsableProposalId {
|
||||
raw: proposal_attribute.value.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
debug!("submitting derived partial verification key to the contract");
|
||||
let res = self
|
||||
.dkg_client
|
||||
.submit_verification_key_share(key.to_bs58(), resharing)
|
||||
.await?;
|
||||
let hash = res.transaction_hash;
|
||||
let proposal_id = extract_proposal_id_from_logs(&res.logs, hash)?;
|
||||
debug!("Submitted own verification key share, proposal id {proposal_id} is attached to it. tx hash: {hash}");
|
||||
|
||||
Ok(proposal_id)
|
||||
}
|
||||
|
||||
async fn recover_proposal_id(&self) -> Result<u64, KeyDerivationError> {
|
||||
// unfortunately because the [dkg] contract doesn't store the proposal ids, we have to go through the list of ALL
|
||||
// submitted proposals and find the one with our address
|
||||
self.get_validation_proposals()
|
||||
.await?
|
||||
.get(&Addr::unchecked(
|
||||
self.dkg_client.get_address().await.as_ref(),
|
||||
))
|
||||
.copied()
|
||||
.ok_or(KeyDerivationError::UnrecoverableProposalId)
|
||||
}
|
||||
|
||||
fn complete_with_proposal(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
proposal_id: u64,
|
||||
) -> Result<(), KeyDerivationError> {
|
||||
let derivation_state = self.state.key_derivation_state_mut(epoch_id)?;
|
||||
derivation_state.completed = Some(Ok(()));
|
||||
derivation_state.proposal_id = Some(proposal_id);
|
||||
info!("DKG: Finished key derivation");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn complete_with_failure(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
failure: DerivationFailure,
|
||||
) -> Result<(), KeyDerivationError> {
|
||||
let derivation_state = self.state.key_derivation_state_mut(epoch_id)?;
|
||||
error!("DKG: failed to finish the key derivation: {failure}");
|
||||
derivation_state.completed = Some(Err(failure));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if we have already sent the verification transaction, but we failed to obtain valid proposal id in the previous iteration.
|
||||
async fn maybe_recover_proposal_id(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Option<u64>, KeyDerivationError> {
|
||||
let maybe_share = self
|
||||
.dkg_client
|
||||
.get_verification_own_key_share(epoch_id)
|
||||
.await?;
|
||||
|
||||
// we DID send the transaction and the share is on the chain
|
||||
if maybe_share.is_some() {
|
||||
// note: we only ever send the verification key AFTER persisting our key,
|
||||
// so if the share is on the chain we MUST have the key
|
||||
debug_assert!(self.state.coconut_keypair_is_some().await);
|
||||
|
||||
let proposal_id = self.recover_proposal_id().await?;
|
||||
return Ok(Some(proposal_id));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Check if we already have a valid coconut key in the storage, if so, attempt to submit the partial verification key share
|
||||
/// to the contract and return the generated proposal id.
|
||||
async fn maybe_submit_already_generated_keys(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<Option<u64>, KeyDerivationError> {
|
||||
if let Some(keys) = self.state.unchecked_coconut_keypair().await.deref() {
|
||||
let keys_epoch = keys.issued_for_epoch;
|
||||
return if keys_epoch == epoch_id {
|
||||
debug!("we have already generated keys for this epoch but failed to send them to the contract");
|
||||
|
||||
let proposal_id = self
|
||||
.submit_partial_verification_key(keys.keys.verification_key(), resharing)
|
||||
.await?;
|
||||
Ok(Some(proposal_id))
|
||||
} else {
|
||||
error!("the state file has been tampered with - we're in the middle of DKG for epoch {epoch_id}, but we loaded keys for epoch {keys_epoch}");
|
||||
Err(KeyDerivationError::TamperedStateWrongEpochKeys {
|
||||
current_epoch: epoch_id,
|
||||
keys_epoch,
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Third step of the DKG process during which the nym api will generate its Coconut keypair
|
||||
/// with the [Dealing] received from other dealers. It will then submit its verification key
|
||||
/// to the system so that it could be validated by other participants.
|
||||
pub(crate) async fn verification_key_submission(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), KeyDerivationError> {
|
||||
let key_generation_state = self.state.key_derivation_state(epoch_id)?;
|
||||
|
||||
// check if we have already generated the new keys and submitted verification proposal
|
||||
if key_generation_state.completed_with_success() {
|
||||
if key_generation_state.proposal_id.is_none() {
|
||||
error!("the state file has been tampered with - key generation state is marked as complete, but proposal id is not set");
|
||||
return Err(KeyDerivationError::TamperedStateNoProposal);
|
||||
}
|
||||
if !self.state.coconut_keypair_is_some().await {
|
||||
error!("the state file has been tampered with - key generation state is marked as complete, but the key doesn't exist");
|
||||
return Err(KeyDerivationError::TamperedStateNoKeys);
|
||||
}
|
||||
|
||||
// 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 generated key for this epoch and submitted validation proposal"
|
||||
);
|
||||
return Ok(());
|
||||
} else if let Some(failure) = key_generation_state.completion_failure() {
|
||||
error!("key derivation failed with unrecoverable failure: {failure}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !self.state.dealing_exchange_state(epoch_id)?.completed {
|
||||
return Err(KeyDerivationError::IncompleteDealingExchange);
|
||||
}
|
||||
|
||||
// FAILURE CASE:
|
||||
// check if we have already sent the verification key transaction, but it timed out or got stuck in the mempool and
|
||||
// eventually got executed without us knowing about it, because it's illegal to recommit the key
|
||||
if let Some(proposal_id) = self.maybe_recover_proposal_id(epoch_id).await? {
|
||||
return self.complete_with_proposal(epoch_id, proposal_id);
|
||||
}
|
||||
|
||||
// FAILURE CASE:
|
||||
// check if we have already generated the keys, but we didn't send the tx at all - maybe the internet connection
|
||||
// was momentarily down or something
|
||||
if let Some(proposal_id) = self
|
||||
.maybe_submit_already_generated_keys(epoch_id, resharing)
|
||||
.await?
|
||||
{
|
||||
return self.complete_with_proposal(epoch_id, proposal_id);
|
||||
}
|
||||
|
||||
// ASSUMPTION:
|
||||
// all nym-apis would have filtered the dealers (receivers) the same way since they'd have had the same data
|
||||
let epoch_receivers = self.state.valid_epoch_receivers_keys(epoch_id)?;
|
||||
|
||||
let dealings = self
|
||||
.get_valid_dealings(&epoch_receivers, epoch_id, resharing)
|
||||
.await?;
|
||||
if dealings.is_empty() {
|
||||
error!("did not recover ANY valid dealings - can't generate the epoch key");
|
||||
return self
|
||||
.complete_with_failure(epoch_id, DerivationFailure::NoValidDealings { epoch_id });
|
||||
}
|
||||
|
||||
let dbg_dealers = dealings[&0].keys().collect::<Vec<_>>();
|
||||
debug!("going to use dealings generated by {dbg_dealers:?}");
|
||||
|
||||
let coconut_keypair =
|
||||
match self.derive_partial_keypair(epoch_id, epoch_receivers, dealings)? {
|
||||
Ok(derived_keys) => derived_keys,
|
||||
Err(derivation_failure) => {
|
||||
error!("we can't derive the coconut key: {derivation_failure}");
|
||||
return self.complete_with_failure(epoch_id, derivation_failure);
|
||||
}
|
||||
};
|
||||
|
||||
// before submitting our keys to the contract, persist the generated keypair
|
||||
if let Err(source) = persist_coconut_keypair(&coconut_keypair, &self.coconut_key_path) {
|
||||
return Err(KeyDerivationError::KeyPersistenceFailure { source });
|
||||
}
|
||||
|
||||
let proposal_id = self
|
||||
.submit_partial_verification_key(coconut_keypair.keys.verification_key(), resharing)
|
||||
.await?;
|
||||
|
||||
self.state.set_coconut_keypair(coconut_keypair).await;
|
||||
self.complete_with_proposal(epoch_id, proposal_id)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 crate::coconut::dkg::state::key_derivation::DealerRejectionReason;
|
||||
use crate::coconut::tests::helpers::{
|
||||
exchange_dealings, initialise_controllers, initialise_dkg, submit_public_keys,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn check_dealers_filter_all_good() -> anyhow::Result<()> {
|
||||
let validators = 3;
|
||||
|
||||
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;
|
||||
|
||||
let key_size = chain.lock().unwrap().dkg_contract.contract_state.key_size;
|
||||
for controller in controllers.iter_mut() {
|
||||
let epoch_receivers = controller.state.valid_epoch_receivers_keys(epoch)?;
|
||||
|
||||
let filtered = controller
|
||||
.get_valid_dealings(&epoch_receivers, epoch, false)
|
||||
.await?;
|
||||
|
||||
assert_eq!(filtered.len(), key_size as usize);
|
||||
for dealing_map in filtered.values() {
|
||||
assert_eq!(dealing_map.len(), validators)
|
||||
}
|
||||
let corrupted_status = &controller
|
||||
.state
|
||||
.key_derivation_state(epoch)?
|
||||
.rejected_dealers;
|
||||
assert!(corrupted_status.is_empty());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn check_dealers_filter_one_bad_dealing() -> anyhow::Result<()> {
|
||||
let validators = 3;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let address = controllers[0].cw_address().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;
|
||||
|
||||
let key_size = chain.lock().unwrap().dkg_contract.contract_state.key_size;
|
||||
|
||||
// corrupt just one dealing
|
||||
chain
|
||||
.lock()
|
||||
.unwrap()
|
||||
.dkg_contract
|
||||
.dealings
|
||||
.entry(epoch)
|
||||
.and_modify(|epoch_dealings| {
|
||||
let validator_dealings = epoch_dealings.get_mut(&address.to_string()).unwrap();
|
||||
let mut last = validator_dealings.pop().unwrap();
|
||||
last.data.0.pop();
|
||||
validator_dealings.push(last);
|
||||
});
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let epoch_receivers = controller.state.valid_epoch_receivers_keys(epoch)?;
|
||||
|
||||
let filtered = controller
|
||||
.get_valid_dealings(&epoch_receivers, epoch, false)
|
||||
.await?;
|
||||
|
||||
assert_eq!(filtered.len(), key_size as usize);
|
||||
let corrupted_status = controller
|
||||
.state
|
||||
.key_derivation_state(epoch)?
|
||||
.rejected_dealers
|
||||
.get(&address)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
corrupted_status,
|
||||
DealerRejectionReason::MalformedDealing { .. }
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn check_dealers_resharing_filter_one_missing_dealing() -> anyhow::Result<()> {
|
||||
let validators = 4;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let address = controllers[0].cw_address().await;
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let epoch = chain.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
let key_size = chain.lock().unwrap().dkg_contract.contract_state.key_size;
|
||||
|
||||
initialise_dkg(&mut controllers, false).await;
|
||||
submit_public_keys(&mut controllers, false).await;
|
||||
|
||||
// add all but the first dealing
|
||||
for controller in controllers.iter_mut().skip(1) {
|
||||
controller.dealing_exchange(epoch, false).await?;
|
||||
}
|
||||
|
||||
for controller in controllers.iter_mut().skip(1) {
|
||||
let epoch_receivers = controller.state.valid_epoch_receivers_keys(epoch)?;
|
||||
|
||||
let filtered = controller
|
||||
.get_valid_dealings(&epoch_receivers, epoch, false)
|
||||
.await?;
|
||||
|
||||
assert_eq!(filtered.len(), key_size as usize);
|
||||
let corrupted_status = controller
|
||||
.state
|
||||
.key_derivation_state(epoch)?
|
||||
.rejected_dealers
|
||||
.get(&address)
|
||||
.unwrap();
|
||||
assert_eq!(corrupted_status, &DealerRejectionReason::NoDealingsProvided);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn check_dealers_filter_all_bad_dealings() -> anyhow::Result<()> {
|
||||
let validators = 3;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let address = controllers[0].cw_address().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;
|
||||
|
||||
let key_size = chain.lock().unwrap().dkg_contract.contract_state.key_size;
|
||||
|
||||
// // corrupt all dealings of one address
|
||||
chain
|
||||
.lock()
|
||||
.unwrap()
|
||||
.dkg_contract
|
||||
.dealings
|
||||
.entry(epoch)
|
||||
.and_modify(|epoch_dealings| {
|
||||
let validator_dealings = epoch_dealings.get_mut(&address.to_string()).unwrap();
|
||||
validator_dealings.iter_mut().for_each(|dealing| {
|
||||
dealing.data.0.pop();
|
||||
});
|
||||
});
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let epoch_receivers = controller.state.valid_epoch_receivers_keys(epoch)?;
|
||||
|
||||
let filtered = controller
|
||||
.get_valid_dealings(&epoch_receivers, epoch, false)
|
||||
.await?;
|
||||
|
||||
assert_eq!(filtered.len(), key_size as usize);
|
||||
for dealings in filtered.values() {
|
||||
assert_eq!(dealings.len(), validators - 1)
|
||||
}
|
||||
|
||||
let corrupted_status = controller
|
||||
.state
|
||||
.key_derivation_state(epoch)?
|
||||
.rejected_dealers
|
||||
.get(&address)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
corrupted_status,
|
||||
DealerRejectionReason::MalformedDealing { .. }
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn check_dealers_filter_dealing_verification_error() -> anyhow::Result<()> {
|
||||
let validators = 3;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let address = controllers[0].cw_address().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;
|
||||
|
||||
let key_size = chain.lock().unwrap().dkg_contract.contract_state.key_size;
|
||||
|
||||
// corrupt just one dealing
|
||||
chain
|
||||
.lock()
|
||||
.unwrap()
|
||||
.dkg_contract
|
||||
.dealings
|
||||
.entry(epoch)
|
||||
.and_modify(|epoch_dealings| {
|
||||
let validator_dealings = epoch_dealings.get_mut(&address.to_string()).unwrap();
|
||||
let mut last = validator_dealings.pop().unwrap();
|
||||
let value = last.data.0.pop().unwrap();
|
||||
if value == 42 {
|
||||
last.data.0.push(43);
|
||||
} else {
|
||||
last.data.0.push(42);
|
||||
}
|
||||
validator_dealings.push(last);
|
||||
});
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let epoch_receivers = controller.state.valid_epoch_receivers_keys(epoch)?;
|
||||
|
||||
let filtered = controller
|
||||
.get_valid_dealings(&epoch_receivers, epoch, false)
|
||||
.await?;
|
||||
|
||||
assert_eq!(filtered.len(), key_size as usize);
|
||||
let corrupted_status = controller
|
||||
.state
|
||||
.key_derivation_state(epoch)?
|
||||
.rejected_dealers
|
||||
.get(&address)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
corrupted_status,
|
||||
DealerRejectionReason::InvalidDealing { .. }
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn partial_keypair_derivation() -> anyhow::Result<()> {
|
||||
let validators = 3;
|
||||
|
||||
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;
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let epoch_receivers = controller.state.valid_epoch_receivers_keys(epoch)?;
|
||||
|
||||
let filtered = controller
|
||||
.get_valid_dealings(&epoch_receivers, epoch, false)
|
||||
.await?;
|
||||
|
||||
let res = controller
|
||||
.derive_partial_keypair(epoch, epoch_receivers, filtered)
|
||||
.unwrap();
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn partial_keypair_derivation_with_threshold() -> anyhow::Result<()> {
|
||||
let validators = 4;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let address = controllers[0].cw_address().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;
|
||||
|
||||
// corrupt just one dealing
|
||||
chain
|
||||
.lock()
|
||||
.unwrap()
|
||||
.dkg_contract
|
||||
.dealings
|
||||
.entry(epoch)
|
||||
.and_modify(|epoch_dealings| {
|
||||
let validator_dealings = epoch_dealings.get_mut(&address.to_string()).unwrap();
|
||||
let mut last = validator_dealings.pop().unwrap();
|
||||
last.data.0.pop();
|
||||
validator_dealings.push(last);
|
||||
});
|
||||
|
||||
for controller in controllers.iter_mut().skip(1) {
|
||||
let epoch_receivers = controller.state.valid_epoch_receivers_keys(epoch)?;
|
||||
let filtered = controller
|
||||
.get_valid_dealings(&epoch_receivers, epoch, false)
|
||||
.await?;
|
||||
|
||||
let res = controller
|
||||
.derive_partial_keypair(epoch, epoch_receivers, filtered)
|
||||
.unwrap();
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn submit_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;
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let res = controller.verification_key_submission(epoch, false).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
assert!(controller
|
||||
.state
|
||||
.key_derivation_state(epoch)?
|
||||
.completed_with_success());
|
||||
let keys = controller.state.take_coconut_keypair().await;
|
||||
assert!(keys.is_some());
|
||||
assert_eq!(keys.as_ref().unwrap().issued_for_epoch, epoch);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::controller::DkgController;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use cw3::Status;
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KeyFinalizationError {
|
||||
#[error(transparent)]
|
||||
CoconutError(#[from] CoconutError),
|
||||
|
||||
#[error("our proposal for key verification is still open (or is pending) (proposal id: {proposal_id}) ")]
|
||||
UnresolvedProposal { proposal_id: u64 },
|
||||
|
||||
#[error("our proposal for key verification has been rejected (proposal id: {proposal_id})")]
|
||||
RejectedProposal { proposal_id: u64 },
|
||||
|
||||
#[error("can't complete key finalization without key validation")]
|
||||
IncompleteKeyValidation,
|
||||
}
|
||||
|
||||
impl<R: RngCore + CryptoRng> DkgController<R> {
|
||||
pub(crate) async fn verification_key_finalization(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<(), KeyFinalizationError> {
|
||||
let key_finalization_state = self.state.key_finalization_state(epoch_id)?;
|
||||
|
||||
// check if we have already executed our own proposal
|
||||
if key_finalization_state.completed() {
|
||||
// the only way this could be a false positive is if the chain forked and blocks got reverted,
|
||||
// but I don't think we have to worry about that
|
||||
debug!("our key has already been verified");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !self.state.key_validation_state(epoch_id)?.completed {
|
||||
return Err(KeyFinalizationError::IncompleteKeyValidation);
|
||||
}
|
||||
|
||||
let proposal_id = self.state.proposal_id(epoch_id)?;
|
||||
|
||||
// check whether our key has already been verified with executed proposal,
|
||||
// either by us in previous iteration after a timeout
|
||||
// or by another party
|
||||
let status = self.dkg_client.get_proposal_status(proposal_id).await?;
|
||||
match status {
|
||||
// if the proposal hasn't been resolved, there's not much we can do but wait and pray
|
||||
Status::Pending | Status::Open => {
|
||||
// 'theoretically' it's possible that more votes are going to come in, but it's very unlikely
|
||||
warn!("our proposal ({proposal_id}) still hasn't received enough votes to get accepted");
|
||||
return Err(KeyFinalizationError::UnresolvedProposal { proposal_id });
|
||||
}
|
||||
// if the proposal has been rejected, there's nothing we can do, we failed the DKG
|
||||
Status::Rejected => {
|
||||
// technically there's nothing enforcing this, so as long as our keys have been properly generated
|
||||
// (even though they've been rejected by other parties), they could still issue [cryptographically] valid credentials
|
||||
error!("our key verification proposal ({proposal_id}) has been rejected - we can't use our derived keys!");
|
||||
self.state.key_finalization_state_mut(epoch_id)?.completed = true;
|
||||
return Err(KeyFinalizationError::RejectedProposal { proposal_id });
|
||||
}
|
||||
// if the proposal has passed, execute it to finalize our key
|
||||
Status::Passed => {
|
||||
self.dkg_client
|
||||
.execute_verification_key_share(proposal_id)
|
||||
.await?;
|
||||
}
|
||||
// if they proposal has already been executed, we're done!
|
||||
Status::Executed => {
|
||||
// generally each dealer is responsible for executing its own proposals,
|
||||
// but technically there's nothing preventing other dealers from executing them
|
||||
debug!("our dkg proposal has already been executed");
|
||||
}
|
||||
}
|
||||
|
||||
self.state.key_finalization_state_mut(epoch_id)?.completed = true;
|
||||
self.state.validate_coconut_keypair();
|
||||
info!("DKG: Finalized own verification key on chain");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: the following tests currently do NOT cover all cases
|
||||
// I've (@JS) only updated old, existing, tests. nothing more
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::coconut::tests::helpers::{
|
||||
derive_keypairs, exchange_dealings, initialise_controllers, initialise_dkg,
|
||||
submit_public_keys, validate_keys,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn finalize_verification_key() -> anyhow::Result<()> {
|
||||
let validators = 4;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let epoch = chain.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
|
||||
initialise_dkg(&mut controllers, false).await;
|
||||
submit_public_keys(&mut controllers, false).await;
|
||||
exchange_dealings(&mut controllers, false).await;
|
||||
derive_keypairs(&mut controllers, false).await;
|
||||
validate_keys(&mut controllers, false).await;
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let res = controller.verification_key_finalization(epoch).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
assert!(controller.state.key_finalization_state(epoch)?.completed);
|
||||
}
|
||||
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let guard = chain.lock().unwrap();
|
||||
let proposals = &guard.multisig_contract.proposals;
|
||||
assert_eq!(proposals.len(), validators);
|
||||
|
||||
for proposal in proposals.values() {
|
||||
assert_eq!(Status::Executed, proposal.status)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::controller::DkgController;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::state::bandwidth_voucher_params;
|
||||
use cosmwasm_std::Addr;
|
||||
use cw3::Vote;
|
||||
use nym_coconut::{check_vk_pairing, Base58, VerificationKey};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_coconut_dkg_common::verification_key::ContractVKShare;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
fn vote_matches(voted_yes: bool, chain_vote: Vote) -> bool {
|
||||
if voted_yes && chain_vote == Vote::Yes {
|
||||
true
|
||||
} else {
|
||||
!voted_yes && chain_vote == Vote::No
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum KeyValidationError {
|
||||
#[error(transparent)]
|
||||
CoconutError(#[from] CoconutError),
|
||||
|
||||
#[error("can't complete key validation without key derivation")]
|
||||
IncompleteKeyDerivation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ShareRejectionReason {
|
||||
#[error("{owner} does not appear to be present in the list of receivers for epoch {epoch_id}")]
|
||||
NotAReceiver { epoch_id: EpochId, owner: Addr },
|
||||
|
||||
#[error("the share from {owner} for epoch {epoch_id} already appears as verified on chain!")]
|
||||
AlreadyVerifiedOnChain { epoch_id: EpochId, owner: Addr },
|
||||
|
||||
#[error(
|
||||
"the share from {owner} for epoch {epoch_id} does not use valid base58 encoding: {source}"
|
||||
)]
|
||||
MalformedKeyEncoding {
|
||||
epoch_id: EpochId,
|
||||
owner: Addr,
|
||||
#[source]
|
||||
source: nym_coconut::CoconutError,
|
||||
},
|
||||
|
||||
#[error("did not derive partial keys for {owner} at index {receiver_index} for epoch {epoch_id} during the dealings exchange")]
|
||||
MissingDerivedPartialKey {
|
||||
epoch_id: EpochId,
|
||||
owner: Addr,
|
||||
receiver_index: usize,
|
||||
},
|
||||
|
||||
#[error("the provided keys {owner} at index {receiver_index} for epoch {epoch_id} either did not match the partial keys derived during the dealings exchange or failed the local bilinear pairing consistency check")]
|
||||
InconsistentKeys {
|
||||
epoch_id: EpochId,
|
||||
owner: Addr,
|
||||
receiver_index: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl<R: RngCore + CryptoRng> DkgController<R> {
|
||||
async fn verify_share(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
share: ContractVKShare,
|
||||
) -> Result<(Option<bool>, Option<ShareRejectionReason>), KeyValidationError> {
|
||||
fn reject(
|
||||
reason: ShareRejectionReason,
|
||||
) -> Result<(Option<bool>, Option<ShareRejectionReason>), KeyValidationError> {
|
||||
Ok((Some(false), Some(reason)))
|
||||
}
|
||||
|
||||
let owner = share.owner;
|
||||
|
||||
if share.verified {
|
||||
error!("the share from {owner} has already been validated on chain - this should be impossible unless this machine is running seriously behind");
|
||||
let reason = ShareRejectionReason::AlreadyVerifiedOnChain { epoch_id, owner };
|
||||
// explicitly return 'None' for the vote as we don't have to (nor even should) vote for this share
|
||||
return Ok((None, Some(reason)));
|
||||
}
|
||||
|
||||
// get the receiver index [of the dealings] for this participant
|
||||
let Some(receiver_index) = self
|
||||
.state
|
||||
.valid_epoch_receivers(epoch_id)?
|
||||
.iter()
|
||||
.position(|(addr, _)| addr == owner)
|
||||
else {
|
||||
return reject(ShareRejectionReason::NotAReceiver { epoch_id, owner });
|
||||
};
|
||||
|
||||
// attempt to recover the underlying key from its bs58 representation
|
||||
let recovered_key = match VerificationKey::try_from_bs58(share.share) {
|
||||
Ok(key) => key,
|
||||
Err(source) => {
|
||||
return reject(ShareRejectionReason::MalformedKeyEncoding {
|
||||
epoch_id,
|
||||
owner,
|
||||
source,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// retrieve the key we have recovered ourselves during the dealings exchange
|
||||
let Some(self_derived) = self
|
||||
.state
|
||||
.key_derivation_state(epoch_id)?
|
||||
.derived_partials_for(receiver_index)
|
||||
else {
|
||||
return reject(ShareRejectionReason::MissingDerivedPartialKey {
|
||||
epoch_id,
|
||||
owner,
|
||||
receiver_index,
|
||||
});
|
||||
};
|
||||
|
||||
if !check_vk_pairing(bandwidth_voucher_params(), &self_derived, &recovered_key) {
|
||||
return reject(ShareRejectionReason::InconsistentKeys {
|
||||
epoch_id,
|
||||
owner,
|
||||
receiver_index,
|
||||
});
|
||||
}
|
||||
|
||||
// all is good -> accept the keys!
|
||||
Ok((Some(true), None))
|
||||
}
|
||||
|
||||
async fn generate_votes(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<HashMap<u64, bool>, KeyValidationError> {
|
||||
let proposals = self.get_validation_proposals().await?;
|
||||
let vk_shares = self
|
||||
.dkg_client
|
||||
.get_verification_key_shares(epoch_id)
|
||||
.await?;
|
||||
|
||||
let mut votes = HashMap::new();
|
||||
for contract_share in vk_shares {
|
||||
let owner = contract_share.owner.clone();
|
||||
debug!("verifying vk share from {owner}");
|
||||
|
||||
// there's no point in checking anything if there doesn't exist an associated multisig proposal
|
||||
let Some(proposal_id) = proposals.get(&owner) else {
|
||||
warn!("there does not seem to exist proposal for share validation from {owner}");
|
||||
continue;
|
||||
};
|
||||
|
||||
// if this is our share, obviously vote for yes without spending time on verification
|
||||
if owner.as_ref() == self.dkg_client.get_address().await.as_ref() {
|
||||
votes.insert(*proposal_id, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
let (vote, rejection_reason) = self.verify_share(epoch_id, contract_share).await?;
|
||||
if let Some(vote) = vote {
|
||||
votes.insert(*proposal_id, vote);
|
||||
}
|
||||
if let Some(rejection_reason) = rejection_reason {
|
||||
warn!("rejecting share from {owner} (proposal: {proposal_id}): {rejection_reason}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(votes)
|
||||
}
|
||||
|
||||
async fn resubmit_validation_votes(&self, epoch_id: EpochId) -> Result<(), KeyValidationError> {
|
||||
let key_validation_state = self.state.key_validation_state(epoch_id)?;
|
||||
|
||||
for (&proposal, &vote) in &key_validation_state.votes {
|
||||
// check whether we might have already voted on this particular proposal
|
||||
// (the vote might have gotten stuck in the mempool)
|
||||
let chain_vote = self.dkg_client.get_vote(proposal).await?;
|
||||
if let Some(chain_vote) = chain_vote.vote {
|
||||
warn!("we have already voted for proposal {proposal} before - we probably crashed or the chain timed out!");
|
||||
|
||||
// that's an extremely weird behaviour -> perhaps the user voted manually outside the nym-api,
|
||||
// but we can't do anything about it
|
||||
if !vote_matches(vote, chain_vote.vote) {
|
||||
error!("our vote for proposal {proposal} doesn't match the on-chain data! We decided to vote '{vote}' but the chain has {:?}", chain_vote.vote);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
warn!("we have already decided on the vote status for proposal {proposal} before (vote: {vote}), but failed to submit it");
|
||||
self.dkg_client
|
||||
.vote_verification_key_share(proposal, vote)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn verification_key_validation(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<(), KeyValidationError> {
|
||||
let key_validation_state = self.state.key_validation_state(epoch_id)?;
|
||||
|
||||
// check if we have already validated and voted for all keys
|
||||
if key_validation_state.completed() {
|
||||
// the only way this could be a false positive is if the chain forked and blocks got reverted,
|
||||
// but I don't think we have to worry about that
|
||||
debug!("we have already voted in all validation proposals");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !self
|
||||
.state
|
||||
.key_derivation_state(epoch_id)?
|
||||
.completed_with_success()
|
||||
{
|
||||
return Err(KeyValidationError::IncompleteKeyDerivation);
|
||||
}
|
||||
|
||||
// FAILURE CASE:
|
||||
// check if we have already verified the keys, but some voting txs either didn't get executed
|
||||
// or got executed without us knowing about it
|
||||
if !key_validation_state.votes.is_empty() {
|
||||
debug!(
|
||||
"we have already validated all keys for this epoch, but might have failed to vote"
|
||||
);
|
||||
self.resubmit_validation_votes(epoch_id).await?;
|
||||
|
||||
// if we managed to resubmit the votes (i.e. we didn't return an error)
|
||||
// it means the state is complete now
|
||||
info!("DKG: resubmitted previously generated votes - finished key validation");
|
||||
self.state.key_validation_state_mut(epoch_id)?.completed = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let votes = self.generate_votes(epoch_id).await?;
|
||||
self.state.key_validation_state_mut(epoch_id)?.votes = votes.clone();
|
||||
|
||||
// send the votes
|
||||
for (proposal, vote) in votes {
|
||||
// FUTURE OPTIMIZATION: we could batch them in a single tx
|
||||
self.dkg_client
|
||||
.vote_verification_key_share(proposal, vote)
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.state.key_validation_state_mut(epoch_id)?.completed = true;
|
||||
|
||||
info!("DKG: validated all the other verification keys");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: the following tests currently do NOT cover all cases
|
||||
// I've (@JS) only updated old, existing, tests. nothing more
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::coconut::tests::helpers::{
|
||||
derive_keypairs, exchange_dealings, initialise_controllers, initialise_dkg,
|
||||
submit_public_keys,
|
||||
};
|
||||
use cw3::Status;
|
||||
use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn validate_verification_key() -> anyhow::Result<()> {
|
||||
let validators = 4;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let epoch = chain.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
|
||||
initialise_dkg(&mut controllers, false).await;
|
||||
submit_public_keys(&mut controllers, false).await;
|
||||
exchange_dealings(&mut controllers, false).await;
|
||||
derive_keypairs(&mut controllers, false).await;
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let res = controller.verification_key_validation(epoch).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
assert!(controller.state.key_validation_state(epoch)?.completed);
|
||||
}
|
||||
|
||||
let guard = chain.lock().unwrap();
|
||||
let proposals = &guard.multisig_contract.proposals;
|
||||
assert_eq!(proposals.len(), validators);
|
||||
|
||||
for proposal in proposals.values() {
|
||||
assert_eq!(Status::Passed, proposal.status)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn validate_verification_key_malformed_share() -> anyhow::Result<()> {
|
||||
let validators = 4;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let epoch = chain.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
|
||||
initialise_dkg(&mut controllers, false).await;
|
||||
submit_public_keys(&mut controllers, false).await;
|
||||
exchange_dealings(&mut controllers, false).await;
|
||||
derive_keypairs(&mut controllers, false).await;
|
||||
|
||||
let first_dealer = controllers[0].dkg_client.get_address().await;
|
||||
|
||||
{
|
||||
let mut guard = chain.lock().unwrap();
|
||||
let shares = guard
|
||||
.dkg_contract
|
||||
.verification_shares
|
||||
.get_mut(&epoch)
|
||||
.unwrap();
|
||||
let share = shares.get_mut(first_dealer.as_ref()).unwrap();
|
||||
// mess up the share
|
||||
share.share.push('x');
|
||||
}
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let res = controller.verification_key_validation(epoch).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
assert!(controller.state.key_validation_state(epoch)?.completed);
|
||||
}
|
||||
|
||||
let guard = chain.lock().unwrap();
|
||||
let proposals = &guard.multisig_contract.proposals;
|
||||
assert_eq!(proposals.len(), validators);
|
||||
|
||||
// the proposal from the first dealer would have gotten rejected
|
||||
for proposal in proposals.values() {
|
||||
let addr = owner_from_cosmos_msgs(&proposal.msgs).unwrap();
|
||||
if addr.as_str() == first_dealer.as_ref() {
|
||||
assert_eq!(Status::Rejected, proposal.status)
|
||||
} else {
|
||||
assert_eq!(Status::Passed, proposal.status)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn validate_verification_key_unpaired_share() -> anyhow::Result<()> {
|
||||
let validators = 2;
|
||||
|
||||
let mut controllers = initialise_controllers(validators).await;
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let epoch = chain.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
|
||||
initialise_dkg(&mut controllers, false).await;
|
||||
submit_public_keys(&mut controllers, false).await;
|
||||
exchange_dealings(&mut controllers, false).await;
|
||||
derive_keypairs(&mut controllers, false).await;
|
||||
|
||||
let first_dealer = controllers[0].dkg_client.get_address().await;
|
||||
let second_dealer = controllers[1].dkg_client.get_address().await;
|
||||
|
||||
{
|
||||
let mut guard = chain.lock().unwrap();
|
||||
let shares = guard
|
||||
.dkg_contract
|
||||
.verification_shares
|
||||
.get_mut(&epoch)
|
||||
.unwrap();
|
||||
let second_share = shares.get(second_dealer.as_ref()).unwrap().clone();
|
||||
|
||||
let share = shares.get_mut(first_dealer.as_ref()).unwrap();
|
||||
// mess up the share
|
||||
share.share = second_share.share;
|
||||
}
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
let res = controller.verification_key_validation(epoch).await;
|
||||
assert!(res.is_ok());
|
||||
|
||||
assert!(controller.state.key_validation_state(epoch)?.completed);
|
||||
}
|
||||
|
||||
let guard = chain.lock().unwrap();
|
||||
let proposals = &guard.multisig_contract.proposals;
|
||||
assert_eq!(proposals.len(), validators);
|
||||
|
||||
// the proposal from the first dealer would have gotten rejected
|
||||
for proposal in proposals.values() {
|
||||
let addr = owner_from_cosmos_msgs(&proposal.msgs).unwrap();
|
||||
if addr.as_str() == first_dealer.as_ref() {
|
||||
assert_eq!(Status::Rejected, proposal.status)
|
||||
} else {
|
||||
assert_eq!(Status::Passed, proposal.status)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,102 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub(crate) fn params() -> &'static nym_dkg::bte::Params {
|
||||
static PARAMS: OnceLock<nym_dkg::bte::Params> = OnceLock::new();
|
||||
PARAMS.get_or_init(nym_dkg::bte::setup)
|
||||
}
|
||||
|
||||
pub(crate) mod client;
|
||||
pub(crate) mod complaints;
|
||||
pub(crate) mod controller;
|
||||
pub(crate) mod dealing;
|
||||
mod helpers;
|
||||
pub(crate) mod key_derivation;
|
||||
pub(crate) mod key_finalization;
|
||||
pub(crate) mod key_validation;
|
||||
pub(crate) mod public_key;
|
||||
pub(crate) mod state;
|
||||
pub(crate) mod verification_key;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::coconut::tests::helpers::{
|
||||
derive_keypairs, exchange_dealings, finalize, init_chain, initialise_controller,
|
||||
initialise_dkg, submit_public_keys, validate_keys,
|
||||
};
|
||||
use nym_coconut::aggregate_verification_keys;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn reshare_preserves_master_key() -> anyhow::Result<()> {
|
||||
let validators = 4;
|
||||
let chain = init_chain();
|
||||
|
||||
let mut controllers = vec![];
|
||||
for i in 0..validators {
|
||||
controllers.push(initialise_controller(chain.clone(), i).await)
|
||||
}
|
||||
|
||||
let chain = controllers[0].chain_state.clone();
|
||||
let epoch = chain.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
|
||||
// EPOCH 0 DKG
|
||||
initialise_dkg(&mut controllers, false).await;
|
||||
submit_public_keys(&mut controllers, false).await;
|
||||
exchange_dealings(&mut controllers, false).await;
|
||||
derive_keypairs(&mut controllers, false).await;
|
||||
validate_keys(&mut controllers, false).await;
|
||||
finalize(&mut controllers).await;
|
||||
|
||||
// get the master key
|
||||
let mut vks = vec![];
|
||||
let mut indices = vec![];
|
||||
for controller in controllers.iter() {
|
||||
let vk = controller.unchecked_coconut_vk().await;
|
||||
let index = controller.state.assigned_index(epoch)?;
|
||||
vks.push(vk);
|
||||
indices.push(index);
|
||||
}
|
||||
let initial_first_key = vks[0].clone();
|
||||
let initial_master_vk = aggregate_verification_keys(&vks, Some(&indices))?;
|
||||
|
||||
let new_controller = initialise_controller(chain.clone(), validators).await;
|
||||
controllers.push(new_controller);
|
||||
|
||||
chain.lock().unwrap().advance_epoch_in_reshare_mode();
|
||||
|
||||
let next_epoch = epoch + 1;
|
||||
// sanity check
|
||||
assert_eq!(
|
||||
next_epoch,
|
||||
chain.lock().unwrap().dkg_contract.epoch.epoch_id
|
||||
);
|
||||
|
||||
// EPOCH 1 DKG (resharing)
|
||||
submit_public_keys(&mut controllers, true).await;
|
||||
exchange_dealings(&mut controllers, true).await;
|
||||
derive_keypairs(&mut controllers, true).await;
|
||||
validate_keys(&mut controllers, true).await;
|
||||
finalize(&mut controllers).await;
|
||||
|
||||
let mut vks = vec![];
|
||||
let mut indices = vec![];
|
||||
for controller in controllers.iter() {
|
||||
let vk = controller.unchecked_coconut_vk().await;
|
||||
let index = controller.state.assigned_index(next_epoch)?;
|
||||
vks.push(vk);
|
||||
indices.push(index);
|
||||
}
|
||||
|
||||
let updated_first_key = vks[0].clone();
|
||||
let reshared_master_vk = aggregate_verification_keys(&vks, Some(&indices))?;
|
||||
|
||||
// individual keys changed
|
||||
assert_ne!(initial_first_key, updated_first_key);
|
||||
|
||||
// but master didn't
|
||||
assert_eq!(initial_master_vk, reshared_master_vk);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,112 +1,132 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::client::DkgClient;
|
||||
use crate::coconut::dkg::state::State;
|
||||
use crate::coconut::dkg::controller::DkgController;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use log::debug;
|
||||
use nym_coconut_dkg_common::dealer::DealerType;
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use thiserror::Error;
|
||||
|
||||
pub(crate) async fn public_key_submission(
|
||||
dkg_client: &DkgClient,
|
||||
state: &mut State,
|
||||
resharing: bool,
|
||||
) -> Result<(), CoconutError> {
|
||||
if state.was_in_progress() {
|
||||
let own_address = dkg_client.get_address().await.as_ref().to_string();
|
||||
let is_initial_dealer = dkg_client
|
||||
.get_initial_dealers()
|
||||
.await?
|
||||
.map(|data| data.initial_dealers.iter().any(|d| *d == own_address))
|
||||
.unwrap_or(false);
|
||||
let reset_coconut_keypair = !resharing || !is_initial_dealer;
|
||||
debug!(
|
||||
"Resetting state, with coconut keypair reset: {}",
|
||||
reset_coconut_keypair
|
||||
);
|
||||
state.reset_persistent(reset_coconut_keypair).await;
|
||||
}
|
||||
if state.node_index().is_some() {
|
||||
debug!("Node index was set previously, nothing to do");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let bte_key = bs58::encode(&state.dkg_keypair().public_key().to_bytes()).into_string();
|
||||
let dealer_details = dkg_client.get_self_registered_dealer_details().await?;
|
||||
let index = if let Some(details) = dealer_details.details {
|
||||
if dealer_details.dealer_type == DealerType::Past {
|
||||
// If it was a dealer in a previous epoch, re-register it for this epoch
|
||||
debug!("Registering for the current DKG round, with keys from a previous epoch");
|
||||
dkg_client
|
||||
.register_dealer(bte_key, state.announce_address().to_string(), resharing)
|
||||
.await?;
|
||||
}
|
||||
details.assigned_index
|
||||
} else {
|
||||
debug!("Registering for the first time to be a dealer");
|
||||
// First time registration
|
||||
dkg_client
|
||||
.register_dealer(bte_key, state.announce_address().to_string(), resharing)
|
||||
.await?
|
||||
};
|
||||
state.set_node_index(Some(index));
|
||||
info!("DKG: Using node index {}", index);
|
||||
|
||||
Ok(())
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PublicKeySubmissionError {
|
||||
#[error(transparent)]
|
||||
CoconutError(#[from] CoconutError),
|
||||
}
|
||||
|
||||
impl<R: RngCore + CryptoRng> DkgController<R> {
|
||||
/// First step of the DKG process during which the nym api will register for the key exchange
|
||||
/// by submitting its:
|
||||
/// - BTE public key (alongside the proof of discrete log)
|
||||
/// - ed25519 public key
|
||||
/// - announce address to be used by clients for obtaining credentials
|
||||
/// Upon successful registration, the node will receive a unique "NodeIndex"
|
||||
/// which is the x-coordinate of the to be derived keys.
|
||||
///
|
||||
/// During this step any prior coconut keys will be invalidated, i.e. keys from the previous epoch
|
||||
/// won't be used for issuing new credentials.
|
||||
///
|
||||
/// Furthermore, if the node experienced any failures during this step, a recovery will be attempted.
|
||||
pub(crate) async fn public_key_submission(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
resharing: bool,
|
||||
) -> Result<(), PublicKeySubmissionError> {
|
||||
self.state.maybe_init_dkg_state(epoch_id);
|
||||
let registration_state = self.state.registration_state(epoch_id)?;
|
||||
|
||||
// check if we have already submitted the key
|
||||
if registration_state.completed() {
|
||||
// the only way this could be a false positive is if the chain forked and blocks got reverted,
|
||||
// but I don't think we have to worry about that
|
||||
debug!("we have already submitted the keys for this epoch");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// if we have coconut keys available, it means we have already completed the DKG before (in previous epoch)
|
||||
// in which case, invalidate it so that it wouldn't be used for credential issuance
|
||||
self.state.invalidate_coconut_keypair();
|
||||
|
||||
// FAILURE CASE:
|
||||
// check if we have already sent the registration transaction, but it timed out or got stuck in the mempool and
|
||||
// eventually got executed without us knowing about it
|
||||
// in that case we MUST recover the assigned index since we won't be allowed to register again
|
||||
let dealer_details = self.dkg_client.get_self_registered_dealer_details().await?;
|
||||
if dealer_details.dealer_type.is_current() {
|
||||
if let Some(details) = dealer_details.details {
|
||||
// the tx did actually go through
|
||||
self.state.registration_state_mut(epoch_id)?.assigned_index =
|
||||
Some(details.assigned_index);
|
||||
info!("DKG: recovered node index: {}", details.assigned_index);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// perform the full registration instead
|
||||
let bte_key = bs58::encode(&self.state.dkg_keypair().public_key().to_bytes()).into_string();
|
||||
let identity_key = self.state.identity_key().to_base58_string();
|
||||
let announce_address = self.state.announce_address().to_string();
|
||||
|
||||
let assigned_index = self
|
||||
.dkg_client
|
||||
.register_dealer(bte_key, identity_key, announce_address, resharing)
|
||||
.await?;
|
||||
self.state.registration_state_mut(epoch_id)?.assigned_index = Some(assigned_index);
|
||||
info!("DKG: Using node index {assigned_index}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: the following tests currently do NOT cover all cases
|
||||
// I've (@JS) only updated old, existing, tests. nothing more
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::coconut::dkg::state::PersistentState;
|
||||
use crate::coconut::tests::DummyClient;
|
||||
use crate::coconut::KeyPair;
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use rand::rngs::OsRng;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use url::Url;
|
||||
|
||||
const TEST_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
|
||||
use crate::coconut::tests::fixtures;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // expensive test
|
||||
async fn submit_public_key() {
|
||||
let dkg_client = DkgClient::new(DummyClient::new(
|
||||
AccountId::from_str(TEST_VALIDATOR_ADDRESS).unwrap(),
|
||||
));
|
||||
let mut state = State::new(
|
||||
PathBuf::default(),
|
||||
PersistentState::default(),
|
||||
Url::parse("localhost:8000").unwrap(),
|
||||
DkgKeyPair::new(&nym_dkg::bte::setup(), OsRng),
|
||||
KeyPair::new(),
|
||||
);
|
||||
async fn submit_public_key() -> anyhow::Result<()> {
|
||||
let mut controller = fixtures::dkg_controller_fixture().await;
|
||||
let epoch = controller.dkg_client.get_current_epoch().await?.epoch_id;
|
||||
|
||||
assert!(dkg_client
|
||||
assert!(controller
|
||||
.dkg_client
|
||||
.get_self_registered_dealer_details()
|
||||
.await
|
||||
.unwrap()
|
||||
.await?
|
||||
.details
|
||||
.is_none());
|
||||
public_key_submission(&dkg_client, &mut state, false)
|
||||
.await
|
||||
.unwrap();
|
||||
let client_idx = dkg_client
|
||||
controller.public_key_submission(epoch, false).await?;
|
||||
let client_idx = controller
|
||||
.dkg_client
|
||||
.get_self_registered_dealer_details()
|
||||
.await
|
||||
.unwrap()
|
||||
.await?
|
||||
.details
|
||||
.unwrap()
|
||||
.assigned_index;
|
||||
assert_eq!(state.node_index().unwrap(), client_idx);
|
||||
assert_eq!(
|
||||
controller
|
||||
.state
|
||||
.registration_state(epoch)?
|
||||
.assigned_index
|
||||
.unwrap(),
|
||||
client_idx
|
||||
);
|
||||
|
||||
// keeps the same index from chain, not calling register_dealer again
|
||||
state.set_node_index(None);
|
||||
public_key_submission(&dkg_client, &mut state, false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(state.node_index().unwrap(), client_idx);
|
||||
controller
|
||||
.state
|
||||
.registration_state_mut(epoch)?
|
||||
.assigned_index = None;
|
||||
controller.public_key_submission(epoch, false).await?;
|
||||
assert_eq!(
|
||||
controller
|
||||
.state
|
||||
.registration_state(epoch)?
|
||||
.assigned_index
|
||||
.unwrap(),
|
||||
client_idx
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,402 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::complaints::ComplaintReason;
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::keypair::KeyPair as CoconutKeyPair;
|
||||
use cosmwasm_std::Addr;
|
||||
use log::debug;
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::types::EpochState;
|
||||
use nym_dkg::bte::{keys::KeyPair as DkgKeyPair, PublicKey, PublicKeyWithProof};
|
||||
use nym_dkg::{NodeIndex, RecoveredVerificationKeys, Threshold};
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use url::Url;
|
||||
|
||||
fn bte_pk_serialize<S: Serializer>(
|
||||
val: &PublicKeyWithProof,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
val.to_bytes().serialize(serializer)
|
||||
}
|
||||
|
||||
fn bte_pk_deserialize<'de, D>(deserializer: D) -> Result<PublicKeyWithProof, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let vec: Vec<u8> = Deserialize::deserialize(deserializer)?;
|
||||
PublicKeyWithProof::try_from_bytes(&vec).map_err(|err| Error::custom(format_args!("{:?}", err)))
|
||||
}
|
||||
|
||||
// note: each dealer is also a receiver which simplifies some logic significantly
|
||||
#[derive(Clone, Deserialize, Debug, Serialize)]
|
||||
pub(crate) struct DkgParticipant {
|
||||
pub(crate) _address: Addr,
|
||||
#[serde(serialize_with = "bte_pk_serialize")]
|
||||
#[serde(deserialize_with = "bte_pk_deserialize")]
|
||||
pub(crate) bte_public_key_with_proof: PublicKeyWithProof,
|
||||
pub(crate) assigned_index: NodeIndex,
|
||||
}
|
||||
|
||||
impl TryFrom<DealerDetails> for DkgParticipant {
|
||||
type Error = ComplaintReason;
|
||||
|
||||
fn try_from(dealer: DealerDetails) -> Result<Self, Self::Error> {
|
||||
let bte_public_key_with_proof = bs58::decode(dealer.bte_public_key_with_proof)
|
||||
.into_vec()
|
||||
.map(|bytes| PublicKeyWithProof::try_from_bytes(&bytes))
|
||||
.map_err(|_| ComplaintReason::MalformedBTEPublicKey)?
|
||||
.map_err(|_| ComplaintReason::MalformedBTEPublicKey)?;
|
||||
|
||||
if !bte_public_key_with_proof.verify() {
|
||||
return Err(ComplaintReason::InvalidBTEPublicKey);
|
||||
}
|
||||
|
||||
Ok(DkgParticipant {
|
||||
_address: dealer.address,
|
||||
bte_public_key_with_proof,
|
||||
assigned_index: dealer.assigned_index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub(crate) trait ConsistentState {
|
||||
fn node_index_value(&self) -> Result<NodeIndex, CoconutError>;
|
||||
fn receiver_index_value(&self) -> Result<usize, CoconutError>;
|
||||
fn threshold(&self) -> Result<Threshold, CoconutError>;
|
||||
async fn coconut_keypair_is_some(&self) -> Result<(), CoconutError>;
|
||||
fn proposal_id_value(&self) -> Result<u64, CoconutError>;
|
||||
async fn is_consistent(&self, epoch_state: EpochState) -> Result<(), CoconutError> {
|
||||
match epoch_state {
|
||||
EpochState::PublicKeySubmission { .. } => {}
|
||||
EpochState::DealingExchange { .. } => {
|
||||
self.node_index_value()?;
|
||||
}
|
||||
EpochState::VerificationKeySubmission { .. } => {
|
||||
self.receiver_index_value()?;
|
||||
self.threshold()?;
|
||||
}
|
||||
EpochState::VerificationKeyValidation { .. } => {
|
||||
self.coconut_keypair_is_some().await?;
|
||||
}
|
||||
EpochState::VerificationKeyFinalization { .. } => {
|
||||
self.proposal_id_value()?;
|
||||
}
|
||||
EpochState::InProgress => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ConsistentState for State {
|
||||
fn node_index_value(&self) -> Result<NodeIndex, CoconutError> {
|
||||
self.node_index.ok_or(CoconutError::UnrecoverableState {
|
||||
reason: String::from("Node index should have been set"),
|
||||
})
|
||||
}
|
||||
|
||||
fn receiver_index_value(&self) -> Result<usize, CoconutError> {
|
||||
self.receiver_index.ok_or(CoconutError::UnrecoverableState {
|
||||
reason: String::from("Receiver index should have been set"),
|
||||
})
|
||||
}
|
||||
|
||||
fn threshold(&self) -> Result<Threshold, CoconutError> {
|
||||
let threshold = self.threshold.ok_or(CoconutError::UnrecoverableState {
|
||||
reason: String::from("Threshold should have been set"),
|
||||
})?;
|
||||
if self.current_dealers_by_idx().len() < threshold as usize {
|
||||
Err(CoconutError::UnrecoverableState {
|
||||
reason: String::from(
|
||||
"Not enough good dealers in the signer set to achieve threshold",
|
||||
),
|
||||
})
|
||||
} else {
|
||||
Ok(threshold)
|
||||
}
|
||||
}
|
||||
|
||||
async fn coconut_keypair_is_some(&self) -> Result<(), CoconutError> {
|
||||
if self.coconut_keypair_is_some().await {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(CoconutError::UnrecoverableState {
|
||||
reason: String::from("Coconut keypair should have been set"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn proposal_id_value(&self) -> Result<u64, CoconutError> {
|
||||
self.proposal_id.ok_or(CoconutError::UnrecoverableState {
|
||||
reason: String::from("Proposal id should have been set"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn vks_serialize<S: Serializer>(
|
||||
val: &[RecoveredVerificationKeys],
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let vec: Vec<Vec<u8>> = val.iter().map(|vk| vk.to_bytes()).collect();
|
||||
vec.serialize(serializer)
|
||||
}
|
||||
|
||||
fn vks_deserialize<'de, D>(deserializer: D) -> Result<Vec<RecoveredVerificationKeys>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let vec: Vec<Vec<u8>> = Deserialize::deserialize(deserializer)?;
|
||||
vec.into_iter()
|
||||
.map(|b| {
|
||||
RecoveredVerificationKeys::try_from_bytes(&b)
|
||||
.map_err(|err| D::Error::custom(format_args!("{:?}", err)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize)]
|
||||
pub(crate) struct PersistentState {
|
||||
node_index: Option<NodeIndex>,
|
||||
dealers: BTreeMap<Addr, Result<DkgParticipant, ComplaintReason>>,
|
||||
receiver_index: Option<usize>,
|
||||
threshold: Option<Threshold>,
|
||||
#[serde(serialize_with = "vks_serialize")]
|
||||
#[serde(deserialize_with = "vks_deserialize")]
|
||||
recovered_vks: Vec<RecoveredVerificationKeys>,
|
||||
proposal_id: Option<u64>,
|
||||
voted_vks: bool,
|
||||
executed_proposal: bool,
|
||||
was_in_progress: bool,
|
||||
}
|
||||
|
||||
impl From<&State> for PersistentState {
|
||||
fn from(s: &State) -> Self {
|
||||
PersistentState {
|
||||
node_index: s.node_index,
|
||||
dealers: s.dealers.clone(),
|
||||
receiver_index: s.receiver_index,
|
||||
threshold: s.threshold,
|
||||
recovered_vks: s.recovered_vks.clone(),
|
||||
proposal_id: s.proposal_id,
|
||||
voted_vks: s.voted_vks,
|
||||
executed_proposal: s.executed_proposal,
|
||||
was_in_progress: s.was_in_progress,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PersistentState {
|
||||
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), CoconutError> {
|
||||
std::fs::write(path, serde_json::to_string(self)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, CoconutError> {
|
||||
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct State {
|
||||
persistent_state_path: PathBuf,
|
||||
announce_address: Url,
|
||||
dkg_keypair: DkgKeyPair,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
node_index: Option<NodeIndex>,
|
||||
dealers: BTreeMap<Addr, Result<DkgParticipant, ComplaintReason>>,
|
||||
receiver_index: Option<usize>,
|
||||
threshold: Option<Threshold>,
|
||||
recovered_vks: Vec<RecoveredVerificationKeys>,
|
||||
proposal_id: Option<u64>,
|
||||
voted_vks: bool,
|
||||
executed_proposal: bool,
|
||||
was_in_progress: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(
|
||||
persistent_state_path: PathBuf,
|
||||
persistent_state: PersistentState,
|
||||
announce_address: Url,
|
||||
dkg_keypair: DkgKeyPair,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
) -> Self {
|
||||
State {
|
||||
persistent_state_path,
|
||||
announce_address,
|
||||
dkg_keypair,
|
||||
coconut_keypair,
|
||||
node_index: persistent_state.node_index,
|
||||
dealers: persistent_state.dealers,
|
||||
receiver_index: persistent_state.receiver_index,
|
||||
threshold: persistent_state.threshold,
|
||||
recovered_vks: persistent_state.recovered_vks,
|
||||
proposal_id: persistent_state.proposal_id,
|
||||
voted_vks: persistent_state.voted_vks,
|
||||
executed_proposal: persistent_state.executed_proposal,
|
||||
was_in_progress: persistent_state.was_in_progress,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn reset_persistent(&mut self, reset_coconut_keypair: bool) {
|
||||
if reset_coconut_keypair {
|
||||
self.coconut_keypair.set(None).await;
|
||||
}
|
||||
self.node_index = Default::default();
|
||||
self.dealers = Default::default();
|
||||
self.receiver_index = Default::default();
|
||||
self.threshold = Default::default();
|
||||
self.recovered_vks = Default::default();
|
||||
self.proposal_id = Default::default();
|
||||
self.voted_vks = Default::default();
|
||||
self.executed_proposal = Default::default();
|
||||
self.was_in_progress = Default::default();
|
||||
}
|
||||
|
||||
pub fn persistent_state_path(&self) -> PathBuf {
|
||||
self.persistent_state_path.clone()
|
||||
}
|
||||
|
||||
pub fn announce_address(&self) -> &Url {
|
||||
&self.announce_address
|
||||
}
|
||||
|
||||
pub fn dkg_keypair(&self) -> &DkgKeyPair {
|
||||
&self.dkg_keypair
|
||||
}
|
||||
|
||||
pub async fn coconut_keypair_is_some(&self) -> bool {
|
||||
self.coconut_keypair.get().await.is_some()
|
||||
}
|
||||
|
||||
pub async fn take_coconut_keypair(&self) -> Option<nym_coconut::KeyPair> {
|
||||
self.coconut_keypair.take().await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn coconut_keypair(
|
||||
&self,
|
||||
) -> tokio::sync::RwLockReadGuard<'_, Option<nym_coconut::KeyPair>> {
|
||||
self.coconut_keypair.get().await
|
||||
}
|
||||
|
||||
pub fn node_index(&self) -> Option<NodeIndex> {
|
||||
self.node_index
|
||||
}
|
||||
|
||||
pub fn receiver_index(&self) -> Option<usize> {
|
||||
self.receiver_index
|
||||
}
|
||||
|
||||
pub fn current_dealers_by_addr(&self) -> BTreeMap<Addr, NodeIndex> {
|
||||
self.dealers
|
||||
.iter()
|
||||
.filter_map(|(addr, dealer)| {
|
||||
dealer
|
||||
.as_ref()
|
||||
.ok()
|
||||
.map(|participant| (addr.clone(), participant.assigned_index))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn current_dealers_by_idx(&self) -> BTreeMap<NodeIndex, PublicKey> {
|
||||
self.dealers
|
||||
.iter()
|
||||
.filter_map(|(_, dealer)| {
|
||||
dealer.as_ref().ok().map(|participant| {
|
||||
(
|
||||
participant.assigned_index,
|
||||
*participant.bte_public_key_with_proof.public_key(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn recovered_vks(&self) -> &Vec<RecoveredVerificationKeys> {
|
||||
&self.recovered_vks
|
||||
}
|
||||
|
||||
pub fn voted_vks(&self) -> bool {
|
||||
self.voted_vks
|
||||
}
|
||||
|
||||
pub fn executed_proposal(&self) -> bool {
|
||||
self.executed_proposal
|
||||
}
|
||||
|
||||
pub fn was_in_progress(&self) -> bool {
|
||||
self.was_in_progress
|
||||
}
|
||||
|
||||
pub fn set_recovered_vks(&mut self, recovered_vks: Vec<RecoveredVerificationKeys>) {
|
||||
self.recovered_vks = recovered_vks;
|
||||
}
|
||||
|
||||
pub async fn set_coconut_keypair(
|
||||
&mut self,
|
||||
coconut_keypair: Option<nym_coconut_interface::KeyPair>,
|
||||
) {
|
||||
self.coconut_keypair.set(coconut_keypair).await
|
||||
}
|
||||
|
||||
pub fn set_node_index(&mut self, node_index: Option<NodeIndex>) {
|
||||
self.node_index = node_index;
|
||||
}
|
||||
|
||||
pub fn set_dealers(&mut self, dealers: Vec<DealerDetails>) {
|
||||
self.dealers = BTreeMap::from_iter(
|
||||
dealers
|
||||
.into_iter()
|
||||
.map(|details| (details.address.clone(), DkgParticipant::try_from(details))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn mark_bad_dealer(&mut self, dealer_addr: &Addr, reason: ComplaintReason) {
|
||||
if let Some((_, value)) = self
|
||||
.dealers
|
||||
.iter_mut()
|
||||
.find(|(addr, _)| *addr == dealer_addr)
|
||||
{
|
||||
debug!(
|
||||
"Dealer {} misbehaved: {:?}. It will be marked locally as bad dealer and ignored",
|
||||
dealer_addr, reason
|
||||
);
|
||||
*value = Err(reason);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_receiver_index(&mut self, receiver_index: Option<usize>) {
|
||||
self.receiver_index = receiver_index;
|
||||
}
|
||||
|
||||
pub fn set_threshold(&mut self, threshold: Option<Threshold>) {
|
||||
self.threshold = threshold;
|
||||
}
|
||||
|
||||
pub fn set_proposal_id(&mut self, proposal_id: u64) {
|
||||
self.proposal_id = Some(proposal_id);
|
||||
}
|
||||
|
||||
pub fn set_voted_vks(&mut self) {
|
||||
self.voted_vks = true;
|
||||
}
|
||||
|
||||
pub fn set_executed_proposal(&mut self) {
|
||||
self.executed_proposal = true;
|
||||
}
|
||||
|
||||
pub fn set_was_in_progress(&mut self) {
|
||||
self.was_in_progress = true;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn all_dealers(&self) -> &BTreeMap<Addr, Result<DkgParticipant, ComplaintReason>> {
|
||||
&self.dealers
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::serde_helpers::generated_dealings;
|
||||
use crate::coconut::dkg::state::DkgParticipant;
|
||||
use nym_coconut_dkg_common::types::DealingIndex;
|
||||
use nym_dkg::{Dealing, NodeIndex};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct DealingExchangeState {
|
||||
pub(crate) dealers: BTreeMap<NodeIndex, DkgParticipant>,
|
||||
|
||||
#[serde(with = "generated_dealings")]
|
||||
pub(crate) generated_dealings: HashMap<DealingIndex, Dealing>,
|
||||
|
||||
pub(crate) receiver_index: Option<usize>,
|
||||
|
||||
pub(crate) completed: bool,
|
||||
}
|
||||
|
||||
impl DealingExchangeState {
|
||||
/// Specifies whether this dealer has already shared dealings in this DKG epoch
|
||||
pub fn completed(&self) -> bool {
|
||||
self.completed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct InProgressState {
|
||||
// indicate whether this node has been in this state before and performed any one-off tasks
|
||||
pub(crate) entered: bool,
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use super::serde_helpers::recovered_keys;
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut_dkg_common::types::{DealingIndex, EpochId};
|
||||
use nym_dkg::{G2Projective, NodeIndex, RecoveredVerificationKeys, Threshold};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use thiserror::Error;
|
||||
|
||||
type ReceiverIndex = usize;
|
||||
|
||||
#[derive(Debug, Clone, Error, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DerivationFailure {
|
||||
#[error("there were no valid dealings in epoch {epoch_id}")]
|
||||
NoValidDealings { epoch_id: EpochId },
|
||||
|
||||
#[error("did not receive sufficient number of dealings for key derivation. got {available} per key fragment whislt the threshold is {threshold}")]
|
||||
InsufficientNumberOfDealings {
|
||||
available: usize,
|
||||
threshold: Threshold,
|
||||
},
|
||||
|
||||
#[error("could not recover partial verification keys for index {dealing_index}: {err_msg}")]
|
||||
KeyRecoveryFailure {
|
||||
dealing_index: DealingIndex,
|
||||
err_msg: String,
|
||||
},
|
||||
|
||||
#[error("could not decrypt share at index {dealing_index} generated by dealer at index {dealer_index}: {err_msg}")]
|
||||
ShareDecryptionFailure {
|
||||
dealing_index: DealingIndex,
|
||||
dealer_index: NodeIndex,
|
||||
err_msg: String,
|
||||
},
|
||||
|
||||
#[error("the derived verification key does not match the expected partial elements")]
|
||||
MismatchedPartialKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DealerRejectionReason {
|
||||
#[error("no dealings were provided")]
|
||||
NoDealingsProvided,
|
||||
|
||||
#[error("insufficient number of dealings was provided. got {got} but expected {expected}")]
|
||||
InsufficientNumberOfDealingsProvided { got: usize, expected: usize },
|
||||
|
||||
#[error("no [verified] verification key from the previous epoch was available")]
|
||||
MissingVerifiedLastEpochKey,
|
||||
|
||||
#[error("the key size from the previous epoch does not match the resharing dealing requirements: {key_size} vs {expected}")]
|
||||
LastEpochKeyOfWrongSize { key_size: usize, expected: usize },
|
||||
|
||||
#[error("the dealing at index {index} is malformed: {err_msg}")]
|
||||
MalformedDealing {
|
||||
index: DealingIndex,
|
||||
err_msg: String,
|
||||
},
|
||||
|
||||
#[error("the dealing at index {index} is [cryptographically] valid: {err_msg}")]
|
||||
InvalidDealing {
|
||||
index: DealingIndex,
|
||||
err_msg: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct KeyDerivationState {
|
||||
pub(crate) expected_threshold: Option<Threshold>,
|
||||
|
||||
#[serde(with = "recovered_keys")]
|
||||
pub(crate) derived_partials: BTreeMap<DealingIndex, RecoveredVerificationKeys>,
|
||||
|
||||
pub(crate) rejected_dealers: HashMap<Addr, DealerRejectionReason>,
|
||||
|
||||
pub(crate) proposal_id: Option<u64>,
|
||||
|
||||
pub(crate) completed: Option<Result<(), DerivationFailure>>,
|
||||
}
|
||||
|
||||
impl KeyDerivationState {
|
||||
pub fn derived_partials_for(&self, receiver_index: ReceiverIndex) -> Option<Vec<G2Projective>> {
|
||||
let mut recovered = Vec::new();
|
||||
for keys in self.derived_partials.values() {
|
||||
// SAFETY:
|
||||
// make sure the receiver index of this receiver/dealer is within the size of the derived keys
|
||||
if keys.recovered_partials.len() <= receiver_index {
|
||||
return None;
|
||||
};
|
||||
recovered.push(keys.recovered_partials[receiver_index])
|
||||
}
|
||||
Some(recovered)
|
||||
}
|
||||
|
||||
pub fn completed_with_success(&self) -> bool {
|
||||
matches!(self.completed, Some(Ok(_)))
|
||||
}
|
||||
|
||||
pub fn completion_failure(&self) -> Option<DerivationFailure> {
|
||||
self.completed.clone().and_then(Result::err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct FinalizationState {
|
||||
pub(crate) completed: bool,
|
||||
}
|
||||
|
||||
impl FinalizationState {
|
||||
/// Specifies whether this (or another) dealer has already executed its verification proposal
|
||||
pub fn completed(&self) -> bool {
|
||||
self.completed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
type ProposalId = u64;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct ValidationState {
|
||||
pub votes: HashMap<ProposalId, bool>,
|
||||
|
||||
pub completed: bool,
|
||||
}
|
||||
|
||||
impl ValidationState {
|
||||
/// Specifies whether this dealer has already registered in the particular DKG epoch
|
||||
pub fn completed(&self) -> bool {
|
||||
self.completed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::state::dealing_exchange::DealingExchangeState;
|
||||
use crate::coconut::dkg::state::in_progress::InProgressState;
|
||||
use crate::coconut::dkg::state::key_derivation::KeyDerivationState;
|
||||
use crate::coconut::dkg::state::key_finalization::FinalizationState;
|
||||
use crate::coconut::dkg::state::key_validation::ValidationState;
|
||||
use crate::coconut::dkg::state::registration::{
|
||||
DkgParticipant, ParticipantState, RegistrationState,
|
||||
};
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch};
|
||||
use cosmwasm_std::Addr;
|
||||
use log::debug;
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use nym_dkg::{bte, NodeIndex, Threshold};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::{Path, PathBuf};
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
pub(crate) mod dealing_exchange;
|
||||
pub(crate) mod in_progress;
|
||||
pub(crate) mod key_derivation;
|
||||
pub(crate) mod key_finalization;
|
||||
pub(crate) mod key_validation;
|
||||
pub(crate) mod registration;
|
||||
pub(crate) mod serde_helpers;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub(crate) struct PersistentState {
|
||||
timestamp: OffsetDateTime,
|
||||
|
||||
dkg_instances: HashMap<EpochId, DkgState>,
|
||||
}
|
||||
|
||||
impl Default for PersistentState {
|
||||
fn default() -> Self {
|
||||
PersistentState {
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
|
||||
dkg_instances: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&State> for PersistentState {
|
||||
fn from(s: &State) -> Self {
|
||||
PersistentState {
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
|
||||
dkg_instances: s.dkg_instances.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PersistentState {
|
||||
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), CoconutError> {
|
||||
debug!("persisting the dkg state");
|
||||
std::fs::write(path, serde_json::to_string(self)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, CoconutError> {
|
||||
Ok(serde_json::from_str(&std::fs::read_to_string(path)?)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct DkgState {
|
||||
pub(crate) registration: RegistrationState,
|
||||
|
||||
pub(crate) dealing_exchange: DealingExchangeState,
|
||||
|
||||
pub(crate) key_generation: KeyDerivationState,
|
||||
|
||||
pub(crate) key_validation: ValidationState,
|
||||
|
||||
pub(crate) key_finalization: FinalizationState,
|
||||
|
||||
pub(crate) in_progress: InProgressState,
|
||||
}
|
||||
|
||||
impl DkgState {
|
||||
pub(crate) fn set_dealers(&mut self, raw_dealers: Vec<DealerDetails>) {
|
||||
assert!(self.dealing_exchange.dealers.is_empty());
|
||||
for raw_dealer in raw_dealers {
|
||||
let dkg_participant = DkgParticipant::from(raw_dealer);
|
||||
let address = &dkg_participant.address;
|
||||
if let ParticipantState::Invalid(rejection) = &dkg_participant.state {
|
||||
warn!("{address} dealer is malformed: {rejection}",)
|
||||
}
|
||||
self.dealing_exchange
|
||||
.dealers
|
||||
.insert(dkg_participant.assigned_index, dkg_participant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct State {
|
||||
/// Path to the file containing the persistent state
|
||||
persistent_state_path: PathBuf,
|
||||
|
||||
dkg_instances: HashMap<EpochId, DkgState>,
|
||||
|
||||
announce_address: Url,
|
||||
|
||||
identity_key: identity::PublicKey,
|
||||
|
||||
dkg_keypair: DkgKeyPair,
|
||||
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(
|
||||
persistent_state_path: PathBuf,
|
||||
persistent_state: PersistentState,
|
||||
announce_address: Url,
|
||||
dkg_keypair: DkgKeyPair,
|
||||
identity_key: identity::PublicKey,
|
||||
coconut_keypair: CoconutKeyPair,
|
||||
) -> Self {
|
||||
State {
|
||||
persistent_state_path,
|
||||
dkg_instances: persistent_state.dkg_instances,
|
||||
announce_address,
|
||||
identity_key,
|
||||
dkg_keypair,
|
||||
coconut_keypair,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn persist(&self) -> Result<(), CoconutError> {
|
||||
PersistentState::from(self).save_to_file(self.persistent_state_path())
|
||||
}
|
||||
|
||||
pub fn clear_previous_epoch(&mut self, current_epoch: EpochId) {
|
||||
if let Some(previous) = current_epoch.checked_sub(1) {
|
||||
self.dkg_instances.remove(&previous);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_init_dkg_state(&mut self, epoch_id: EpochId) {
|
||||
// given we're not using that entry here, I think the explicit check and insert is more readable
|
||||
#[allow(clippy::map_entry)]
|
||||
if !self.dkg_instances.contains_key(&epoch_id) {
|
||||
self.dkg_instances.insert(epoch_id, Default::default());
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtain the list of dealers for the provided epoch that have submitted valid public keys.
|
||||
pub fn valid_epoch_receivers(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Vec<(Addr, NodeIndex)>, CoconutError> {
|
||||
Ok(self
|
||||
.dealing_exchange_state(epoch_id)?
|
||||
.dealers
|
||||
.values()
|
||||
.filter_map(|d| {
|
||||
if d.state.is_valid() {
|
||||
Some((d.address.clone(), d.assigned_index))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Filters out DKG participants based on whether they submitted valid public key
|
||||
pub fn valid_epoch_receivers_keys(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<BTreeMap<NodeIndex, bte::PublicKey>, CoconutError> {
|
||||
Ok(self
|
||||
.dealing_exchange_state(epoch_id)?
|
||||
.dealers
|
||||
.values()
|
||||
.filter_map(|d| d.state.public_key().map(|k| (d.assigned_index, k)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn dkg_state(&self, epoch_id: EpochId) -> Result<&DkgState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn dkg_state_mut(&mut self, epoch_id: EpochId) -> Result<&mut DkgState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn registration_state(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&RegistrationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.map(|state| &state.registration)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn registration_state_mut(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&mut RegistrationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.map(|state| &mut state.registration)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn in_progress_state(&self, epoch_id: EpochId) -> Result<&InProgressState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.map(|state| &state.in_progress)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn in_progress_state_mut(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&mut InProgressState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.map(|state| &mut state.in_progress)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn dealing_exchange_state(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&DealingExchangeState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.map(|state| &state.dealing_exchange)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn dealing_exchange_state_mut(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&mut DealingExchangeState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.map(|state| &mut state.dealing_exchange)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn key_derivation_state(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&KeyDerivationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.map(|state| &state.key_generation)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn key_derivation_state_mut(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&mut KeyDerivationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.map(|state| &mut state.key_generation)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn key_validation_state(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&ValidationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.map(|state| &state.key_validation)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn key_validation_state_mut(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&mut ValidationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.map(|state| &mut state.key_validation)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn key_finalization_state(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&FinalizationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get(&epoch_id)
|
||||
.map(|state| &state.key_finalization)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn key_finalization_state_mut(
|
||||
&mut self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<&mut FinalizationState, CoconutError> {
|
||||
self.dkg_instances
|
||||
.get_mut(&epoch_id)
|
||||
.map(|state| &mut state.key_finalization)
|
||||
.ok_or(CoconutError::MissingDkgState { epoch_id })
|
||||
}
|
||||
|
||||
pub fn threshold(&self, epoch_id: EpochId) -> Result<Threshold, CoconutError> {
|
||||
self.key_derivation_state(epoch_id)?
|
||||
.expected_threshold
|
||||
.ok_or(CoconutError::UnavailableThreshold { epoch_id })
|
||||
}
|
||||
|
||||
pub fn assigned_index(&self, epoch_id: EpochId) -> Result<NodeIndex, CoconutError> {
|
||||
self.registration_state(epoch_id)?
|
||||
.assigned_index
|
||||
.ok_or(CoconutError::UnavailableAssignedIndex { epoch_id })
|
||||
}
|
||||
|
||||
pub fn receiver_index(&self, epoch_id: EpochId) -> Result<usize, CoconutError> {
|
||||
self.dealing_exchange_state(epoch_id)?
|
||||
.receiver_index
|
||||
.ok_or(CoconutError::UnavailableReceiverIndex { epoch_id })
|
||||
}
|
||||
|
||||
pub fn proposal_id(&self, epoch_id: EpochId) -> Result<u64, CoconutError> {
|
||||
self.key_derivation_state(epoch_id)?
|
||||
.proposal_id
|
||||
.ok_or(CoconutError::UnavailableProposalId { epoch_id })
|
||||
}
|
||||
|
||||
pub fn persistent_state_path(&self) -> &Path {
|
||||
self.persistent_state_path.as_path()
|
||||
}
|
||||
|
||||
pub fn announce_address(&self) -> &Url {
|
||||
&self.announce_address
|
||||
}
|
||||
|
||||
pub fn identity_key(&self) -> identity::PublicKey {
|
||||
self.identity_key
|
||||
}
|
||||
|
||||
pub fn dkg_keypair(&self) -> &DkgKeyPair {
|
||||
&self.dkg_keypair
|
||||
}
|
||||
|
||||
pub async fn coconut_keypair_is_some(&self) -> bool {
|
||||
self.coconut_keypair.get().await.is_some()
|
||||
}
|
||||
|
||||
pub async fn take_coconut_keypair(&self) -> Option<KeyPairWithEpoch> {
|
||||
self.coconut_keypair.take().await
|
||||
}
|
||||
|
||||
pub fn invalidate_coconut_keypair(&self) {
|
||||
self.coconut_keypair.invalidate()
|
||||
}
|
||||
|
||||
pub fn validate_coconut_keypair(&self) {
|
||||
self.coconut_keypair.validate()
|
||||
}
|
||||
|
||||
pub async fn unchecked_coconut_keypair(
|
||||
&self,
|
||||
) -> tokio::sync::RwLockReadGuard<'_, Option<KeyPairWithEpoch>> {
|
||||
self.coconut_keypair.read_keys().await
|
||||
}
|
||||
|
||||
pub async fn set_coconut_keypair(&mut self, coconut_keypair: KeyPairWithEpoch) {
|
||||
self.coconut_keypair.set(coconut_keypair).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg::state::serde_helpers::bte_pk_serde;
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::types::EncodedBTEPublicKeyWithProof;
|
||||
use nym_dkg::bte::PublicKeyWithProof;
|
||||
use nym_dkg::{bte, NodeIndex};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Deserialize, Debug, Serialize)]
|
||||
pub(crate) enum ParticipantState {
|
||||
Invalid(KeyRejectionReason),
|
||||
VerifiedKey(#[serde(with = "bte_pk_serde")] Box<PublicKeyWithProof>),
|
||||
}
|
||||
|
||||
impl ParticipantState {
|
||||
pub fn is_valid(&self) -> bool {
|
||||
matches!(self, ParticipantState::VerifiedKey(..))
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> Option<bte::PublicKey> {
|
||||
match self {
|
||||
ParticipantState::Invalid(_) => None,
|
||||
ParticipantState::VerifiedKey(key_with_proof) => Some(*key_with_proof.public_key()),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_raw_encoded_key(raw: EncodedBTEPublicKeyWithProof) -> Self {
|
||||
let bytes = match bs58::decode(raw).into_vec() {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => {
|
||||
return ParticipantState::Invalid(
|
||||
KeyRejectionReason::MalformedBTEPublicKeyEncoding {
|
||||
err_msg: err.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let key = match PublicKeyWithProof::try_from_bytes(&bytes) {
|
||||
Ok(key) => key,
|
||||
Err(err) => {
|
||||
return ParticipantState::Invalid(KeyRejectionReason::MalformedBTEPublicKey {
|
||||
err_msg: err.to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if !key.verify() {
|
||||
return ParticipantState::Invalid(KeyRejectionReason::InvalidBTEPublicKey);
|
||||
}
|
||||
|
||||
ParticipantState::VerifiedKey(Box::new(key))
|
||||
}
|
||||
}
|
||||
|
||||
// note: each dealer is also a receiver which simplifies some logic significantly
|
||||
#[derive(Clone, Deserialize, Debug, Serialize)]
|
||||
pub(crate) struct DkgParticipant {
|
||||
pub(crate) address: Addr,
|
||||
pub(crate) assigned_index: NodeIndex,
|
||||
pub(crate) state: ParticipantState,
|
||||
}
|
||||
|
||||
impl From<DealerDetails> for DkgParticipant {
|
||||
fn from(dealer: DealerDetails) -> Self {
|
||||
DkgParticipant {
|
||||
address: dealer.address,
|
||||
state: ParticipantState::from_raw_encoded_key(dealer.bte_public_key_with_proof),
|
||||
assigned_index: dealer.assigned_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DkgParticipant {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn unwrap_key(&self) -> PublicKeyWithProof {
|
||||
if let ParticipantState::VerifiedKey(key) = &self.state {
|
||||
return *key.clone();
|
||||
}
|
||||
panic!("no key")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn unwrap_rejection(&self) -> KeyRejectionReason {
|
||||
if let ParticipantState::Invalid(rejection) = &self.state {
|
||||
return rejection.clone();
|
||||
}
|
||||
panic!("not rejected")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Error, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub(crate) enum KeyRejectionReason {
|
||||
#[error("provided BTE Public key encoding is malformed: {err_msg}")]
|
||||
MalformedBTEPublicKeyEncoding { err_msg: String },
|
||||
|
||||
#[error("provided BTE Public key has invalid byte representation: {err_msg}")]
|
||||
MalformedBTEPublicKey { err_msg: String },
|
||||
|
||||
#[error("provided BTE public key does not verify correctly")]
|
||||
InvalidBTEPublicKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub struct RegistrationState {
|
||||
pub(crate) assigned_index: Option<NodeIndex>,
|
||||
}
|
||||
|
||||
impl RegistrationState {
|
||||
/// Specifies whether this dealer has already registered in the particular DKG epoch
|
||||
pub fn completed(&self) -> bool {
|
||||
self.assigned_index.is_some()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub(super) mod bte_pk_serde {
|
||||
use nym_dkg::bte::PublicKeyWithProof;
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub fn serialize<S: Serializer>(
|
||||
val: &PublicKeyWithProof,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
val.to_bytes().serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Box<PublicKeyWithProof>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let vec: Vec<u8> = Deserialize::deserialize(deserializer)?;
|
||||
PublicKeyWithProof::try_from_bytes(&vec)
|
||||
.map_err(|err| Error::custom(format_args!("{:?}", err)))
|
||||
.map(Box::new)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) mod recovered_keys {
|
||||
use nym_coconut_dkg_common::types::DealingIndex;
|
||||
use nym_dkg::RecoveredVerificationKeys;
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
type Helper = BTreeMap<DealingIndex, Vec<u8>>;
|
||||
|
||||
pub fn serialize<S: Serializer>(
|
||||
val: &BTreeMap<DealingIndex, RecoveredVerificationKeys>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let helper: Helper = val
|
||||
.iter()
|
||||
.map(|(idx, rec)| (*idx, rec.to_bytes()))
|
||||
.collect();
|
||||
helper.serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<BTreeMap<DealingIndex, RecoveredVerificationKeys>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let helper = Helper::deserialize(deserializer)?;
|
||||
helper
|
||||
.into_iter()
|
||||
.map(|(idx, rec)| {
|
||||
RecoveredVerificationKeys::try_from_bytes(&rec)
|
||||
.map_err(|err| D::Error::custom(format_args!("{:?}", err)))
|
||||
.map(|vk| (idx, vk))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) mod generated_dealings {
|
||||
use nym_coconut_dkg_common::types::DealingIndex;
|
||||
use nym_dkg::Dealing;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn serialize<S: Serializer>(
|
||||
dealings: &HashMap<DealingIndex, Dealing>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
let mut helper = HashMap::new();
|
||||
|
||||
for (dealing_index, dealing) in dealings {
|
||||
helper.insert(*dealing_index, dealing.to_bytes());
|
||||
}
|
||||
|
||||
helper.serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<HashMap<DealingIndex, Dealing>, D::Error> {
|
||||
<HashMap<DealingIndex, Vec<u8>>>::deserialize(deserializer)?
|
||||
.into_iter()
|
||||
.map(|(index, raw_dealing)| {
|
||||
Dealing::try_from_bytes(&raw_dealing)
|
||||
.map_err(serde::de::Error::custom)
|
||||
.map(|dealing| (index, dealing))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,8 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// 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::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,28 @@ 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("insufficient number of dealings provided to derive the key")]
|
||||
InsufficientDealings {
|
||||
// TODO: details
|
||||
},
|
||||
}
|
||||
|
||||
impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::error::CoconutError;
|
||||
use crate::coconut::state::BANDWIDTH_CREDENTIAL_PARAMS;
|
||||
use crate::coconut::state::bandwidth_voucher_params;
|
||||
use nym_api_requests::coconut::BlindSignRequestBody;
|
||||
use nym_coconut::{BlindedSignature, SecretKey};
|
||||
use nym_validator_client::nyxd::error::NyxdError::AbciError;
|
||||
@@ -29,7 +29,7 @@ pub(crate) fn blind_sign(
|
||||
let attributes_ref = public_attributes.iter().collect::<Vec<_>>();
|
||||
|
||||
Ok(nym_coconut_interface::blind_sign(
|
||||
&BANDWIDTH_CREDENTIAL_PARAMS,
|
||||
bandwidth_voucher_params(),
|
||||
signing_key,
|
||||
&request.inner_sign_request,
|
||||
&attributes_ref,
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeyPair {
|
||||
inner: Arc<RwLock<Option<nym_coconut_interface::KeyPair>>>,
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn take(&self) -> Option<nym_coconut::KeyPair> {
|
||||
self.inner.write().await.take()
|
||||
}
|
||||
|
||||
pub async fn get(&self) -> RwLockReadGuard<'_, Option<nym_coconut::KeyPair>> {
|
||||
self.inner.read().await
|
||||
}
|
||||
|
||||
pub async fn set(&self, keypair: Option<nym_coconut_interface::KeyPair>) {
|
||||
let mut w_lock = self.inner.write().await;
|
||||
*w_lock = keypair;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_dkg::Scalar;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
|
||||
mod persistence;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeyPair {
|
||||
// keys: Arc<RwLock<HashMap<EpochId, nym_coconut_interface::KeyPair>>>,
|
||||
keys: Arc<RwLock<Option<KeyPairWithEpoch>>>,
|
||||
valid: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KeyPairWithEpoch {
|
||||
pub(crate) keys: nym_coconut_interface::KeyPair,
|
||||
pub(crate) issued_for_epoch: EpochId,
|
||||
}
|
||||
|
||||
impl KeyPairWithEpoch {
|
||||
pub(crate) fn new(keys: nym_coconut_interface::KeyPair, issued_for_epoch: EpochId) -> Self {
|
||||
KeyPairWithEpoch {
|
||||
keys,
|
||||
issued_for_epoch,
|
||||
}
|
||||
}
|
||||
|
||||
// extract underlying secrets from the coconut's secret key.
|
||||
// the caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized
|
||||
// `KeyPair` and `SecretKey` implement ZeroizeOnDrop; `Scalar` does not (it implements `Copy` -> important to keep in mind)
|
||||
pub(crate) fn hazmat_into_secrets(self) -> Vec<Scalar> {
|
||||
let (x, mut secrets) = self.keys.secret_key().hazmat_to_raw();
|
||||
|
||||
secrets.insert(0, x);
|
||||
secrets
|
||||
// since `nym_coconut_interface::KeyPair` implements `ZeroizeOnDrop` and we took ownership of the keypair,
|
||||
// it will get zeroized after we exit this scope
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
keys: Arc::new(RwLock::new(None)),
|
||||
valid: Arc::new(Default::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn take(&self) -> Option<KeyPairWithEpoch> {
|
||||
self.keys.write().await.take()
|
||||
}
|
||||
|
||||
pub async fn get(&self) -> Option<RwLockReadGuard<'_, Option<KeyPairWithEpoch>>> {
|
||||
if self.is_valid() {
|
||||
Some(self.read_keys().await)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn read_keys(&self) -> RwLockReadGuard<'_, Option<KeyPairWithEpoch>> {
|
||||
self.keys.read().await
|
||||
}
|
||||
|
||||
pub async fn set(&self, keypair: KeyPairWithEpoch) {
|
||||
let mut w_lock = self.keys.write().await;
|
||||
*w_lock = Some(keypair);
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.valid.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn validate(&self) {
|
||||
self.valid.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn invalidate(&self) {
|
||||
self.valid.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::keys::KeyPairWithEpoch;
|
||||
use crate::coconut::state::bandwidth_voucher_params;
|
||||
use nym_coconut::{CoconutError, KeyPair, SecretKey};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_pemstore::traits::PemStorableKey;
|
||||
use std::mem;
|
||||
|
||||
impl PemStorableKey for KeyPairWithEpoch {
|
||||
// that's not the best error for this, but it felt like an overkill to define a dedicated struct just for this purpose
|
||||
type Error = CoconutError;
|
||||
|
||||
fn pem_type() -> &'static str {
|
||||
"COCONUT KEY WITH EPOCH"
|
||||
}
|
||||
|
||||
fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = self.issued_for_epoch.to_be_bytes().to_vec();
|
||||
bytes.append(&mut self.keys.secret_key().to_bytes());
|
||||
bytes
|
||||
}
|
||||
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
if bytes.len() <= mem::size_of::<EpochId>() {
|
||||
return Err(CoconutError::Deserialization(
|
||||
"insufficient number of bytes to decode secret key with epoch id".into(),
|
||||
));
|
||||
}
|
||||
let epoch_id = EpochId::from_be_bytes([
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
]);
|
||||
|
||||
let sk = SecretKey::from_bytes(&bytes[mem::size_of::<EpochId>()..])?;
|
||||
let vk = sk.verification_key(bandwidth_voucher_params());
|
||||
|
||||
Ok(KeyPairWithEpoch {
|
||||
keys: KeyPair::from_keys(sk, vk),
|
||||
issued_for_epoch: epoch_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use self::comm::APICommunicationChannel;
|
||||
use crate::coconut::client::Client as LocalClient;
|
||||
use crate::coconut::state::State;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use keypair::KeyPair;
|
||||
use keys::KeyPair;
|
||||
use nym_config::defaults::NYM_API_VERSION;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_validator_client::nym_api::routes::{BANDWIDTH, COCONUT_ROUTES};
|
||||
@@ -18,7 +18,7 @@ mod deposit;
|
||||
pub(crate) mod dkg;
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod keypair;
|
||||
pub(crate) mod keys;
|
||||
pub(crate) mod state;
|
||||
pub(crate) mod storage;
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -5,10 +5,9 @@ use crate::coconut::client::Client as LocalClient;
|
||||
use crate::coconut::comm::APICommunicationChannel;
|
||||
use crate::coconut::deposit::validate_deposit_tx;
|
||||
use crate::coconut::error::Result;
|
||||
use crate::coconut::keypair::KeyPair;
|
||||
use crate::coconut::keys::KeyPair;
|
||||
use crate::coconut::storage::CoconutStorageExt;
|
||||
use crate::support::storage::NymApiStorage;
|
||||
use lazy_static::lazy_static;
|
||||
use nym_api_requests::coconut::helpers::issued_credential_plaintext;
|
||||
use nym_api_requests::coconut::BlindSignRequestBody;
|
||||
use nym_coconut::Parameters;
|
||||
@@ -17,16 +16,16 @@ use nym_coconut_interface::{BlindedSignature, VerificationKey};
|
||||
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_validator_client::nyxd::{Hash, TxResponse};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
// keep it as a global static due to relatively high cost of computing the curve points;
|
||||
// plus we expect all clients to use the same set of parameters
|
||||
//
|
||||
// future note: once we allow for credentials with variable number of attributes, just create Parameters(max_allowed_attributes)
|
||||
// and take as many hs elements as required (since they will match for all variants)
|
||||
lazy_static! {
|
||||
pub(crate) static ref BANDWIDTH_CREDENTIAL_PARAMS: Parameters =
|
||||
BandwidthVoucher::default_parameters();
|
||||
pub(crate) fn bandwidth_voucher_params() -> &'static Parameters {
|
||||
static BANDWIDTH_CREDENTIAL_PARAMS: OnceLock<Parameters> = OnceLock::new();
|
||||
BANDWIDTH_CREDENTIAL_PARAMS.get_or_init(BandwidthVoucher::default_parameters)
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::dkg;
|
||||
use crate::coconut::dkg::client::DkgClient;
|
||||
use crate::coconut::dkg::controller::keys::persist_coconut_keypair;
|
||||
use crate::coconut::dkg::controller::DkgController;
|
||||
use crate::coconut::dkg::state::State;
|
||||
use crate::coconut::keys::KeyPair;
|
||||
use crate::coconut::tests::{DummyClient, SharedFakeChain};
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut::VerificationKey;
|
||||
use nym_coconut_dkg_common::types::{DealerDetails, EpochId, InitialReplacementData};
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
|
||||
use nym_dkg::{NodeIndex, Threshold};
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use rand_chacha::{
|
||||
rand_core::{RngCore, SeedableRng},
|
||||
ChaCha20Rng,
|
||||
};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
pub fn test_rng(seed: [u8; 32]) -> ChaCha20Rng {
|
||||
ChaCha20Rng::from_seed(seed)
|
||||
}
|
||||
|
||||
pub fn test_rng_07(seed: [u8; 32]) -> rand_chacha_02::ChaCha20Rng {
|
||||
use rand_chacha_02::rand_core::SeedableRng;
|
||||
rand_chacha_02::ChaCha20Rng::from_seed(seed)
|
||||
}
|
||||
|
||||
pub fn pseudorandom_account(rng: &mut ChaCha20Rng) -> AccountId {
|
||||
let mut dummy_account_key_hash = [0u8; 32];
|
||||
rng.fill_bytes(&mut dummy_account_key_hash);
|
||||
AccountId::new("n", &dummy_account_key_hash).unwrap()
|
||||
}
|
||||
|
||||
pub fn dealer_fixture(mut rng: &mut ChaCha20Rng, id: NodeIndex) -> DealerDetails {
|
||||
// we might possibly need that private key later on
|
||||
let keypair = DkgKeyPair::new(dkg::params(), &mut rng);
|
||||
|
||||
// lol, instantiate rng with an rng due to incompatibility, but even though it looks dodgy AF,
|
||||
// it's 100% deterministic
|
||||
let mut secondary_seed = [0u8; 32];
|
||||
rng.fill_bytes(&mut secondary_seed);
|
||||
|
||||
let addr = pseudorandom_account(rng);
|
||||
let identity_keypair = identity::KeyPair::new(&mut test_rng_07(secondary_seed));
|
||||
let bte_public_key_with_proof = bs58::encode(&keypair.public_key().to_bytes()).into_string();
|
||||
|
||||
let port = 8080 + id;
|
||||
DealerDetails {
|
||||
address: Addr::unchecked(addr.to_string()),
|
||||
bte_public_key_with_proof,
|
||||
ed25519_identity: identity_keypair.public_key().to_base58_string(),
|
||||
announce_address: format!("http://localhost:{port}"),
|
||||
assigned_index: id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dealers_fixtures(rng: &mut ChaCha20Rng, n: usize) -> Vec<DealerDetails> {
|
||||
let mut dealers = Vec::new();
|
||||
for i in 1..=n {
|
||||
dealers.push(dealer_fixture(rng, i as NodeIndex))
|
||||
}
|
||||
dealers
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TestingDkgControllerBuilder {
|
||||
rng: Option<ChaCha20Rng>,
|
||||
rng_seed: Option<[u8; 32]>,
|
||||
address: Option<AccountId>,
|
||||
keypair: Option<KeyPair>,
|
||||
|
||||
chain_state: Option<SharedFakeChain>,
|
||||
|
||||
epoch_id: Option<EpochId>,
|
||||
threshold: Option<Threshold>,
|
||||
self_dealer: Option<DealerDetails>,
|
||||
dealers: Vec<DealerDetails>,
|
||||
initial_dealers: Option<InitialReplacementData>,
|
||||
}
|
||||
|
||||
impl TestingDkgControllerBuilder {
|
||||
pub fn with_magic_seed_val(mut self, val: u8) -> Self {
|
||||
self.rng_seed = Some([val; 32]);
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_rng(mut self, rng: ChaCha20Rng) -> Self {
|
||||
self.rng = Some(rng);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_initial_epoch_id(mut self, initial: EpochId) -> Self {
|
||||
self.epoch_id = Some(initial);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_keypair(mut self, keypair: KeyPair) -> Self {
|
||||
self.keypair = Some(keypair);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_shared_chain_state(mut self, fake_chain: SharedFakeChain) -> Self {
|
||||
self.chain_state = Some(fake_chain);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_as_dealer(mut self, dealer_details: DealerDetails) -> Self {
|
||||
self.self_dealer = Some(dealer_details);
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_dealer(mut self, dealer_details: DealerDetails) -> Self {
|
||||
self.dealers.push(dealer_details);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_dealers(mut self, dealers_details: Vec<DealerDetails>) -> Self {
|
||||
self.dealers = dealers_details;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_initial_dealers(mut self, initial_dealers: InitialReplacementData) -> Self {
|
||||
self.initial_dealers = Some(initial_dealers);
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_address(mut self, address: impl Into<String>) -> Self {
|
||||
let addr = address.into();
|
||||
self.address = Some(addr.parse().unwrap());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_threshold(mut self, threshold: Threshold) -> Self {
|
||||
self.threshold = Some(threshold);
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn build(self) -> TestingDkgController {
|
||||
let mut rng = self.rng.unwrap_or_else(|| {
|
||||
let rng_seed = self.rng_seed.unwrap_or([69u8; 32]);
|
||||
test_rng(rng_seed)
|
||||
});
|
||||
|
||||
let had_dealer_info = self.self_dealer.is_some();
|
||||
// let had_keypair = self.keypair.is_some();
|
||||
|
||||
// is this ideal? no, but it works : P
|
||||
let self_dealer = self.self_dealer.unwrap_or_else(|| {
|
||||
let address = self
|
||||
.address
|
||||
.unwrap_or_else(|| pseudorandom_account(&mut rng));
|
||||
let mut secondary_seed = [0u8; 32];
|
||||
rng.fill_bytes(&mut secondary_seed);
|
||||
|
||||
let identity_keypair = identity::KeyPair::new(&mut test_rng_07(secondary_seed));
|
||||
|
||||
DealerDetails {
|
||||
address: Addr::unchecked(address.as_ref()),
|
||||
bte_public_key_with_proof: "foomp".to_string(),
|
||||
ed25519_identity: identity_keypair.public_key().to_base58_string(),
|
||||
announce_address: "http://localhost:8080".to_string(),
|
||||
assigned_index: 1,
|
||||
}
|
||||
});
|
||||
|
||||
let chain_state = self.chain_state.unwrap_or_default();
|
||||
let dummy_client = DummyClient::new(
|
||||
self_dealer.address.to_string().parse().unwrap(),
|
||||
chain_state.clone(),
|
||||
);
|
||||
|
||||
// insert initial data into the chain state
|
||||
{
|
||||
let mut state_guard = chain_state.lock().unwrap();
|
||||
if let Some(threshold) = self.threshold {
|
||||
state_guard.dkg_contract.threshold = Some(threshold)
|
||||
}
|
||||
for dealer in self.dealers {
|
||||
state_guard
|
||||
.dkg_contract
|
||||
.dealers
|
||||
.insert(dealer.assigned_index, dealer);
|
||||
}
|
||||
if let Some(epoch_id) = self.epoch_id {
|
||||
state_guard.dkg_contract.epoch.epoch_id = epoch_id;
|
||||
}
|
||||
if let Some(initial_dealers) = self.initial_dealers {
|
||||
state_guard.dkg_contract.initial_dealers = Some(initial_dealers)
|
||||
}
|
||||
}
|
||||
|
||||
let dummy_client = DkgClient::new(dummy_client);
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
|
||||
let dkg_state_path = tmp_dir.path().join("persistent_state.json");
|
||||
let coconut_key_path = tmp_dir.path().join("coconut_keypair.pem");
|
||||
|
||||
// if we had a keypair, make sure to put it on disk otherwise, if we're testing dealing exchange,
|
||||
// we'll fail to archive it
|
||||
let keypair = if let Some(keypair) = self.keypair {
|
||||
if let Some(keys) = keypair.read_keys().await.as_ref() {
|
||||
persist_coconut_keypair(keys, &coconut_key_path).unwrap();
|
||||
}
|
||||
keypair
|
||||
} else {
|
||||
KeyPair::new()
|
||||
};
|
||||
|
||||
let mut state = State::new(
|
||||
dkg_state_path,
|
||||
Default::default(),
|
||||
self_dealer.announce_address.parse().unwrap(),
|
||||
// TODO: we might need to fix up the key here
|
||||
DkgKeyPair::new(&nym_dkg::bte::setup(), &mut rng),
|
||||
self_dealer.ed25519_identity.parse().unwrap(),
|
||||
keypair,
|
||||
);
|
||||
|
||||
let epoch = chain_state.lock().unwrap().dkg_contract.epoch.epoch_id;
|
||||
if had_dealer_info {
|
||||
// if we had dealer info it means we must have gone through key registration
|
||||
state.maybe_init_dkg_state(epoch);
|
||||
state.registration_state_mut(epoch).unwrap().assigned_index =
|
||||
Some(self_dealer.assigned_index);
|
||||
}
|
||||
|
||||
// if had_keypair {
|
||||
// // if we had keypair, it means we must have gone through dealing exchange
|
||||
// state.dealing_exchange_state(epoch).unwrap();
|
||||
// }
|
||||
|
||||
TestingDkgController {
|
||||
controller: DkgController::test_mock(rng, dummy_client, state, coconut_key_path),
|
||||
chain_state,
|
||||
_tmp_dir: tmp_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn dkg_controller_fixture() -> TestingDkgController {
|
||||
TestingDkgControllerBuilder::default().build().await
|
||||
}
|
||||
|
||||
pub(crate) struct TestingDkgController {
|
||||
pub(crate) controller: DkgController<ChaCha20Rng>,
|
||||
|
||||
pub(crate) chain_state: SharedFakeChain,
|
||||
|
||||
_tmp_dir: TempDir,
|
||||
}
|
||||
|
||||
impl TestingDkgController {
|
||||
pub async fn address(&self) -> AccountId {
|
||||
self.dkg_client.get_address().await
|
||||
}
|
||||
|
||||
pub async fn cw_address(&self) -> Addr {
|
||||
Addr::unchecked(self.address().await.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) async fn unchecked_coconut_vk(&self) -> VerificationKey {
|
||||
self.state
|
||||
.unchecked_coconut_keypair()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.keys
|
||||
.verification_key()
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TestingDkgController {
|
||||
type Target = DkgController<ChaCha20Rng>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.controller
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for TestingDkgController {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.controller
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::coconut::tests::fixtures::{TestingDkgController, TestingDkgControllerBuilder};
|
||||
use crate::coconut::tests::SharedFakeChain;
|
||||
use nym_coconut_dkg_common::types::EpochState;
|
||||
use nym_dkg::bte::PublicKeyWithProof;
|
||||
|
||||
pub(crate) fn unchecked_decode_bte_key(raw: &str) -> PublicKeyWithProof {
|
||||
let bytes = bs58::decode(raw).into_vec().unwrap();
|
||||
PublicKeyWithProof::try_from_bytes(&bytes).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn init_chain() -> SharedFakeChain {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub(crate) async fn initialise_controllers(amount: usize) -> Vec<TestingDkgController> {
|
||||
let chain = init_chain();
|
||||
|
||||
let mut controllers = Vec::with_capacity(amount);
|
||||
assert!(amount <= u8::MAX as usize);
|
||||
for rng_seed in 0..amount {
|
||||
let controller = initialise_controller(chain.clone(), rng_seed as u8).await;
|
||||
|
||||
controllers.push(controller)
|
||||
}
|
||||
|
||||
controllers
|
||||
}
|
||||
|
||||
pub(crate) async fn initialise_controller(chain: SharedFakeChain, id: u8) -> TestingDkgController {
|
||||
TestingDkgControllerBuilder::default()
|
||||
.with_shared_chain_state(chain)
|
||||
.with_magic_seed_val(id)
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn initialise_dkg(controllers: &mut [TestingDkgController], resharing: bool) {
|
||||
assert_eq!(
|
||||
controllers[0]
|
||||
.chain_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.dkg_contract
|
||||
.epoch
|
||||
.state,
|
||||
EpochState::WaitingInitialisation
|
||||
);
|
||||
|
||||
// add every dealer to group contract
|
||||
for controller in controllers.iter() {
|
||||
let address = controller.dkg_client.get_address().await;
|
||||
let mut chain = controllers[0].chain_state.lock().unwrap();
|
||||
chain.add_member(address.as_ref(), 10);
|
||||
}
|
||||
|
||||
let mut chain = controllers[0].chain_state.lock().unwrap();
|
||||
chain.dkg_contract.epoch.state = EpochState::PublicKeySubmission { resharing }
|
||||
}
|
||||
|
||||
pub(crate) async fn submit_public_keys(controllers: &mut [TestingDkgController], resharing: bool) {
|
||||
let epoch = controllers[0]
|
||||
.chain_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.dkg_contract
|
||||
.epoch
|
||||
.epoch_id;
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
controller
|
||||
.public_key_submission(epoch, resharing)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let threshold = (2 * controllers.len() as u64 + 3 - 1) / 3;
|
||||
|
||||
let mut guard = controllers[0].chain_state.lock().unwrap();
|
||||
guard.dkg_contract.epoch.state = EpochState::DealingExchange { resharing };
|
||||
guard.dkg_contract.threshold = Some(threshold)
|
||||
}
|
||||
|
||||
pub(crate) async fn exchange_dealings(controllers: &mut [TestingDkgController], resharing: bool) {
|
||||
let epoch = controllers[0]
|
||||
.chain_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.dkg_contract
|
||||
.epoch
|
||||
.epoch_id;
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
controller.dealing_exchange(epoch, resharing).await.unwrap();
|
||||
}
|
||||
|
||||
let mut guard = controllers[0].chain_state.lock().unwrap();
|
||||
guard.dkg_contract.epoch.state = EpochState::VerificationKeySubmission { resharing };
|
||||
}
|
||||
|
||||
pub(crate) async fn derive_keypairs(controllers: &mut [TestingDkgController], resharing: bool) {
|
||||
let epoch = controllers[0]
|
||||
.chain_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.dkg_contract
|
||||
.epoch
|
||||
.epoch_id;
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
controller
|
||||
.verification_key_submission(epoch, resharing)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let mut guard = controllers[0].chain_state.lock().unwrap();
|
||||
guard.dkg_contract.epoch.state = EpochState::VerificationKeyValidation { resharing }
|
||||
}
|
||||
|
||||
pub(crate) async fn validate_keys(controllers: &mut [TestingDkgController], resharing: bool) {
|
||||
let epoch = controllers[0]
|
||||
.chain_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.dkg_contract
|
||||
.epoch
|
||||
.epoch_id;
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
controller.verification_key_validation(epoch).await.unwrap();
|
||||
}
|
||||
|
||||
let mut guard = controllers[0].chain_state.lock().unwrap();
|
||||
guard.dkg_contract.epoch.state = EpochState::VerificationKeyFinalization { resharing }
|
||||
}
|
||||
|
||||
pub(crate) async fn finalize(controllers: &mut [TestingDkgController]) {
|
||||
let epoch = controllers[0]
|
||||
.chain_state
|
||||
.lock()
|
||||
.unwrap()
|
||||
.dkg_contract
|
||||
.epoch
|
||||
.epoch_id;
|
||||
|
||||
for controller in controllers.iter_mut() {
|
||||
controller
|
||||
.verification_key_finalization(epoch)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let mut guard = controllers[0].chain_state.lock().unwrap();
|
||||
guard.dkg_contract.epoch.state = EpochState::InProgress {}
|
||||
}
|
||||
+1377
-783
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user