mirror of
https://github.com/alexgo-io/stacks-puppet-node.git
synced 2026-01-12 16:53:21 +08:00
feat: (Signer) Persist encrypted dkg shares in StackerDB
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3463,6 +3463,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"serde_stacker",
|
||||
"sha2 0.10.8",
|
||||
"slog",
|
||||
"slog-json",
|
||||
"slog-term",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:?}");
|
||||
|
||||
Reference in New Issue
Block a user