feat: (Signer) Persist encrypted dkg shares in StackerDB

This commit is contained in:
Mårten Blankfors
2024-04-04 14:15:33 +02:00
parent 5d2cbdd1ab
commit 952275aba6
7 changed files with 158 additions and 10 deletions

1
Cargo.lock generated
View File

@@ -3463,6 +3463,7 @@ dependencies = [
"serde_derive",
"serde_json",
"serde_stacker",
"sha2 0.10.8",
"slog",
"slog-json",
"slog-term",

View File

@@ -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<u8>),
}
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)
}

View File

@@ -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;
}

View File

@@ -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"

View File

@@ -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<Option<Vec<u8>>, 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::<SignerMessage, _>(&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")

View File

@@ -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<SignerConfig> 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<SignerConfig> 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<u8> {
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<u8> {
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);
}
}

View File

@@ -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:?}");