Feature/dkg state to disk (#1872)

* Add PersistentState

* Save and load state to/from disk

* If in progress, don't continually write the same state

* Fix tests and add serde one

* Update changelog

* Fix clippy
This commit is contained in:
Bogdan-Ștefan Neacşu
2022-12-09 13:00:27 +02:00
committed by GitHub
parent 85515fe16e
commit ebaf99fadb
12 changed files with 247 additions and 29 deletions
+3 -1
View File
@@ -1,7 +1,9 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[derive(Debug, Eq, PartialEq)]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) enum ComplaintReason {
MalformedBTEPublicKey,
InvalidBTEPublicKey,
+17 -2
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::coconut::dkg::client::DkgClient;
use crate::coconut::dkg::state::{ConsistentState, State};
use crate::coconut::dkg::state::{ConsistentState, PersistentState, State};
use crate::coconut::dkg::verification_key::{
verification_key_finalization, verification_key_validation,
};
@@ -63,12 +63,20 @@ impl<R: RngCore + Clone> DkgController<R> {
)) {
coconut_keypair.set(coconut_keypair_value).await;
}
let persistent_state =
PersistentState::load_from_file(config.persistent_state_path()).unwrap_or_default();
Ok(DkgController {
dkg_client: DkgClient::new(nymd_client),
secret_key_path: config.secret_key_path(),
verification_key_path: config.verification_key_path(),
state: State::new(config.get_announce_address(), dkg_keypair, coconut_keypair),
state: State::new(
config.persistent_state_path(),
persistent_state,
config.get_announce_address(),
dkg_keypair,
coconut_keypair,
),
rng,
polling_rate: config.get_dkg_contract_polling_rate(),
})
@@ -114,6 +122,13 @@ impl<R: RngCore + Clone> DkgController<R> {
};
if let Err(e) = ret {
warn!("Could not handle this iteration for the epoch state: {}", e);
} else if epoch_state != EpochState::InProgress {
let persistent_state = PersistentState::from(&self.state);
if let Err(e) =
persistent_state.save_to_file(self.state.persistent_state_path())
{
warn!("Could not backup the state for this iteration: {}", e);
}
}
}
}
+6
View File
@@ -55,6 +55,7 @@ pub(crate) async fn dealing_exchange(
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 coconut_dkg_common::dealer::DealerDetails;
@@ -63,6 +64,7 @@ pub(crate) mod tests {
use dkg::bte::Params;
use rand::rngs::OsRng;
use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, RwLock};
use url::Url;
@@ -110,6 +112,8 @@ pub(crate) mod tests {
);
let params = setup();
let mut state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
DkgKeyPair::new(&params, OsRng),
KeyPair::new(),
@@ -163,6 +167,8 @@ pub(crate) mod tests {
);
let params = setup();
let mut state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
DkgKeyPair::new(&params, OsRng),
KeyPair::new(),
@@ -34,10 +34,12 @@ pub(crate) async fn public_key_submission(
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::coconut::dkg::state::PersistentState;
use crate::coconut::tests::DummyClient;
use crate::coconut::KeyPair;
use dkg::bte::keys::KeyPair as DkgKeyPair;
use rand::rngs::OsRng;
use std::path::PathBuf;
use std::str::FromStr;
use url::Url;
use validator_client::nymd::AccountId;
@@ -51,6 +53,8 @@ pub(crate) mod tests {
AccountId::from_str(TEST_VALIDATOR_ADDRESS).unwrap(),
));
let mut state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
DkgKeyPair::new(&dkg::bte::setup(), OsRng),
KeyPair::new(),
+112 -23
View File
@@ -9,13 +9,33 @@ use coconut_dkg_common::types::EpochState;
use cosmwasm_std::Addr;
use dkg::bte::{keys::KeyPair as DkgKeyPair, PublicKey, PublicKeyWithProof};
use dkg::{NodeIndex, RecoveredVerificationKeys, Threshold};
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::BTreeMap;
use std::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(Debug)]
#[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,
}
@@ -71,20 +91,6 @@ pub(crate) trait ConsistentState {
}
}
pub(crate) struct State {
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,
}
#[async_trait]
impl ConsistentState for State {
fn node_index_value(&self) -> Result<NodeIndex, CoconutError> {
@@ -131,27 +137,110 @@ impl ConsistentState for State {
}
}
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,
}
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,
}
}
}
impl PersistentState {
pub fn save_to_file(&self, path: PathBuf) -> Result<(), CoconutError> {
std::fs::write(path, serde_json::to_string(self)?)?;
Ok(())
}
pub fn load_from_file(path: PathBuf) -> 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,
}
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: None,
dealers: BTreeMap::new(),
receiver_index: None,
threshold: None,
recovered_vks: vec![],
proposal_id: None,
voted_vks: false,
executed_proposal: false,
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,
}
}
pub fn persistent_state_path(&self) -> PathBuf {
self.persistent_state_path.clone()
}
pub fn announce_address(&self) -> &Url {
&self.announce_address
}
@@ -249,6 +249,7 @@ pub(crate) mod tests {
use super::*;
use crate::coconut::dkg::dealing::dealing_exchange;
use crate::coconut::dkg::public_key::public_key_submission;
use crate::coconut::dkg::state::PersistentState;
use crate::coconut::tests::DummyClient;
use crate::coconut::KeyPair;
use coconut_dkg_common::dealer::DealerDetails;
@@ -259,6 +260,7 @@ pub(crate) mod tests {
use rand::Rng;
use std::collections::HashMap;
use std::env::temp_dir;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, RwLock};
use url::Url;
@@ -289,6 +291,8 @@ pub(crate) mod tests {
);
let keypair = DkgKeyPair::new(&params, OsRng);
let state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
keypair,
KeyPair::new(),
+3
View File
@@ -23,6 +23,9 @@ pub enum CoconutError {
#[error("{0}")]
IOError(#[from] std::io::Error),
#[error("{0}")]
SerdeJsonError(#[from] serde_json::Error),
#[error("Could not parse Ed25519 data")]
Ed25519ParseError(#[from] Ed25519RecoveryError),
+16
View File
@@ -282,6 +282,9 @@ pub struct CoconutSigner {
/// Specifies whether rewarding service is enabled in this process.
enabled: bool,
/// Path to a JSON file where state is persisted between different stages of DKG.
dkg_persistent_state_path: PathBuf,
/// Path to the coconut verification key.
verification_key_path: PathBuf,
@@ -299,6 +302,7 @@ pub struct CoconutSigner {
}
impl CoconutSigner {
pub const DKG_PERSISTENT_STATE_FILE: &'static str = "dkg_persistent_state.json";
pub const DKG_DECRYPTION_KEY_FILE: &'static str = "dkg_decryption_key.pem";
pub const DKG_PUBLIC_KEY_WITH_PROOF_FILE: &'static str = "dkg_public_key_with_proof.pem";
pub const COCONUT_VERIFICATION_KEY_FILE: &'static str = "coconut_verification_key.pem";
@@ -312,6 +316,10 @@ impl CoconutSigner {
Config::default_data_directory(None).join(Self::COCONUT_SECRET_KEY_FILE)
}
fn default_dkg_persistent_state_path() -> PathBuf {
Config::default_data_directory(None).join(Self::DKG_PERSISTENT_STATE_FILE)
}
fn default_dkg_decryption_key_path() -> PathBuf {
Config::default_data_directory(None).join(Self::DKG_DECRYPTION_KEY_FILE)
}
@@ -325,6 +333,7 @@ impl Default for CoconutSigner {
fn default() -> Self {
Self {
enabled: Default::default(),
dkg_persistent_state_path: CoconutSigner::default_dkg_persistent_state_path(),
verification_key_path: CoconutSigner::default_coconut_verification_key_path(),
secret_key_path: CoconutSigner::default_coconut_secret_key_path(),
decryption_key_path: CoconutSigner::default_dkg_decryption_key_path(),
@@ -345,6 +354,8 @@ impl Config {
Config::default_data_directory(Some(id)).join(NodeStatusAPI::DB_FILE);
self.network_monitor.credentials_database_path =
Config::default_data_directory(Some(id)).join(NetworkMonitor::DB_FILE);
self.coconut_signer.dkg_persistent_state_path =
Config::default_data_directory(Some(id)).join(CoconutSigner::DKG_PERSISTENT_STATE_FILE);
self.coconut_signer.verification_key_path = Config::default_data_directory(Some(id))
.join(CoconutSigner::COCONUT_VERIFICATION_KEY_FILE);
self.coconut_signer.secret_key_path =
@@ -509,6 +520,11 @@ impl Config {
self.node_status_api.database_path.clone()
}
#[cfg(feature = "coconut")]
pub fn persistent_state_path(&self) -> PathBuf {
self.coconut_signer.dkg_persistent_state_path.clone()
}
#[cfg(feature = "coconut")]
pub fn verification_key_path(&self) -> PathBuf {
self.coconut_signer.verification_key_path.clone()