From 952275aba6a326fb6b694e467cf018fe9a4b7d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Thu, 4 Apr 2024 14:15:33 +0200 Subject: [PATCH] feat: (Signer) Persist encrypted dkg shares in StackerDB --- Cargo.lock | 1 + libsigner/src/messages.rs | 23 ++++- stacks-common/src/libcommon.rs | 2 +- stacks-signer/Cargo.toml | 1 + stacks-signer/src/client/stackerdb.rs | 51 ++++++++++- stacks-signer/src/signer.rs | 89 ++++++++++++++++++- .../src/nakamoto_node/sign_coordinator.rs | 1 + 7 files changed, 158 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9cfd1ad9a..ce518fc3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3463,6 +3463,7 @@ dependencies = [ "serde_derive", "serde_json", "serde_stacker", + "sha2 0.10.8", "slog", "slog-json", "slog-term", diff --git a/libsigner/src/messages.rs b/libsigner/src/messages.rs index 1b6e7f179..86bfbabad 100644 --- a/libsigner/src/messages.rs +++ b/libsigner/src/messages.rs @@ -94,14 +94,17 @@ MessageSlotID { /// Transactions list for miners and signers to observe Transactions = 11, /// DKG Results - DkgResults = 12 + DkgResults = 12, + /// Persisted encrypted signer state containing DKG shares + EncryptedSignerState = 13 }); define_u8_enum!(SignerMessageTypePrefix { BlockResponse = 0, Packet = 1, Transactions = 2, - DkgResults = 3 + DkgResults = 3, + EncryptedSignerState = 4 }); impl MessageSlotID { @@ -142,6 +145,9 @@ impl From<&SignerMessage> for SignerMessageTypePrefix { SignerMessage::BlockResponse(_) => SignerMessageTypePrefix::BlockResponse, SignerMessage::Transactions(_) => SignerMessageTypePrefix::Transactions, SignerMessage::DkgResults { .. } => SignerMessageTypePrefix::DkgResults, + SignerMessage::EncryptedSignerState { .. } => { + SignerMessageTypePrefix::EncryptedSignerState + } } } } @@ -234,6 +240,8 @@ pub enum SignerMessage { /// The polynomial commits used to construct the aggregate key party_polynomials: Vec<(u32, PolyCommitment)>, }, + /// The encrypted state of the signer to be persisted + EncryptedSignerState(Vec), } impl Debug for SignerMessage { @@ -255,6 +263,9 @@ impl Debug for SignerMessage { .field("party_polynomials", &party_polynomials) .finish() } + Self::EncryptedSignerState(s) => { + f.debug_tuple("EncryptedSignerState").field(s).finish() + } } } } @@ -278,6 +289,7 @@ impl SignerMessage { Self::BlockResponse(_) => MessageSlotID::BlockResponse, Self::Transactions(_) => MessageSlotID::Transactions, Self::DkgResults { .. } => MessageSlotID::DkgResults, + Self::EncryptedSignerState(_) => MessageSlotID::EncryptedSignerState, } } } @@ -345,6 +357,9 @@ impl StacksMessageCodec for SignerMessage { party_polynomials.iter().map(|(a, b)| (a, b)), )?; } + SignerMessage::EncryptedSignerState(encrypted_state) => { + write_next(fd, encrypted_state)?; + } }; Ok(()) } @@ -383,6 +398,10 @@ impl StacksMessageCodec for SignerMessage { party_polynomials, } } + SignerMessageTypePrefix::EncryptedSignerState => { + let encrypted_state = read_next::<_, _>(fd)?; + SignerMessage::EncryptedSignerState(encrypted_state) + } }; Ok(message) } diff --git a/stacks-common/src/libcommon.rs b/stacks-common/src/libcommon.rs index 0a9fa9d64..37e00d540 100644 --- a/stacks-common/src/libcommon.rs +++ b/stacks-common/src/libcommon.rs @@ -62,5 +62,5 @@ pub mod consts { /// The number of StackerDB slots each signing key needs /// to use to participate in DKG and block validation signing. - pub const SIGNER_SLOTS_PER_USER: u32 = 13; + pub const SIGNER_SLOTS_PER_USER: u32 = 14; } diff --git a/stacks-signer/Cargo.toml b/stacks-signer/Cargo.toml index 861e9204f..86cb75bb9 100644 --- a/stacks-signer/Cargo.toml +++ b/stacks-signer/Cargo.toml @@ -31,6 +31,7 @@ reqwest = { version = "0.11.22", default-features = false, features = ["blocking serde = "1" serde_derive = "1" serde_stacker = "0.1" +sha2 = "0.10" slog = { version = "2.5.2", features = [ "max_level_trace" ] } slog-json = { version = "2.3.0", optional = true } slog-term = "2.6.0" diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index 4b6e993bc..09e623143 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -19,10 +19,10 @@ use blockstack_lib::net::api::poststackerdbchunk::StackerDBErrorCodes; use hashbrown::HashMap; use libsigner::{MessageSlotID, SignerMessage, SignerSession, StackerDBSession}; use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; -use slog::{slog_debug, slog_warn}; +use slog::{slog_debug, slog_error, slog_warn}; use stacks_common::codec::{read_next, StacksMessageCodec}; use stacks_common::types::chainstate::StacksPrivateKey; -use stacks_common::{debug, warn}; +use stacks_common::{debug, error, warn}; use super::ClientError; use crate::client::retry_with_exponential_backoff; @@ -130,7 +130,7 @@ impl StackerDB { }; debug!( - "Sending a chunk to stackerdb slot ID {slot_id} with version {slot_version} to contract {:?}!\n{chunk:?}", + "Sending a chunk to stackerdb slot ID {slot_id} with version {slot_version} and message ID {msg_id} to contract {:?}!\n{chunk:?}", &session.stackerdb_contract_id ); @@ -243,6 +243,51 @@ impl StackerDB { Self::get_transactions(&mut self.next_transaction_session, signer_ids) } + /// Get the encrypted state for the given signer + pub fn get_encrypted_signer_state( + &mut self, + signer_id: SignerSlotID, + ) -> Result>, ClientError> { + debug!("Getting the persisted encrypted state for signer {signer_id}"); + let Some(state_session) = self + .signers_message_stackerdb_sessions + .get_mut(&MessageSlotID::EncryptedSignerState) + else { + return Err(ClientError::NotConnected); + }; + + let send_request = || { + state_session + .get_latest_chunks(&[signer_id.0]) + .map_err(backoff::Error::transient) + }; + + let Some(chunk) = retry_with_exponential_backoff(send_request)?.pop().ok_or( + ClientError::UnexpectedResponseFormat(format!( + "Missing response for state session request for signer {}", + signer_id + )), + )? + else { + debug!("No persisted state for signer {signer_id}"); + return Ok(None); + }; + + if chunk.is_empty() { + debug!("Empty persisted state for signer {signer_id}"); + return Ok(None); + } + + let SignerMessage::EncryptedSignerState(state) = + read_next::(&mut chunk.as_slice())? + else { + error!("Wrong message type stored in signer state slot for signer {signer_id}"); + return Ok(None); + }; + + Ok(Some(state)) + } + /// Retrieve the signer set this stackerdb client is attached to pub fn get_signer_set(&self) -> u32 { u32::try_from(self.reward_cycle % 2).expect("FATAL: reward cycle % 2 exceeds u32::MAX") diff --git a/stacks-signer/src/signer.rs b/stacks-signer/src/signer.rs index 3da523eea..389daedbd 100644 --- a/stacks-signer/src/signer.rs +++ b/stacks-signer/src/signer.rs @@ -28,7 +28,9 @@ use libsigner::{ BlockProposalSigners, BlockRejection, BlockResponse, MessageSlotID, RejectCode, SignerEvent, SignerMessage, }; +use rand_core::OsRng; use serde_derive::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use slog::{slog_debug, slog_error, slog_info, slog_warn}; use stacks_common::codec::{read_next, StacksMessageCodec}; use stacks_common::types::chainstate::{ConsensusHash, StacksAddress}; @@ -38,6 +40,7 @@ use stacks_common::{debug, error, info, warn}; use wsts::common::{MerkleRoot, Signature}; use wsts::curve::keys::PublicKey; use wsts::curve::point::Point; +use wsts::curve::scalar::Scalar; use wsts::net::{Message, NonceRequest, Packet, SignatureShareRequest}; use wsts::state_machine::coordinator::fire::Coordinator as FireCoordinator; use wsts::state_machine::coordinator::{ @@ -219,7 +222,7 @@ impl Signer { impl From for Signer { fn from(signer_config: SignerConfig) -> Self { - let stackerdb = StackerDB::from(&signer_config); + let mut stackerdb = StackerDB::from(&signer_config); let num_signers = signer_config .signer_entries @@ -276,6 +279,20 @@ impl From for Signer { signer_config.signer_entries.public_keys, ); + if let Some(encrypted_state) = stackerdb + .get_encrypted_signer_state(signer_config.signer_slot_id.into()) + .expect("Failed to load encrypted signer state from StackerDB") + { + let serialized_state = decrypt(&state_machine.network_private_key, &encrypted_state); + let state = serde_json::from_slice(&serialized_state) + .expect("Failed to deserialize decryoted state"); + debug!( + "Reward cycle #{} Signer #{}: Loading signer", + signer_config.reward_cycle, signer_config.signer_id + ); + state_machine.signer = v2::Signer::load(&state); + } + if let Some(state) = signer_db .get_signer_state(signer_config.reward_cycle) .expect("Failed to load signer state") @@ -574,6 +591,7 @@ impl Signer { .filter_map(|msg| match msg { SignerMessage::DkgResults { .. } | SignerMessage::BlockResponse(_) + | SignerMessage::EncryptedSignerState { .. } | SignerMessage::Transactions(_) => None, // TODO: if a signer tries to trigger DKG and we already have one set in the contract, ignore the request. SignerMessage::Packet(packet) => { @@ -701,8 +719,14 @@ impl Signer { self.update_operation(); } - debug!("{self}: Saving signer state"); - self.save_signer_state(); + if packets.iter().any(|packet| match packet.msg { + Message::DkgEnd(_) => true, + _ => false, + }) { + debug!("{self}: Saving signer state"); + self.save_signer_state_in_signerdb(); + self.save_signer_state_in_stackerdb(); + } self.send_outbound_messages(signer_outbound_messages); self.send_outbound_messages(coordinator_outbound_messages); } @@ -1233,13 +1257,32 @@ impl Signer { /// /// # Panics /// Panics if the insertion fails - fn save_signer_state(&self) { + fn save_signer_state_in_signerdb(&self) { let state = self.state_machine.signer.save(); self.signer_db .insert_signer_state(self.reward_cycle, &state) .expect("Failed to persist signer state"); } + /// Persist state needed to ensure the signer can continue to perform + /// DKG and participate in signing rounds accross crashes + /// + /// # Panics + /// Panics if the encryption fails or if the insertion into stackerDB fails + fn save_signer_state_in_stackerdb(&mut self) { + let state = self.state_machine.signer.save(); + let serialized_state = + serde_json::to_vec(&state).expect("Failed to serialize signer state"); + + let encrypted_state = encrypt(&self.state_machine.network_private_key, &serialized_state); + + let message = SignerMessage::EncryptedSignerState(encrypted_state); + + self.stackerdb + .send_message_with_retry(message) + .expect("Failed to send encrypted state to stackerdb"); + } + /// Send any operation results across the provided channel fn send_operation_results( &mut self, @@ -1453,3 +1496,41 @@ impl Signer { Ok(()) } } + +fn encrypt(private_key: &Scalar, msg: &[u8]) -> Vec { + wsts::util::encrypt(&derive_encryption_key(private_key), msg, &mut OsRng) + .expect("Failed to encrypt message") +} + +fn decrypt(private_key: &Scalar, encrypted_msg: &[u8]) -> Vec { + wsts::util::decrypt(&derive_encryption_key(private_key), encrypted_msg) + .expect("Failed to decrypt message") +} + +fn derive_encryption_key(private_key: &Scalar) -> [u8; 32] { + let mut hasher = Sha256::new(); + + hasher.update("SIGNER_STATE_ENCRYPTION_KEY/".as_bytes()); + hasher.update(private_key.to_bytes()); + + hasher.finalize().into() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encrypted_messages_should_be_possible_to_decrypt() { + let msg = "Nobody's gonna know".as_bytes(); + let key = Scalar::random(&mut OsRng); + + let encrypted = encrypt(&key, msg); + + assert_ne!(encrypted, msg); + + let decrypted = decrypt(&key, &encrypted); + + assert_eq!(decrypted, msg); + } +} diff --git a/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs b/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs index b1118bebf..f3b2d002a 100644 --- a/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs +++ b/testnet/stacks-node/src/nakamoto_node/sign_coordinator.rs @@ -407,6 +407,7 @@ impl SignCoordinator { .filter_map(|msg| match msg { SignerMessage::DkgResults { .. } | SignerMessage::BlockResponse(_) + | SignerMessage::EncryptedSignerState { .. } | SignerMessage::Transactions(_) => None, SignerMessage::Packet(packet) => { debug!("Received signers packet: {packet:?}");