Feature/fix resharing (#3139)

* Compare verified vks against current group instead of initial dealers

* Fix various dkg logs

* API auto-advance epoch even on corrupt states

* Use verified vks as ultimate truth for dealers

* Set initial dealers based of verified vk

* Extend register period even more

* Fix test

* Use shares from current epoch

* Save initial dealers only when triggering resharing

* Fix tests

* Backup the last InProgress state too

* Reset previous signers that are not initial dealers

* Add unit test for bug reproduction

* More verbose debug logging

* Handle edge case for coconut keypair removal

* Update dkg api test

* Remove dealings directly for each key

* Replacement data is saved only on the first reshare start

* More debug logging

* On failed DKG, just reset

* Clippy fix
This commit is contained in:
Bogdan-Ștefan Neacşu
2023-03-15 11:16:43 +00:00
committed by GitHub
parent 8e96318478
commit 9eaf9cf491
13 changed files with 553 additions and 170 deletions
@@ -21,7 +21,7 @@ pub const TOTAL_DEALINGS: usize = 2 + 2 + 1;
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
pub struct InitialReplacementData {
pub initial_dealers: Vec<Addr>,
pub initial_height: Option<u64>,
pub initial_height: u64,
}
#[derive(
@@ -22,7 +22,7 @@ fn verify_dealer(deps: DepsMut<'_>, dealer: &Addr, resharing: bool) -> Result<()
let state = STATE.load(deps.storage)?;
let height = if resharing {
INITIAL_REPLACEMENT_DATA.load(deps.storage)?.initial_height
Some(INITIAL_REPLACEMENT_DATA.load(deps.storage)?.initial_height)
} else {
None
};
@@ -105,7 +105,7 @@ pub(crate) mod tests {
deps.as_mut().storage,
&InitialReplacementData {
initial_dealers: vec![details1.address, details2.address, details3.address],
initial_height: Some(1),
initial_height: 1,
},
)
.unwrap();
@@ -126,7 +126,7 @@ pub(crate) mod tests {
INITIAL_REPLACEMENT_DATA
.update::<_, ContractError>(deps.as_mut().storage, |mut data| {
data.initial_height = Some(2);
data.initial_height = 2;
Ok(data)
})
.unwrap();
@@ -110,7 +110,7 @@ pub(crate) mod tests {
deps.as_mut().storage,
&InitialReplacementData {
initial_dealers: vec![],
initial_height: None,
initial_height: 1,
},
)
.unwrap();
@@ -7,6 +7,7 @@ use crate::epoch_state::storage::{CURRENT_EPOCH, INITIAL_REPLACEMENT_DATA, THRES
use crate::epoch_state::utils::check_epoch_state;
use crate::error::ContractError;
use crate::state::STATE;
use crate::verification_key_shares::storage::verified_dealers;
use cosmwasm_std::{Addr, Deps, DepsMut, Env, Order, Response, Storage};
use nym_coconut_dkg_common::types::{Epoch, EpochState, InitialReplacementData};
@@ -19,7 +20,13 @@ 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 {
dealings.remove(storage, &details.address);
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)?;
@@ -46,15 +53,9 @@ fn dealers_still_active(
}
fn dealers_eq_members(deps: &DepsMut<'_>) -> Result<bool, ContractError> {
let dealers_still_active = dealers_still_active(
&deps.as_ref(),
current_dealers()
.keys(deps.storage, None, None, Order::Ascending)
.flatten(),
)?;
let all_dealers = current_dealers()
.keys(deps.storage, None, None, Order::Ascending)
.count();
let verified_dealers = verified_dealers(deps.storage)?;
let all_dealers = verified_dealers.len();
let dealers_still_active = dealers_still_active(&deps.as_ref(), verified_dealers.into_iter())?;
let group_members = STATE
.load(deps.storage)?
.group_addr
@@ -66,7 +67,11 @@ fn dealers_eq_members(deps: &DepsMut<'_>) -> Result<bool, ContractError> {
fn replacement_threshold_surpassed(deps: &DepsMut<'_>) -> Result<bool, ContractError> {
let threshold = THRESHOLD.load(deps.storage)? as usize;
let initial_dealers = INITIAL_REPLACEMENT_DATA.load(deps.storage)?.initial_dealers;
let initial_dealers = verified_dealers(deps.storage)?;
if initial_dealers.is_empty() {
// possibly failed DKG, just reset and start again
return Ok(true);
}
let initial_dealer_count = initial_dealers.len();
let replacement_threshold = threshold - (initial_dealers.len() + 2 - 1) / 2 + 1;
let removed_dealer_count =
@@ -90,24 +95,23 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
let next_epoch = if let Some(state) = current_epoch.state.next() {
// We are during DKG process
let mut new_state = state;
if let EpochState::DealingExchange { resharing } = state {
if let EpochState::DealingExchange { .. } = state {
let current_dealers = current_dealers()
.keys(deps.storage, None, None, Order::Ascending)
.collect::<Result<Vec<Addr>, _>>()?;
if current_dealers.is_empty() {
// If no dealer registered yet, we just stay in the same state until there's at least one
let group_members =
STATE
.load(deps.storage)?
.group_addr
.list_members(&deps.querier, None, None)?;
if current_dealers.len() < group_members.len() {
// If not all group members registered yet, we just stay in the same state until
// they either register or they get kicked out of the group
new_state = current_epoch.state;
} else {
// note: ceiling in integer division can be achieved via q = (x + y - 1) / y;
let threshold = (2 * current_dealers.len() as u64 + 3 - 1) / 3;
THRESHOLD.save(deps.storage, &threshold)?;
if !resharing {
let replacement_data = InitialReplacementData {
initial_dealers: current_dealers,
initial_height: None,
};
INITIAL_REPLACEMENT_DATA.save(deps.storage, &replacement_data)?;
}
}
};
Epoch::new(
@@ -129,13 +133,23 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
// Dealer set changed, we need to redo DKG...
let state = if replacement_threshold_surpassed(&deps)? {
// ... in reset mode
INITIAL_REPLACEMENT_DATA.remove(deps.storage);
EpochState::default()
} else {
// ... in reshare mode
INITIAL_REPLACEMENT_DATA.update::<_, ContractError>(deps.storage, |mut data| {
data.initial_height = Some(env.block.height);
Ok(data)
})?;
if INITIAL_REPLACEMENT_DATA.may_load(deps.storage)?.is_some() {
INITIAL_REPLACEMENT_DATA.update::<_, ContractError>(deps.storage, |mut data| {
data.initial_height = env.block.height;
Ok(data)
})?;
} else {
let replacement_data = InitialReplacementData {
initial_dealers: verified_dealers(deps.storage)?,
initial_height: env.block.height,
};
INITIAL_REPLACEMENT_DATA.save(deps.storage, &replacement_data)?;
}
EpochState::PublicKeySubmission { resharing: true }
};
reset_epoch_state(deps.storage)?;
@@ -158,10 +172,8 @@ pub(crate) fn try_surpassed_threshold(
check_epoch_state(deps.storage, EpochState::InProgress)?;
let threshold = THRESHOLD.load(deps.storage)?;
let dealers = current_dealers()
.keys(deps.storage, None, None, Order::Ascending)
.flatten();
if dealers_still_active(&deps.as_ref(), dealers)? < threshold as usize {
let dealers = verified_dealers(deps.storage)?;
if dealers_still_active(&deps.as_ref(), dealers.into_iter())? < threshold as usize {
reset_epoch_state(deps.storage)?;
CURRENT_EPOCH.update::<_, ContractError>(deps.storage, |epoch| {
Ok(Epoch::new(
@@ -180,8 +192,9 @@ pub(crate) fn try_surpassed_threshold(
pub(crate) mod tests {
use super::*;
use crate::error::ContractError::EarlyEpochStateAdvancement;
use crate::support::tests::fixtures::dealer_details_fixture;
use crate::support::tests::fixtures::{dealer_details_fixture, vk_share_fixture};
use crate::support::tests::helpers::{init_contract, GROUP_MEMBERS};
use crate::verification_key_shares::storage::vk_shares;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::Addr;
use cw4::Member;
@@ -204,11 +217,15 @@ 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 initial_dealers = dealers.iter().map(|d| d.address.clone()).collect();
let data = InitialReplacementData {
initial_dealers,
initial_height: None,
initial_height: 1,
};
for share in shares {
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);
THRESHOLD.save(deps.as_mut().storage, &threshold).unwrap();
@@ -247,39 +264,39 @@ pub(crate) mod tests {
assert!(dealers_eq_members(&deps.as_mut()).unwrap());
let details = dealer_details_fixture(1);
let different_details = dealer_details_fixture(2);
current_dealers()
.save(deps.as_mut().storage, &details.address, &details)
let share = vk_share_fixture("owner2", 0);
let different_share = vk_share_fixture("owner4", 0);
vk_shares()
.save(deps.as_mut().storage, (&share.owner, 0), &share)
.unwrap();
assert!(!dealers_eq_members(&deps.as_mut()).unwrap());
current_dealers()
.remove(deps.as_mut().storage, &details.address)
vk_shares()
.remove(deps.as_mut().storage, (&share.owner, 0))
.unwrap();
GROUP_MEMBERS.lock().unwrap().push((
Member {
addr: "owner1".to_string(),
addr: "owner2".to_string(),
weight: 10,
},
1,
));
assert!(!dealers_eq_members(&deps.as_mut()).unwrap());
current_dealers()
vk_shares()
.save(
deps.as_mut().storage,
&different_details.address,
&different_details,
(&different_share.owner, 0),
&different_share,
)
.unwrap();
assert!(!dealers_eq_members(&deps.as_mut()).unwrap());
current_dealers()
.remove(deps.as_mut().storage, &different_details.address)
vk_shares()
.remove(deps.as_mut().storage, (&different_share.owner, 0))
.unwrap();
current_dealers()
.save(deps.as_mut().storage, &details.address, &details)
vk_shares()
.save(deps.as_mut().storage, (&share.owner, 0), &share)
.unwrap();
assert!(dealers_eq_members(&deps.as_mut()).unwrap());
}
@@ -407,6 +424,12 @@ pub(crate) mod tests {
);
// setup dealer details
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)
.unwrap();
}
let all_details: [_; 4] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 1));
for details in all_details.iter() {
current_dealers()
@@ -431,12 +454,6 @@ pub(crate) mod tests {
.time
.plus_seconds(epoch.time_configuration.dealing_exchange_time_secs)
);
let replacement_data = INITIAL_REPLACEMENT_DATA.load(&deps.storage).unwrap();
let expected_replacement_data = InitialReplacementData {
initial_dealers: all_details.iter().map(|d| d.address.clone()).collect(),
initial_height: None,
};
assert_eq!(replacement_data, expected_replacement_data);
env.block.time = env
.block
@@ -588,8 +605,14 @@ pub(crate) mod tests {
);
assert_eq!(curr_epoch, expected_epoch);
assert!(THRESHOLD.may_load(&deps.storage).unwrap().is_none());
let replacement_data = INITIAL_REPLACEMENT_DATA.load(&deps.storage).unwrap();
let expected_replacement_data = InitialReplacementData {
initial_dealers: all_details.iter().map(|d| d.address.clone()).collect(),
initial_height: 12345,
};
assert_eq!(replacement_data, expected_replacement_data);
let all_details: [_; 2] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 2));
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();
current_dealers()
@@ -607,6 +630,17 @@ pub(crate) mod tests {
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
}
let all_shares: [_; 4] = std::array::from_fn(|i| {
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)
.unwrap();
}
// Group changed even more, surpassing threshold, so re-run dkg in reset mode
*GROUP_MEMBERS.lock().unwrap().last_mut().unwrap() = (
Member {
@@ -623,7 +657,7 @@ pub(crate) mod tests {
advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
let curr_epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
let expected_epoch = Epoch::new(
EpochState::PublicKeySubmission { resharing: false },
EpochState::PublicKeySubmission { resharing: true },
prev_epoch.epoch_id + 1,
prev_epoch.time_configuration,
env.block.time,
@@ -672,12 +706,25 @@ pub(crate) mod tests {
}
);
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)
.unwrap();
}
let all_details: [_; 3] = std::array::from_fn(|i| dealer_details_fixture(i as u64 + 1));
for details in all_details.iter() {
current_dealers()
.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));
for share in all_shares.iter() {
vk_shares()
.save(deps.as_mut().storage, (&share.owner, share.epoch_id), share)
.unwrap();
}
for times in [
time_configuration.public_key_submission_time_secs,
@@ -4,7 +4,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::constants::{VK_SHARES_EPOCH_ID_IDX_NAMESPACE, VK_SHARES_PK_NAMESPACE};
use cosmwasm_std::Addr;
use crate::epoch_state::storage::CURRENT_EPOCH;
use crate::error::ContractError;
use cosmwasm_std::{Addr, Order, Storage};
use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex};
use nym_coconut_dkg_common::types::EpochId;
use nym_coconut_dkg_common::verification_key::ContractVKShare;
@@ -35,3 +37,21 @@ pub(crate) fn vk_shares<'a>() -> IndexedMap<'a, VKShareKey<'a>, ContractVKShare,
};
IndexedMap::new(VK_SHARES_PK_NAMESPACE, indexes)
}
pub(crate) fn verified_dealers(storage: &dyn Storage) -> Result<Vec<Addr>, ContractError> {
let epoch_id = CURRENT_EPOCH.load(storage)?.epoch_id;
Ok(vk_shares()
.idx
.epoch_id
.prefix(epoch_id)
.range(storage, None, None, Order::Ascending)
.flatten()
.filter_map(|(_, share)| {
if share.verified {
Some(share.owner)
} else {
None
}
})
.collect())
}
+68 -49
View File
@@ -84,6 +84,18 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
})
}
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}"),
@@ -99,57 +111,64 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
return;
}
if let Err(err) = self.state.is_consistent(epoch.state).await {
error!("Epoch state is corrupted - {err}, the process should be terminated");
return;
}
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)
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::VerificationKeyFinalization { resharing } => {
verification_key_finalization(&self.dkg_client, &mut self.state, resharing)
}
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
}
// Just wait, in case we need to redo dkg at some point
EpochState::InProgress => {
self.state.set_was_in_progress();
Ok(())
}
};
if let Err(err) = ret {
warn!("Could not handle this iteration for the epoch state: {err}");
} else if epoch.state != EpochState::InProgress {
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}");
}
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) =
+24 -11
View File
@@ -4,6 +4,7 @@
use crate::coconut::dkg::client::DkgClient;
use crate::coconut::dkg::state::{ConsistentState, State};
use crate::coconut::error::CoconutError;
use log::debug;
use nym_coconut_dkg_common::types::TOTAL_DEALINGS;
use nym_contracts_common::dealings::ContractSafeBytes;
use nym_dkg::bte::setup;
@@ -18,6 +19,7 @@ pub(crate) async fn dealing_exchange(
resharing: bool,
) -> Result<(), CoconutError> {
if state.receiver_index().is_some() {
debug!("Receiver index was set previously, nothing to do");
return Ok(());
}
@@ -45,6 +47,7 @@ pub(crate) async fn dealing_exchange(
return Err(CoconutError::CorruptedCoconutKeyPair);
}
// We can now erase the keypair from memory
debug!("Removing coconut keypair from memory");
state.set_coconut_keypair(None).await;
scalars.push(x);
scalars
@@ -59,6 +62,11 @@ pub(crate) async fn dealing_exchange(
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(),
&params,
@@ -71,9 +79,11 @@ pub(crate) async fn dealing_exchange(
.submit_dealing(ContractSafeBytes::from(&dealing), resharing)
.await?;
}
} else {
debug!("Nothing to do, waiting for initial dealers to submit dealings");
}
info!("DKG: Finished submitting dealing");
info!("DKG: Finished dealing exchange");
state.set_receiver_index(receiver_index);
Ok(())
@@ -109,7 +119,7 @@ pub(crate) mod tests {
fn insert_dealers(
params: &Params,
dealer_details_db: &Arc<RwLock<HashMap<String, DealerDetails>>>,
dealer_details_db: &Arc<RwLock<HashMap<String, (DealerDetails, bool)>>>,
) -> Vec<DkgKeyPair> {
let mut keypairs = vec![];
for (idx, addr) in TEST_VALIDATORS_ADDRESS.iter().enumerate() {
@@ -119,12 +129,15 @@ pub(crate) mod tests {
keypairs.push(keypair);
dealer_details_db.write().unwrap().insert(
addr.to_string(),
DealerDetails {
address: Addr::unchecked(*addr),
bte_public_key_with_proof,
announce_address: format!("localhost:80{}", idx),
assigned_index: (idx + 1) as u64,
},
(
DealerDetails {
address: Addr::unchecked(*addr),
bte_public_key_with_proof,
announce_address: format!("localhost:80{}", idx),
assigned_index: (idx + 1) as u64,
},
true,
),
);
}
keypairs
@@ -216,7 +229,7 @@ pub(crate) mod tests {
.unwrap()
.entry(TEST_VALIDATORS_ADDRESS[1].to_string())
.and_modify(|details| {
let mut bytes = bs58::decode(details.bte_public_key_with_proof.clone())
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
@@ -231,7 +244,7 @@ pub(crate) mod tests {
break;
}
}
details.bte_public_key_with_proof = bs58::encode(&bytes).into_string();
details.0.bte_public_key_with_proof = bs58::encode(&bytes).into_string();
});
dealing_exchange(&dkg_client, &mut state, OsRng, false)
@@ -257,7 +270,7 @@ pub(crate) mod tests {
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: Some(100),
initial_height: 100,
})));
let dkg_client = DkgClient::new(
DummyClient::new(
+16 -1
View File
@@ -4,6 +4,7 @@
use crate::coconut::dkg::client::DkgClient;
use crate::coconut::dkg::state::State;
use crate::coconut::error::CoconutError;
use log::debug;
use nym_coconut_dkg_common::dealer::DealerType;
pub(crate) async fn public_key_submission(
@@ -12,9 +13,21 @@ pub(crate) async fn public_key_submission(
resharing: bool,
) -> Result<(), CoconutError> {
if state.was_in_progress() {
state.reset_persistent(resharing).await;
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(());
}
@@ -23,12 +36,14 @@ pub(crate) async fn public_key_submission(
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)
+8 -3
View File
@@ -5,6 +5,7 @@ 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};
@@ -133,7 +134,7 @@ impl ConsistentState for State {
fn proposal_id_value(&self) -> Result<u64, CoconutError> {
self.proposal_id.ok_or(CoconutError::UnrecoverableState {
reason: String::from("Proposal id should have benn set"),
reason: String::from("Proposal id should have been set"),
})
}
}
@@ -242,8 +243,8 @@ impl State {
}
}
pub async fn reset_persistent(&mut self, resharing: bool) {
if !resharing {
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();
@@ -360,6 +361,10 @@ impl State {
.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);
}
}
+258 -18
View File
@@ -8,6 +8,7 @@ use crate::coconut::error::CoconutError;
use crate::coconut::helpers::accepted_vote_err;
use cosmwasm_std::Addr;
use cw3::{ProposalResponse, Status};
use log::debug;
use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID;
use nym_coconut_dkg_common::types::{NodeIndex, TOTAL_DEALINGS};
use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs;
@@ -116,6 +117,11 @@ fn derive_partial_keypair(
}
})
.unzip();
debug!(
"Recovering verification keys from dealings of dealers {:?} with receivers {:?}",
filtered_dealers,
filtered_receivers_by_idx.keys().collect::<Vec<_>>()
);
let recovered = try_recover_verification_keys(
&filtered_dealings,
threshold,
@@ -123,10 +129,12 @@ fn derive_partial_keypair(
)?;
recovered_vks.push(recovered);
debug!("Decrypting shares");
let shares = filtered_dealings
.iter()
.map(|dealing| decrypt_share(dk, node_index_value, &dealing.ciphertexts, None))
.collect::<Result<_, _>>()?;
debug!("Combining shares into one secret");
let scalar = combine_shares(shares, &filtered_dealers)?;
scalars.push(scalar);
}
@@ -152,13 +160,19 @@ pub(crate) async fn verification_key_submission(
resharing: bool,
) -> Result<(), CoconutError> {
if state.coconut_keypair_is_some().await {
debug!("Coconut keypair was set previously, nothing to do");
return Ok(());
}
let threshold = state.threshold()?;
let dealings_maps =
deterministic_filter_dealers(dkg_client, state, threshold, resharing).await?;
debug!(
"Filtered dealers to {:?}",
dealings_maps[0].keys().collect::<Vec<_>>()
);
let coconut_keypair = derive_partial_keypair(state, threshold, dealings_maps)?;
debug!("Derived own coconut keypair");
let vk_share = coconut_keypair.verification_key().to_bs58();
nym_pemstore::store_keypair(&coconut_keypair, keypair_path)?;
let res = dkg_client
@@ -173,6 +187,10 @@ pub(crate) async fn verification_key_submission(
.map_err(|_| CoconutError::ProposalIdError {
reason: String::from("proposal id could not be parsed to u64"),
})?;
debug!(
"Submitted own verification key share, proposal id {} is attached to it",
proposal_id
);
state.set_proposal_id(proposal_id);
state.set_coconut_keypair(Some(coconut_keypair)).await;
info!("DKG: Submitted own verification key");
@@ -195,6 +213,7 @@ pub(crate) async fn verification_key_validation(
_resharing: bool,
) -> Result<(), CoconutError> {
if state.voted_vks() {
debug!("Already voted on the verification keys, nothing to do");
return Ok(());
}
@@ -225,10 +244,15 @@ pub(crate) async fn verification_key_validation(
.position(|node_index| contract_share.node_index == *node_index)
{
let ret = if !check_vk_pairing(&params, &recovered_partials[idx], &vk) {
debug!(
"Voting NO to proposal {} because of failed VK pairing",
proposal_id
);
dkg_client
.vote_verification_key_share(proposal_id, false)
.await
} else {
debug!("Voting YES to proposal {}", proposal_id);
dkg_client
.vote_verification_key_share(proposal_id, true)
.await
@@ -237,6 +261,10 @@ pub(crate) async fn verification_key_validation(
}
}
Err(_) => {
debug!(
"Voting NO to proposal {} because of failed base 58 deserialization",
proposal_id
);
let ret = dkg_client
.vote_verification_key_share(proposal_id, false)
.await;
@@ -256,6 +284,7 @@ pub(crate) async fn verification_key_finalization(
_resharing: bool,
) -> Result<(), CoconutError> {
if state.executed_proposal() {
debug!("Already executed the proposal, nothing to do");
return Ok(());
}
@@ -294,7 +323,7 @@ pub(crate) mod tests {
use validator_client::nyxd::AccountId;
struct MockContractDb {
dealer_details_db: Arc<RwLock<HashMap<String, DealerDetails>>>,
dealer_details_db: Arc<RwLock<HashMap<String, (DealerDetails, bool)>>>,
dealings_db: Arc<RwLock<HashMap<String, Vec<ContractSafeBytes>>>>,
proposal_db: Arc<RwLock<HashMap<u64, ProposalResponse>>>,
verification_share_db: Arc<RwLock<HashMap<String, ContractVKShare>>>,
@@ -315,10 +344,11 @@ pub(crate) mod tests {
}
}
const TEST_VALIDATORS_ADDRESS: [&str; 3] = [
const TEST_VALIDATORS_ADDRESS: [&str; 4] = [
"n1aq9kakfgwqcufr23lsv644apavcntrsqsk4yus",
"n1s9l3xr4g0rglvk4yctktmck3h4eq0gp6z2e20v",
"n19kl4py32vsk297dm93ezem992cdyzdy4zuc2x6",
"n1jfrs6cmw9t7dv0x8cgny6geunzjh56n2s89fkv",
];
async fn prepare_clients_and_states(db: &MockContractDb) -> Vec<(DkgClient, State)> {
@@ -418,7 +448,7 @@ pub(crate) mod tests {
.unwrap();
assert_eq!(filtered.len(), TOTAL_DEALINGS);
for mapping in filtered.iter() {
assert_eq!(mapping.len(), 3);
assert_eq!(mapping.len(), 4);
}
}
}
@@ -471,7 +501,7 @@ pub(crate) mod tests {
for (dkg_client, state) in clients_and_states.iter_mut().skip(1) {
*db.initial_dealers_db.write().unwrap() = Some(InitialReplacementData {
initial_dealers: vec![Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])],
initial_height: None,
initial_height: 1,
});
let filtered = deterministic_filter_dealers(dkg_client, state, 2, true)
.await
@@ -504,7 +534,7 @@ pub(crate) mod tests {
for (dkg_client, state) in clients_and_states.iter_mut().skip(1) {
*db.initial_dealers_db.write().unwrap() = Some(InitialReplacementData {
initial_dealers: vec![],
initial_height: None,
initial_height: 1,
});
let filtered = deterministic_filter_dealers(dkg_client, state, 2, true)
.await
@@ -542,7 +572,7 @@ pub(crate) mod tests {
.unwrap();
assert_eq!(filtered.len(), TOTAL_DEALINGS);
for mapping in filtered.iter() {
assert_eq!(mapping.len(), 2);
assert_eq!(mapping.len(), 3);
}
let corrupted_status = state
.all_dealers()
@@ -803,6 +833,9 @@ pub(crate) mod tests {
async fn reshare_preserves_keys() {
let db = MockContractDb::new();
let mut clients_and_states = prepare_clients_and_states_with_finalization(&db).await;
for (_, state) in clients_and_states.iter_mut() {
state.set_was_in_progress();
}
let params = Parameters::new(4).unwrap();
let mut vks = vec![];
@@ -839,23 +872,22 @@ pub(crate) mod tests {
KeyPair::new(),
);
let removed_dealer = clients_and_states.first().unwrap().0.get_address().await;
db.dealer_details_db
.write()
.unwrap()
.remove(removed_dealer.as_ref());
for (_, active) in db.dealer_details_db.write().unwrap().values_mut() {
*active = false;
}
*db.dealings_db.write().unwrap() = Default::default();
*db.verification_share_db.write().unwrap() = Default::default();
let mut initial_dealers = vec![];
for (dkg_client, _) in clients_and_states.iter() {
let client_address = Addr::unchecked(dkg_client.get_address().await.as_ref());
initial_dealers.push(client_address);
}
*db.initial_dealers_db.write().unwrap() = Some(InitialReplacementData {
initial_dealers: vec![
Addr::unchecked(clients_and_states[1].0.get_address().await.as_ref()),
Addr::unchecked(clients_and_states[2].0.get_address().await.as_ref()),
],
initial_height: None,
initial_dealers,
initial_height: 1,
});
*clients_and_states.first_mut().unwrap() = (new_dkg_client, state);
clients_and_states[1].1.set_was_in_progress();
clients_and_states[2].1.set_was_in_progress();
for (dkg_client, state) in clients_and_states.iter_mut() {
public_key_submission(dkg_client, state, true)
@@ -880,6 +912,214 @@ pub(crate) mod tests {
std::fs::remove_file(private_key_path).unwrap();
std::fs::remove_file(public_key_path).unwrap();
}
for (dkg_client, state) in clients_and_states.iter_mut() {
verification_key_validation(dkg_client, state, true)
.await
.unwrap();
}
for (dkg_client, state) in clients_and_states.iter_mut() {
verification_key_finalization(dkg_client, state, true)
.await
.unwrap();
}
assert!(db
.proposal_db
.read()
.unwrap()
.values()
.all(|proposal| { proposal.status == Status::Executed }));
let mut vks = vec![];
let mut indices = vec![];
for (_, state) in clients_and_states.iter() {
let vk = state
.coconut_secret_key()
.await
.unwrap()
.verification_key(&params);
let index = state.node_index().unwrap();
vks.push(vk);
indices.push(index);
}
let reshared_master_vk = aggregate_verification_keys(&vks, Some(&indices)).unwrap();
assert_eq!(initial_master_vk, reshared_master_vk);
}
#[tokio::test]
#[ignore] // expensive test
async fn reshare_after_reset() {
let db = MockContractDb::new();
let mut clients_and_states = prepare_clients_and_states_with_finalization(&db).await;
for (_, state) in clients_and_states.iter_mut() {
state.set_was_in_progress();
}
let new_dkg_client = DkgClient::new(
DummyClient::new(
AccountId::from_str("n1vxkywf9g4cg0k2dehanzwzz64jw782qm0kuynf").unwrap(),
)
.with_dealer_details(&db.dealer_details_db)
.with_dealings(&db.dealings_db)
.with_proposal_db(&db.proposal_db)
.with_verification_share(&db.verification_share_db)
.with_threshold(&db.threshold_db)
.with_initial_dealers_db(&db.initial_dealers_db),
);
let keypair = DkgKeyPair::new(&setup(), OsRng);
let state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
keypair,
KeyPair::new(),
);
let new_dkg_client2 = DkgClient::new(
DummyClient::new(
AccountId::from_str("n1sqkxzh7nl6kgndr4ew9795t2nkwmd8tpql67q7").unwrap(),
)
.with_dealer_details(&db.dealer_details_db)
.with_dealings(&db.dealings_db)
.with_proposal_db(&db.proposal_db)
.with_verification_share(&db.verification_share_db)
.with_threshold(&db.threshold_db)
.with_initial_dealers_db(&db.initial_dealers_db),
);
let keypair = DkgKeyPair::new(&setup(), OsRng);
let state2 = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
keypair,
KeyPair::new(),
);
for (_, active) in db.dealer_details_db.write().unwrap().values_mut() {
*active = false;
}
*db.dealings_db.write().unwrap() = Default::default();
*db.verification_share_db.write().unwrap() = Default::default();
clients_and_states.pop().unwrap();
let (initial_client2, initial_state2) = clients_and_states.pop().unwrap();
clients_and_states.push((new_dkg_client, state));
clients_and_states.push((new_dkg_client2, state2));
// DKG in reset mode
for (dkg_client, state) in clients_and_states.iter_mut() {
public_key_submission(dkg_client, state, false)
.await
.unwrap();
}
for (dkg_client, state) in clients_and_states.iter_mut() {
dealing_exchange(dkg_client, state, OsRng, false)
.await
.unwrap();
}
for (dkg_client, state) in clients_and_states.iter_mut() {
let random_file: usize = OsRng.gen();
let private_key_path = temp_dir().join(format!("private{}.pem", random_file));
let public_key_path = temp_dir().join(format!("public{}.pem", random_file));
let keypair_path = KeyPairPath::new(private_key_path.clone(), public_key_path.clone());
verification_key_submission(dkg_client, state, &keypair_path, false)
.await
.unwrap();
std::fs::remove_file(private_key_path).unwrap();
std::fs::remove_file(public_key_path).unwrap();
}
for (dkg_client, state) in clients_and_states.iter_mut() {
verification_key_validation(dkg_client, state, false)
.await
.unwrap();
}
for (dkg_client, state) in clients_and_states.iter_mut() {
verification_key_finalization(dkg_client, state, false)
.await
.unwrap();
}
assert!(db
.proposal_db
.read()
.unwrap()
.values()
.all(|proposal| { proposal.status == Status::Executed }));
for (_, state) in clients_and_states.iter_mut() {
state.set_was_in_progress();
}
// DKG in reshare mode
let params = Parameters::new(4).unwrap();
let mut vks = vec![];
let mut indices = vec![];
for (_, state) in clients_and_states.iter() {
let vk = state
.coconut_secret_key()
.await
.unwrap()
.verification_key(&params);
let index = state.node_index().unwrap();
vks.push(vk);
indices.push(index);
}
let initial_master_vk = aggregate_verification_keys(&vks, Some(&indices)).unwrap();
for (_, active) in db.dealer_details_db.write().unwrap().values_mut() {
*active = false;
}
*db.dealings_db.write().unwrap() = Default::default();
*db.verification_share_db.write().unwrap() = Default::default();
let mut initial_dealers = vec![];
for (dkg_client, _) in clients_and_states.iter() {
let client_address = Addr::unchecked(dkg_client.get_address().await.as_ref());
initial_dealers.push(client_address);
}
*db.initial_dealers_db.write().unwrap() = Some(InitialReplacementData {
initial_dealers,
initial_height: 1,
});
*clients_and_states.last_mut().unwrap() = (initial_client2, initial_state2);
for (dkg_client, state) in clients_and_states.iter_mut() {
public_key_submission(dkg_client, state, true)
.await
.unwrap();
}
for (dkg_client, state) in clients_and_states.iter_mut() {
dealing_exchange(dkg_client, state, OsRng, true)
.await
.unwrap();
}
for (dkg_client, state) in clients_and_states.iter_mut() {
let random_file: usize = OsRng.gen();
let private_key_path = temp_dir().join(format!("private{}.pem", random_file));
let public_key_path = temp_dir().join(format!("public{}.pem", random_file));
let keypair_path = KeyPairPath::new(private_key_path.clone(), public_key_path.clone());
verification_key_submission(dkg_client, state, &keypair_path, true)
.await
.unwrap();
std::fs::remove_file(private_key_path).unwrap();
std::fs::remove_file(public_key_path).unwrap();
}
for (dkg_client, state) in clients_and_states.iter_mut() {
verification_key_validation(dkg_client, state, true)
.await
.unwrap();
}
for (dkg_client, state) in clients_and_states.iter_mut() {
verification_key_finalization(dkg_client, state, true)
.await
.unwrap();
}
// assert!(db
// .proposal_db
// .read()
// .unwrap()
// .values()
// .all(|proposal| { proposal.status == Status::Executed }));
let mut vks = vec![];
let mut indices = vec![];
for (_, state) in clients_and_states.iter() {
+1 -1
View File
@@ -91,7 +91,7 @@ pub enum CoconutError {
#[error("Failed to recover assigned node index: {reason}")]
NodeIndexRecoveryError { reason: String },
#[error("Unrecoverable state: {reason}. Process should be restarted")]
#[error("Unrecoverable state: {reason}")]
UnrecoverableState { reason: String },
#[error("DKG has not finished yet in order to derive the coconut key")]
+52 -27
View File
@@ -69,7 +69,7 @@ pub(crate) struct DummyClient {
spent_credential_db: Arc<RwLock<HashMap<String, SpendCredentialResponse>>>,
epoch: Arc<RwLock<Epoch>>,
dealer_details: Arc<RwLock<HashMap<String, DealerDetails>>>,
dealer_details: Arc<RwLock<HashMap<String, (DealerDetails, bool)>>>,
threshold: Arc<RwLock<Option<Threshold>>>,
dealings: Arc<RwLock<HashMap<String, Vec<ContractSafeBytes>>>>,
verification_share: Arc<RwLock<HashMap<String, ContractVKShare>>>,
@@ -122,7 +122,7 @@ impl DummyClient {
pub fn with_dealer_details(
mut self,
dealer_details: &Arc<RwLock<HashMap<String, DealerDetails>>>,
dealer_details: &Arc<RwLock<HashMap<String, (DealerDetails, bool)>>>,
) -> Self {
self.dealer_details = Arc::clone(dealer_details);
self
@@ -233,14 +233,25 @@ impl super::client::Client for DummyClient {
}
async fn get_self_registered_dealer_details(&self) -> Result<DealerDetailsResponse> {
let (details, dealer_type) = if let Some((details, current)) = self
.dealer_details
.read()
.unwrap()
.get(self.validator_address.as_ref())
.cloned()
{
let dealer_type = if current {
DealerType::Current
} else {
DealerType::Past
};
(Some(details), dealer_type)
} else {
(None, DealerType::Unknown)
};
Ok(DealerDetailsResponse {
details: self
.dealer_details
.read()
.unwrap()
.get(self.validator_address.as_ref())
.cloned(),
dealer_type: DealerType::Current,
details,
dealer_type,
})
}
@@ -251,6 +262,7 @@ impl super::client::Client for DummyClient {
.unwrap()
.values()
.cloned()
.filter_map(|(d, current)| if current { Some(d) } else { None })
.collect())
}
@@ -287,13 +299,11 @@ impl super::client::Client for DummyClient {
_fee: Option<Fee>,
) -> Result<()> {
if let Some(proposal) = self.proposal_db.write().unwrap().get_mut(&proposal_id) {
// for now, just suppose that first vote is honest
if proposal.status == cw3::Status::Open {
if vote_yes {
proposal.status = cw3::Status::Passed;
} else {
proposal.status = cw3::Status::Rejected;
}
// for now, just suppose that every vote is honest
if !vote_yes {
proposal.status = cw3::Status::Rejected;
} else if vote_yes && proposal.status == cw3::Status::Open {
proposal.status = cw3::Status::Passed;
}
}
Ok(())
@@ -323,22 +333,33 @@ impl super::client::Client for DummyClient {
_resharing: bool,
) -> Result<ExecuteResult> {
let mut dealer_details = self.dealer_details.write().unwrap();
let assigned_index =
if let Some(details) = dealer_details.get(self.validator_address.as_ref()) {
details.assigned_index
} else {
let assigned_index = OsRng.gen();
dealer_details.insert(
self.validator_address.to_string(),
let assigned_index = if let Some((details, active)) =
dealer_details.get_mut(self.validator_address.as_ref())
{
*active = true;
details.assigned_index
} else {
// let assigned_index = OsRng.gen();
let assigned_index = dealer_details
.values()
.map(|(d, _)| d.assigned_index)
.max()
.unwrap_or(0)
+ 1;
dealer_details.insert(
self.validator_address.to_string(),
(
DealerDetails {
address: Addr::unchecked(self.validator_address.to_string()),
bte_public_key_with_proof,
announce_address,
assigned_index,
},
);
assigned_index
};
true,
),
);
assigned_index
};
Ok(ExecuteResult {
logs: vec![Log {
msg_index: 0,
@@ -380,13 +401,17 @@ impl super::client::Client for DummyClient {
share: VerificationKeyShare,
resharing: bool,
) -> Result<ExecuteResult> {
let dealer_details = self
let (dealer_details, active) = self
.dealer_details
.read()
.unwrap()
.get(self.validator_address.as_ref())
.unwrap()
.clone();
if !active {
// Just throw some error, not really the correct one
return Err(CoconutError::DepositEncrKeyNotFound);
}
self.verification_share.write().unwrap().insert(
self.validator_address.to_string(),
ContractVKShare {
@@ -38,7 +38,6 @@ pub(crate) async fn execute(
nym_cli_commands::validator::mixnet::delegators::MixnetDelegatorsCommands::List(args) => {
nym_cli_commands::validator::mixnet::delegators::query_for_delegations::execute(args, create_signing_client_with_nym_api(global_args, network_details)?).await
}
_ => unreachable!(),
}
Ok(())
}