[wip] dealing exchange

This commit is contained in:
Jędrzej Stuczyński
2024-01-18 11:29:40 +00:00
parent 90de0a30a8
commit b0174dcd0b
13 changed files with 592 additions and 206 deletions
@@ -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() {
+1
View File
@@ -13,6 +13,7 @@ pub mod dealing;
pub(crate) mod share;
pub(crate) mod utils;
pub use bls12_381::Scalar;
pub use dealing::*;
pub use share::*;
+3 -1
View File
@@ -99,7 +99,9 @@ impl SecretKey {
Self { x, ys }
}
pub fn into_raw(&self) -> (Scalar, Vec<Scalar>) {
/// Extract the Scalar copy of the underlying secrets.
/// The caller of this function must exercise extreme care to not misuse the data and ensuring it gets zeroized
pub fn hazmat_to_raw(&self) -> (Scalar, Vec<Scalar>) {
(self.x, self.ys.clone())
}
@@ -22,6 +22,9 @@ type Dealer<'a> = &'a Addr;
pub(crate) struct StoredDealing;
// part of `StoredDealing` to make existence lookup cheaper
pub(crate) struct UNIMPLEMENTED_DealingLookup;
impl StoredDealing {
const NAMESPACE: &'static [u8] = b"dealing";
+21
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) enum ComplaintReason {
@@ -11,3 +12,23 @@ pub(crate) enum ComplaintReason {
MalformedDealing,
DealingVerificationError,
}
impl Display for ComplaintReason {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ComplaintReason::MalformedBTEPublicKey => {
write!(f, "provided BTE Public key is malformed")
}
ComplaintReason::InvalidBTEPublicKey => {
write!(f, "provided BTE public key does not verify correctly")
}
ComplaintReason::MissingDealing => write!(f, "one of the dealings is missing"),
ComplaintReason::MalformedDealing => {
write!(f, "one of the provided dealings is malformed")
}
ComplaintReason::DealingVerificationError => {
write!(f, "failed to verify one of the dealings")
}
}
}
}
+12 -14
View File
@@ -4,12 +4,10 @@
use crate::coconut::dkg::client::DkgClient;
use crate::coconut::dkg::controller::error::DkgError;
use crate::coconut::dkg::state::{ConsistentState, PersistentState, State};
use crate::coconut::dkg::verification_key::verification_key_submission;
use crate::coconut::dkg::verification_key::{
verification_key_finalization, verification_key_validation,
};
use crate::coconut::dkg::{
dealing::dealing_exchange, verification_key::verification_key_submission,
};
use crate::coconut::keys::KeyPair as CoconutKeyPair;
use crate::nyxd;
use crate::support::config;
@@ -32,7 +30,7 @@ pub(crate) struct DkgController<R = OsRng> {
pub(crate) dkg_client: DkgClient,
pub(crate) coconut_key_path: PathBuf,
pub(crate) state: State,
rng: R,
pub(super) rng: R,
polling_rate: Duration,
}
@@ -137,16 +135,15 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
.map_err(|source| DkgError::PublicKeySubmissionFailure { source })
}
async fn handle_dealing_exchange(&mut self, resharing: bool) -> Result<(), DkgError> {
async fn handle_dealing_exchange(
&mut self,
epoch_id: EpochId,
resharing: bool,
) -> Result<(), DkgError> {
debug!("DKG: dealing exchange (resharing: {resharing})");
dealing_exchange(
&self.dkg_client,
&mut self.state,
self.rng.clone(),
resharing,
)
.await
.map_err(|source| DkgError::DealingExchangeFailure { source })
self.dealing_exchange(epoch_id, resharing)
.await
.map_err(|source| DkgError::DealingExchangeFailure { source })
}
async fn handle_verification_key_submission(
@@ -221,7 +218,8 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
.await?
}
EpochState::DealingExchange { resharing } => {
self.handle_dealing_exchange(resharing).await?
self.handle_dealing_exchange(epoch.epoch_id, resharing)
.await?
}
EpochState::VerificationKeySubmission { resharing } => {
self.handle_verification_key_submission(epoch.epoch_id, resharing)
+305 -104
View File
@@ -3,130 +3,331 @@
use crate::coconut::dkg;
use crate::coconut::dkg::client::DkgClient;
use crate::coconut::dkg::state::{ConsistentState, State};
use crate::coconut::dkg::complaints::ComplaintReason;
use crate::coconut::dkg::controller::keys::archive_coconut_keypair;
use crate::coconut::dkg::controller::DkgController;
use crate::coconut::dkg::state::{ConsistentState, ParticipantState, State};
use crate::coconut::error::CoconutError;
use crate::coconut::keys::KeyPairWithEpoch;
use log::debug;
use nym_coconut_dkg_common::types::{ContractDealing, PartialContractDealing};
use nym_dkg::Dealing;
use rand::RngCore;
use std::collections::VecDeque;
use nym_coconut_dkg_common::types::{
ContractDealing, DealingIndex, EpochId, PartialContractDealing,
};
use nym_dkg::bte::{PublicKey, PublicKeyWithProof};
use nym_dkg::{Dealing, NodeIndex, Scalar, Threshold};
use rand::{CryptoRng, RngCore};
use rocket::form::validate::Len;
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::fmt::{Debug, Formatter};
use zeroize::Zeroize;
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(());
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(),
}
}
}
impl<R: RngCore + CryptoRng> DkgController<R> {
async fn generate_dealings(
&mut self,
epoch_id: EpochId,
spec: DealingGeneration,
) -> Result<HashMap<DealingIndex, Dealing>, CoconutError> {
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: BTreeMap<_, _> = self
.state
.dkg_state(epoch_id)?
.dealers
.iter()
.filter_map(|(index, dealer)| match &dealer.state {
ParticipantState::Invalid(_) => None,
ParticipantState::VerifiedKey(key) => Some((*index, *key.public_key())),
})
.collect();
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)
}
let contract_state = dkg_client.get_contract_state().await?;
let expected_key_size = contract_state.key_size;
async fn generate_fresh_dealings(
&mut self,
epoch_id: EpochId,
number: u32,
) -> Result<HashMap<DealingIndex, Dealing>, CoconutError> {
self.generate_dealings(epoch_id, DealingGeneration::Fresh { number })
.await
}
let epoch_id = dkg_client.get_current_epoch().await?.epoch_id;
async fn generate_resharing_dealings(
&mut self,
epoch_id: EpochId,
prior_secrets: Vec<Scalar>,
) -> Result<HashMap<DealingIndex, Dealing>, CoconutError> {
self.generate_dealings(epoch_id, DealingGeneration::Resharing { prior_secrets })
.await
}
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);
async fn resubmit_pregenerated_dealings(
&self,
epoch_id: EpochId,
resharing: bool,
) -> Result<(), CoconutError> {
let dealing_state = self.state.dealing_exchange_state(epoch_id)?;
let prior_resharing_secrets = if let Some(mut keypair) = state.take_coconut_keypair().await {
// Double check that we are in resharing mode
if resharing {
todo!()
// let sk = keypair.secret_key();
// if sk.size() + 1 != expected_key_size as usize {
// 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 = dkg::params();
for dealing_index in 0..expected_key_size {
debug!(
"dealing {dealing_index} ({} out of {expected_key_size})",
dealing_index + 1
);
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?;
// see if we have already submitted this one (we might have crashed)
if dkg_client
.get_dealing_status(epoch_id, dealing_index)
.await?
{
warn!("we have already submitted dealing {dealing_index} before - we probably crashed!");
if dealing_submitted {
warn!("we have already submitted dealing {dealing_index} before - we probably crashed or the chain timed out!");
continue;
}
// see if we have already generated, but not submitted this one before (we might have crashed or validator might have had a problem)
let contract_dealing = if let Some(prior_dealing) =
state.get_dealing(epoch_id, dealing_index)
{
warn!("we have already generated dealing {dealing_index} before, but failed to submit it");
PartialContractDealing::new(dealing_index, ContractDealing::from(prior_dealing))
} else {
// generate fresh dealing
let (dealing, _) = Dealing::create(
rng.clone(),
params,
dealer_index,
state.threshold()?,
&receivers,
prior_resharing_secrets.pop_front(),
);
let contract_dealing =
PartialContractDealing::new(dealing_index, ContractDealing::from(&dealing));
state.store_dealing(epoch_id, dealing_index, dealing);
contract_dealing
};
debug!(
"Submitting dealing for indexes {:?} with resharing: {}",
receivers.keys().collect::<Vec<_>>(),
prior_resharing_secrets.front().is_some()
warn!(
"we have already generated dealing {dealing_index} before, but failed to submit it"
);
let contract_dealing =
PartialContractDealing::new(*dealing_index, ContractDealing::from(dealing));
dkg_client
self.dkg_client
.submit_dealing(contract_dealing, resharing)
.await?;
}
} else {
debug!("Nothing to do, waiting for initial dealers to submit dealings");
Ok(())
}
info!("DKG: Finished dealing exchange");
state.set_receiver_index(receiver_index);
/// 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, CoconutError> {
let Some(initial_data) = self.dkg_client.get_initial_dealers().await? else {
return Ok(false);
};
Ok(())
let address = self.dkg_client.get_address().await;
Ok(initial_data
.initial_dealers
.iter()
.any(|d| d.as_str() == address.as_ref()))
}
async fn handle_resharing_with_prior_key(
&mut self,
epoch_id: EpochId,
expected_key_size: u32,
old_keypair: KeyPairWithEpoch,
) -> Result<(), CoconutError> {
// 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(())
}
pub(crate) async fn dealing_exchange(
&mut self,
epoch_id: EpochId,
resharing: bool,
) -> Result<(), CoconutError> {
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(());
}
// 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.
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?;
self.state.dkg_state_mut(epoch_id)?.set_dealers(dealers);
// 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(CoconutError::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?;
}
}
// if we have generated any dealings => submit them, otherwise we're done
let dealings = &self
.state
.dealing_exchange_state(epoch_id)?
.generated_dealings;
let total = dealings.len();
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(())
}
}
#[cfg(test)]
+29 -36
View File
@@ -8,14 +8,14 @@ use log::debug;
use nym_coconut_dkg_common::types::EpochId;
use rand::{CryptoRng, RngCore};
impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
impl<R: RngCore + CryptoRng> DkgController<R> {
pub(crate) async fn public_key_submission(
&mut self,
epoch_id: EpochId,
resharing: bool,
) -> Result<(), CoconutError> {
self.state.init_dkg_state(epoch_id);
let registration_state = self.state.registration_state(epoch_id);
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() {
@@ -25,20 +25,20 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
return Ok(());
}
// if we have coconut keys available, it means we have already completed the DKG before (in previous epoch)
// in which case, archive and reset those keys
if let Some(old_keypair) = self.state.take_coconut_keypair().await {
debug!("resetting and archiving old coconut keypair");
if let Err(source) =
archive_coconut_keypair(&self.coconut_key_path, old_keypair.issued_for_epoch)
{
return Err(CoconutError::KeyArchiveFailure {
epoch_id,
path: self.coconut_key_path.clone(),
source,
});
}
}
// // if we have coconut keys available, it means we have already completed the DKG before (in previous epoch)
// // in which case, archive and reset those keys
// if let Some(old_keypair) = self.state.take_coconut_keypair().await {
// debug!("resetting and archiving old coconut keypair");
// if let Err(source) =
// archive_coconut_keypair(&self.coconut_key_path, old_keypair.issued_for_epoch)
// {
// return Err(CoconutError::KeyArchiveFailure {
// epoch_id,
// path: self.coconut_key_path.clone(),
// source,
// });
// }
// }
// FAILURE CASE:
// check if we have already sent the registration transaction, but it timed out or got stuck in the mempool and
@@ -48,7 +48,7 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
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 =
self.state.registration_state_mut(epoch_id)?.assigned_index =
Some(details.assigned_index);
info!("DKG: recovered node index: {}", details.assigned_index);
return Ok(());
@@ -64,7 +64,7 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
.dkg_client
.register_dealer(bte_key, identity_key, announce_address, resharing)
.await?;
self.state.registration_state_mut(epoch_id).assigned_index = Some(assigned_index);
self.state.registration_state_mut(epoch_id)?.assigned_index = Some(assigned_index);
info!("DKG: Using node index {assigned_index}");
Ok(())
@@ -90,8 +90,7 @@ pub(crate) mod tests {
const TEST_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
#[tokio::test]
#[ignore] // expensive test
async fn submit_public_key() {
async fn submit_public_key() -> anyhow::Result<()> {
let dkg_client = DkgClient::new(DummyClient::new(
AccountId::from_str(TEST_VALIDATOR_ADDRESS).unwrap(),
));
@@ -110,26 +109,21 @@ pub(crate) mod tests {
assert!(controller
.dkg_client
.get_self_registered_dealer_details()
.await
.unwrap()
.await?
.details
.is_none());
controller
.public_key_submission(epoch, false)
.await
.unwrap();
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!(
controller
.state
.registration_state(epoch)
.registration_state(epoch)?
.assigned_index
.unwrap(),
client_idx
@@ -138,19 +132,18 @@ pub(crate) mod tests {
// keeps the same index from chain, not calling register_dealer again
controller
.state
.registration_state_mut(epoch)
.registration_state_mut(epoch)?
.assigned_index = None;
controller
.public_key_submission(epoch, false)
.await
.unwrap();
controller.public_key_submission(epoch, false).await?;
assert_eq!(
controller
.state
.registration_state(epoch)
.registration_state(epoch)?
.assigned_index
.unwrap(),
client_idx
);
Ok(())
}
}
@@ -0,0 +1,24 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use super::serde_helpers::generated_dealings;
use nym_coconut_dkg_common::types::DealingIndex;
use nym_dkg::{Dealing, NodeIndex};
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct DealingExchangeState {
// pub(crate) assigned_index: Option<NodeIndex>,
#[serde(with = "generated_dealings")]
pub(crate) generated_dealings: HashMap<DealingIndex, Dealing>,
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
}
}
+134 -51
View File
@@ -2,57 +2,78 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::coconut::dkg::complaints::ComplaintReason;
use crate::coconut::dkg::controller::keys::archive_coconut_keypair;
use crate::coconut::dkg::state::dealing_exchange::DealingExchangeState;
use crate::coconut::dkg::state::registration::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::{DealingIndex, EpochId, EpochState};
use nym_coconut_dkg_common::types::{
DealingIndex, EncodedBTEPublicKeyWithProof, EpochId, EpochState,
};
use nym_crypto::asymmetric::identity;
use nym_dkg::bte::{keys::KeyPair as DkgKeyPair, PublicKey, PublicKeyWithProof};
use nym_dkg::{Dealing, NodeIndex, RecoveredVerificationKeys, Threshold};
use nym_validator_client::nyxd::{tx, Hash};
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_helpers::{bte_pk_serde, generated_dealings, vks_serde};
use serde_helpers::{bte_pk_serde, generated_dealings_old, vks_serde};
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use time::OffsetDateTime;
use tokio::sync::RwLockReadGuard;
use url::Url;
mod dealing_exchange;
mod registration;
mod serde_helpers;
#[derive(Clone, Deserialize, Debug, Serialize)]
pub(crate) enum ParticipantState {
Invalid(ComplaintReason),
VerifiedKey(#[serde(with = "bte_pk_serde")] PublicKeyWithProof),
}
impl ParticipantState {
pub fn is_valid(&self) -> bool {
matches!(self, ParticipantState::VerifiedKey(..))
}
fn from_raw_encoded_key(raw: EncodedBTEPublicKeyWithProof) -> Self {
// TODO: include more error information
let Ok(bytes) = bs58::decode(raw).into_vec() else {
return ParticipantState::Invalid(ComplaintReason::MalformedBTEPublicKey);
};
let Ok(key) = PublicKeyWithProof::try_from_bytes(&bytes) else {
return ParticipantState::Invalid(ComplaintReason::MalformedBTEPublicKey);
};
if !key.verify() {
return ParticipantState::Invalid(ComplaintReason::InvalidBTEPublicKey);
}
ParticipantState::VerifiedKey(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,
#[serde(with = "bte_pk_serde")]
pub(crate) bte_public_key_with_proof: PublicKeyWithProof,
pub(crate) address: Addr,
pub(crate) assigned_index: NodeIndex,
pub(crate) state: ParticipantState,
}
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,
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,
})
}
}
}
@@ -139,7 +160,7 @@ pub(crate) struct PersistentState {
//
node_index: Option<NodeIndex>,
dealers: BTreeMap<Addr, Result<DkgParticipant, ComplaintReason>>,
#[serde(with = "generated_dealings")]
#[serde(with = "generated_dealings_old")]
generated_dealings: HashMap<EpochId, HashMap<DealingIndex, Dealing>>,
receiver_index: Option<usize>,
threshold: Option<Threshold>,
@@ -189,6 +210,7 @@ impl From<&State> for PersistentState {
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(())
}
@@ -201,6 +223,28 @@ impl PersistentState {
#[derive(Default)]
pub(crate) struct DkgState {
pub(crate) registration: RegistrationState,
// available after registration is finished:
pub(crate) dealers: BTreeMap<NodeIndex, DkgParticipant>,
pub(crate) dealing_exchange: DealingExchangeState,
}
impl DkgState {
pub(crate) fn set_dealers(&mut self, raw_dealers: Vec<DealerDetails>) {
assert!(self.dealers.is_empty());
for raw_dealer in raw_dealers {
let dkg_participant = DkgParticipant::from(raw_dealer);
if let ParticipantState::Invalid(complaint) = &dkg_participant.state {
warn!(
"{} dealer is malformed: {complaint}",
dkg_participant.address
)
}
self.dealers
.insert(dkg_participant.assigned_index, dkg_participant);
}
}
}
pub(crate) struct State {
@@ -288,24 +332,62 @@ impl State {
self.was_in_progress = Default::default();
}
pub fn init_dkg_state(&mut self, epoch_id: EpochId) {
pub fn maybe_init_dkg_state(&mut self, epoch_id: EpochId) {
if !self.dkg_instances.contains_key(&epoch_id) {
self.dkg_instances.insert(epoch_id, Default::default());
}
}
pub fn registration_state(&self, epoch_id: EpochId) -> &RegistrationState {
// safety: before any accessors are called, `init_dkg_state` is used at the beginning of submission handler
&self.dkg_instances[&epoch_id].registration
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 registration_state_mut(&mut self, epoch_id: EpochId) -> &mut RegistrationState {
// safety: before any accessors are called, `init_dkg_state` is used at the beginning of submission handler
&mut self.dkg_instances.get_mut(&epoch_id).unwrap().registration
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 already_registered(&self, epoch_id: EpochId) -> bool {
self.registration_state(epoch_id).completed()
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 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 persistent_state_path(&self) -> &Path {
@@ -379,17 +461,18 @@ impl State {
// FIXME: BUG: if we remove dealers, we won't be able to verify shares of other parties...
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()
todo!()
// 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> {
@@ -424,13 +507,13 @@ impl State {
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 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
@@ -51,6 +51,39 @@ pub(super) mod vks_serde {
}
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()
}
}
pub(super) mod generated_dealings_old {
use nym_coconut_dkg_common::types::{DealingIndex, EpochId};
use nym_dkg::Dealing;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
+3
View File
@@ -138,6 +138,9 @@ pub enum CoconutError {
#[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("insufficient number of dealings provided to derive the key")]
InsufficientDealings {
// TODO: details
+18
View File
@@ -4,8 +4,10 @@
mod persistence;
use nym_coconut_dkg_common::types::EpochId;
use nym_dkg::Scalar;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use tokio::sync::{RwLock, RwLockReadGuard};
@@ -13,6 +15,7 @@ use tokio::sync::{RwLock, RwLockReadGuard};
pub struct KeyPair {
// keys: Arc<RwLock<HashMap<EpochId, nym_coconut_interface::KeyPair>>>,
keys: Arc<RwLock<Option<KeyPairWithEpoch>>>,
valid: Arc<AtomicBool>,
}
#[derive(Debug)]
@@ -21,10 +24,25 @@ pub struct KeyPairWithEpoch {
pub(crate) issued_for_epoch: EpochId,
}
impl KeyPairWithEpoch {
// 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()),
}
}