Skip to content

Commit 34b97e1

Browse files
authored
Merge pull request #4659 from stacks-network/4595-nakamoto-stacks-signer-should-store-its-dkg-shares-in-stackerdb-to-enable-disaster-recovery
feat: Signer using StackerDB for persisting DKG shares
2 parents 71abebc + 53cf49d commit 34b97e1

File tree

9 files changed

+352
-87
lines changed

9 files changed

+352
-87
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libsigner/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ tiny_http = "0.12"
3333
wsts = { workspace = true }
3434

3535
[dev-dependencies]
36+
mutants = "0.0.3"
3637
rand_core = { workspace = true }
3738
rand = { workspace = true }
3839

libsigner/src/messages.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use blockstack_lib::net::api::postblock_proposal::{
3838
BlockValidateReject, BlockValidateResponse, ValidateRejectCode,
3939
};
4040
use blockstack_lib::util_lib::boot::boot_code_id;
41+
use clarity::util::retry::BoundReader;
4142
use clarity::vm::types::serialization::SerializationError;
4243
use clarity::vm::types::QualifiedContractIdentifier;
4344
use hashbrown::{HashMap, HashSet};
@@ -94,14 +95,17 @@ MessageSlotID {
9495
/// Transactions list for miners and signers to observe
9596
Transactions = 11,
9697
/// DKG Results
97-
DkgResults = 12
98+
DkgResults = 12,
99+
/// Persisted encrypted signer state containing DKG shares
100+
EncryptedSignerState = 13
98101
});
99102

100103
define_u8_enum!(SignerMessageTypePrefix {
101104
BlockResponse = 0,
102105
Packet = 1,
103106
Transactions = 2,
104-
DkgResults = 3
107+
DkgResults = 3,
108+
EncryptedSignerState = 4
105109
});
106110

107111
impl MessageSlotID {
@@ -136,12 +140,14 @@ impl TryFrom<u8> for SignerMessageTypePrefix {
136140
}
137141

138142
impl From<&SignerMessage> for SignerMessageTypePrefix {
143+
#[cfg_attr(test, mutants::skip)]
139144
fn from(message: &SignerMessage) -> Self {
140145
match message {
141146
SignerMessage::Packet(_) => SignerMessageTypePrefix::Packet,
142147
SignerMessage::BlockResponse(_) => SignerMessageTypePrefix::BlockResponse,
143148
SignerMessage::Transactions(_) => SignerMessageTypePrefix::Transactions,
144149
SignerMessage::DkgResults { .. } => SignerMessageTypePrefix::DkgResults,
150+
SignerMessage::EncryptedSignerState(_) => SignerMessageTypePrefix::EncryptedSignerState,
145151
}
146152
}
147153
}
@@ -234,9 +240,12 @@ pub enum SignerMessage {
234240
/// The polynomial commits used to construct the aggregate key
235241
party_polynomials: Vec<(u32, PolyCommitment)>,
236242
},
243+
/// The encrypted state of the signer to be persisted
244+
EncryptedSignerState(Vec<u8>),
237245
}
238246

239247
impl Debug for SignerMessage {
248+
#[cfg_attr(test, mutants::skip)]
240249
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241250
match self {
242251
Self::BlockResponse(b) => Debug::fmt(b, f),
@@ -255,12 +264,16 @@ impl Debug for SignerMessage {
255264
.field("party_polynomials", &party_polynomials)
256265
.finish()
257266
}
267+
Self::EncryptedSignerState(s) => {
268+
f.debug_tuple("EncryptedSignerState").field(s).finish()
269+
}
258270
}
259271
}
260272
}
261273

262274
impl SignerMessage {
263275
/// Helper function to determine the slot ID for the provided stacker-db writer id
276+
#[cfg_attr(test, mutants::skip)]
264277
pub fn msg_id(&self) -> MessageSlotID {
265278
match self {
266279
Self::Packet(packet) => match packet.msg {
@@ -278,6 +291,7 @@ impl SignerMessage {
278291
Self::BlockResponse(_) => MessageSlotID::BlockResponse,
279292
Self::Transactions(_) => MessageSlotID::Transactions,
280293
Self::DkgResults { .. } => MessageSlotID::DkgResults,
294+
Self::EncryptedSignerState(_) => MessageSlotID::EncryptedSignerState,
281295
}
282296
}
283297
}
@@ -345,10 +359,14 @@ impl StacksMessageCodec for SignerMessage {
345359
party_polynomials.iter().map(|(a, b)| (a, b)),
346360
)?;
347361
}
362+
SignerMessage::EncryptedSignerState(encrypted_state) => {
363+
write_next(fd, encrypted_state)?;
364+
}
348365
};
349366
Ok(())
350367
}
351368

369+
#[cfg_attr(test, mutants::skip)]
352370
fn consensus_deserialize<R: Read>(fd: &mut R) -> Result<Self, CodecError> {
353371
let type_prefix_byte = read_next::<u8, _>(fd)?;
354372
let type_prefix = SignerMessageTypePrefix::try_from(type_prefix_byte)?;
@@ -383,6 +401,15 @@ impl StacksMessageCodec for SignerMessage {
383401
party_polynomials,
384402
}
385403
}
404+
SignerMessageTypePrefix::EncryptedSignerState => {
405+
// Typically the size of the signer state is much smaller, but in the fully degenerate case the size of the persisted state is
406+
// 2800 * 32 * 4 + C for some small constant C.
407+
// To have some margin, we're expanding the left term with an additional factor 4
408+
let max_encrypted_state_size = 2800 * 32 * 4 * 4;
409+
let mut bound_reader = BoundReader::from_reader(fd, max_encrypted_state_size);
410+
let encrypted_state = read_next::<_, _>(&mut bound_reader)?;
411+
SignerMessage::EncryptedSignerState(encrypted_state)
412+
}
386413
};
387414
Ok(message)
388415
}

stacks-common/src/libcommon.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,5 @@ pub mod consts {
6262

6363
/// The number of StackerDB slots each signing key needs
6464
/// to use to participate in DKG and block validation signing.
65-
pub const SIGNER_SLOTS_PER_USER: u32 = 13;
65+
pub const SIGNER_SLOTS_PER_USER: u32 = 14;
6666
}

stacks-signer/src/client/stackerdb.rs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ use blockstack_lib::net::api::poststackerdbchunk::StackerDBErrorCodes;
1919
use hashbrown::HashMap;
2020
use libsigner::{MessageSlotID, SignerMessage, SignerSession, StackerDBSession};
2121
use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData};
22-
use slog::{slog_debug, slog_warn};
22+
use slog::{slog_debug, slog_error, slog_warn};
2323
use stacks_common::codec::{read_next, StacksMessageCodec};
2424
use stacks_common::types::chainstate::StacksPrivateKey;
25-
use stacks_common::{debug, warn};
25+
use stacks_common::{debug, error, warn};
2626

2727
use super::ClientError;
2828
use crate::client::retry_with_exponential_backoff;
@@ -130,7 +130,7 @@ impl StackerDB {
130130
};
131131

132132
debug!(
133-
"Sending a chunk to stackerdb slot ID {slot_id} with version {slot_version} to contract {:?}!\n{chunk:?}",
133+
"Sending a chunk to stackerdb slot ID {slot_id} with version {slot_version} and message ID {msg_id} to contract {:?}!\n{chunk:?}",
134134
&session.stackerdb_contract_id
135135
);
136136

@@ -243,6 +243,51 @@ impl StackerDB {
243243
Self::get_transactions(&mut self.next_transaction_session, signer_ids)
244244
}
245245

246+
/// Get the encrypted state for the given signer
247+
pub fn get_encrypted_signer_state(
248+
&mut self,
249+
signer_id: SignerSlotID,
250+
) -> Result<Option<Vec<u8>>, ClientError> {
251+
debug!("Getting the persisted encrypted state for signer {signer_id}");
252+
let Some(state_session) = self
253+
.signers_message_stackerdb_sessions
254+
.get_mut(&MessageSlotID::EncryptedSignerState)
255+
else {
256+
return Err(ClientError::NotConnected);
257+
};
258+
259+
let send_request = || {
260+
state_session
261+
.get_latest_chunks(&[signer_id.0])
262+
.map_err(backoff::Error::transient)
263+
};
264+
265+
let Some(chunk) = retry_with_exponential_backoff(send_request)?.pop().ok_or(
266+
ClientError::UnexpectedResponseFormat(format!(
267+
"Missing response for state session request for signer {}",
268+
signer_id
269+
)),
270+
)?
271+
else {
272+
debug!("No persisted state for signer {signer_id}");
273+
return Ok(None);
274+
};
275+
276+
if chunk.is_empty() {
277+
debug!("Empty persisted state for signer {signer_id}");
278+
return Ok(None);
279+
}
280+
281+
let SignerMessage::EncryptedSignerState(state) =
282+
read_next::<SignerMessage, _>(&mut chunk.as_slice())?
283+
else {
284+
error!("Wrong message type stored in signer state slot for signer {signer_id}");
285+
return Ok(None);
286+
};
287+
288+
Ok(Some(state))
289+
}
290+
246291
/// Retrieve the signer set this stackerdb client is attached to
247292
pub fn get_signer_set(&self) -> u32 {
248293
u32::try_from(self.reward_cycle % 2).expect("FATAL: reward cycle % 2 exceeds u32::MAX")

stacks-signer/src/client/stacks_client.rs

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -696,15 +696,21 @@ impl StacksClient {
696696

697697
#[cfg(test)]
698698
mod tests {
699+
use std::collections::HashMap;
699700
use std::io::{BufWriter, Write};
700701
use std::thread::spawn;
701702

703+
use blockstack_lib::burnchains::Address;
702704
use blockstack_lib::chainstate::nakamoto::NakamotoBlockHeader;
703705
use blockstack_lib::chainstate::stacks::address::PoxAddress;
704706
use blockstack_lib::chainstate::stacks::boot::{
705707
NakamotoSignerEntry, PoxStartCycleInfo, RewardSet,
706708
};
707709
use blockstack_lib::chainstate::stacks::ThresholdSignature;
710+
use clarity::vm::types::{
711+
ListData, ListTypeData, ResponseData, SequenceData, TupleData, TupleTypeSignature,
712+
TypeSignature,
713+
};
708714
use rand::thread_rng;
709715
use rand_core::RngCore;
710716
use stacks_common::bitvec::BitVec;
@@ -1039,9 +1045,59 @@ mod tests {
10391045
#[test]
10401046
fn parse_valid_signer_slots_should_succeed() {
10411047
let mock = MockServerClient::new();
1042-
let clarity_value_hex =
1043-
"0x070b000000050c00000002096e756d2d736c6f7473010000000000000000000000000000000d067369676e6572051a8195196a9a7cf9c37cb13e1ed69a7bc047a84e050c00000002096e756d2d736c6f7473010000000000000000000000000000000d067369676e6572051a6505471146dcf722f0580911183f28bef30a8a890c00000002096e756d2d736c6f7473010000000000000000000000000000000d067369676e6572051a1d7f8e3936e5da5f32982cc47f31d7df9fb1b38a0c00000002096e756d2d736c6f7473010000000000000000000000000000000d067369676e6572051a126d1a814313c952e34c7840acec9211e1727fb80c00000002096e756d2d736c6f7473010000000000000000000000000000000d067369676e6572051a7374ea6bb39f2e8d3d334d62b9f302a977de339a";
1044-
let value = ClarityValue::try_deserialize_hex_untyped(clarity_value_hex).unwrap();
1048+
1049+
let signers = [
1050+
"ST20SA6BAK9YFKGVWP4Z1XNMTFF04FA2E0M8YRNNQ",
1051+
"ST1JGAHRH8VEFE8QGB04H261Z52ZF62MAH40CD6ZN",
1052+
"STEQZ3HS6VJXMQSJK0PC8ZSHTZFSZCDKHA7R60XT",
1053+
"ST96T6M18C9WJMQ39HW41B7CJ88Y2WKZQ1CK330M",
1054+
"ST1SQ9TKBPEFJX39X6D6P5EFK0AMQFQHKK9R0MJFC",
1055+
];
1056+
1057+
let tuple_type_signature: TupleTypeSignature = [
1058+
(ClarityName::from("num_slots"), TypeSignature::UIntType),
1059+
(ClarityName::from("signer"), TypeSignature::PrincipalType),
1060+
]
1061+
.into_iter()
1062+
.collect::<HashMap<_, _>>()
1063+
.try_into()
1064+
.unwrap();
1065+
1066+
let list_data: Vec<_> = signers
1067+
.into_iter()
1068+
.map(|signer| {
1069+
let principal_data = StacksAddress::from_string(signer).unwrap().into();
1070+
1071+
let data_map = [
1072+
("num-slots".into(), ClarityValue::UInt(14)),
1073+
(
1074+
"signer".into(),
1075+
ClarityValue::Principal(PrincipalData::Standard(principal_data)),
1076+
),
1077+
]
1078+
.into_iter()
1079+
.collect();
1080+
1081+
ClarityValue::Tuple(TupleData {
1082+
type_signature: tuple_type_signature.clone(),
1083+
data_map,
1084+
})
1085+
})
1086+
.collect();
1087+
1088+
let list_type_signature =
1089+
ListTypeData::new_list(TypeSignature::TupleType(tuple_type_signature), 5).unwrap();
1090+
1091+
let sequence = ClarityValue::Sequence(SequenceData::List(ListData {
1092+
data: list_data,
1093+
type_signature: list_type_signature,
1094+
}));
1095+
1096+
let value = ClarityValue::Response(ResponseData {
1097+
committed: true,
1098+
data: Box::new(sequence),
1099+
});
1100+
10451101
let signer_slots = mock.client.parse_signer_slots(value).unwrap();
10461102
assert_eq!(signer_slots.len(), 5);
10471103
signer_slots

0 commit comments

Comments
 (0)