Skip to content

Commit 132c942

Browse files
authored
Merge pull request #4570 from stacks-network/4388-nakamoto-stacks-signer-should-store-its-party-shares-on-the-side-to-enable-restart
feat: Nakamoto signer persisting its party shares to enable restart
2 parents 93ff0e8 + 93b9ee1 commit 132c942

File tree

11 files changed

+357
-83
lines changed

11 files changed

+357
-83
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ jobs:
8686
- tests::signer::stackerdb_block_proposal
8787
- tests::signer::stackerdb_filter_bad_transactions
8888
- tests::signer::stackerdb_mine_2_nakamoto_reward_cycles
89+
- tests::signer::stackerdb_sign_after_signer_reboot
8990
- tests::nakamoto_integrations::stack_stx_burn_op_integration_test
9091
# Do not run this one until we figure out why it fails in CI
9192
# - tests::neon_integrations::bitcoin_reorg_flap

Cargo.lock

Lines changed: 7 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ rand_core = "0.6"
2020
rand = "0.8"
2121
rand_chacha = "0.3.1"
2222
tikv-jemallocator = "0.5.4"
23-
# wsts = { version = "8.1", default-features = false }
24-
wsts = { git = "https://github.com/stacks-network/wsts.git", branch = "feat/public-sign-ids", default-features = false }
23+
wsts = { version = "9.0.0", default-features = false }
2524

2625
# Use a bit more than default optimization for
2726
# dev builds to speed up test execution

stacks-signer/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ url = "2.1.0"
4646

4747
[dev-dependencies]
4848
clarity = { path = "../clarity", features = ["testing"] }
49+
polynomial = "0.2.6"
50+
num-traits = "0.2.18"
4951

5052
[dependencies.rusqlite]
5153
version = "=0.24.2"

stacks-signer/src/config.rs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -338,19 +338,22 @@ pub fn build_signer_config_tomls(
338338
timeout: Option<Duration>,
339339
network: &Network,
340340
password: &str,
341+
run_stamp: u16,
342+
mut port_start: usize,
341343
) -> Vec<String> {
342344
let mut signer_config_tomls = vec![];
343345

344-
let mut port = 30000;
345-
let run_stamp = rand::random::<u16>();
346-
let db_dir = format!(
347-
"/tmp/stacks-node-tests/integrations-signers/{:#X}",
348-
run_stamp,
349-
);
350-
fs::create_dir_all(&db_dir).unwrap();
351-
for (ix, stacks_private_key) in stacks_private_keys.iter().enumerate() {
352-
let endpoint = format!("localhost:{}", port);
353-
port += 1;
346+
for stacks_private_key in stacks_private_keys {
347+
let endpoint = format!("localhost:{}", port_start);
348+
port_start += 1;
349+
350+
let stacks_public_key = StacksPublicKey::from_private(stacks_private_key).to_hex();
351+
let db_dir = format!(
352+
"/tmp/stacks-node-tests/integrations-signers/{run_stamp}/signer_{stacks_public_key}"
353+
);
354+
let db_path = format!("{db_dir}/signerdb.sqlite");
355+
fs::create_dir_all(&db_dir).unwrap();
356+
354357
let stacks_private_key = stacks_private_key.to_hex();
355358
let mut signer_config_toml = format!(
356359
r#"
@@ -359,7 +362,7 @@ node_host = "{node_host}"
359362
endpoint = "{endpoint}"
360363
network = "{network}"
361364
auth_password = "{password}"
362-
db_path = "{db_dir}/{ix}.sqlite"
365+
db_path = "{db_path}"
363366
"#
364367
);
365368

@@ -394,7 +397,15 @@ mod tests {
394397
let network = Network::Testnet;
395398
let password = "melon";
396399

397-
let config_tomls = build_signer_config_tomls(&[pk], node_host, None, &network, password);
400+
let config_tomls = build_signer_config_tomls(
401+
&[pk],
402+
node_host,
403+
None,
404+
&network,
405+
password,
406+
rand::random(),
407+
3000,
408+
);
398409

399410
let config =
400411
RawConfigFile::load_from_str(&config_tomls[0]).expect("Failed to parse config file");

stacks-signer/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,8 @@ fn handle_generate_files(args: GenerateFilesArgs) {
292292
args.timeout.map(Duration::from_millis),
293293
&args.network,
294294
&args.password,
295+
rand::random(),
296+
3000,
295297
);
296298
debug!("Built {:?} signer config tomls.", signer_config_tomls.len());
297299
for (i, file_contents) in signer_config_tomls.iter().enumerate() {

stacks-signer/src/runloop.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,7 @@ impl SignerRunLoop<Vec<OperationResult>, RunLoopCommand> for RunLoop {
417417
info!(
418418
"{signer}: Queuing an external runloop command ({:?}): {command:?}",
419419
signer
420-
.signing_round
420+
.state_machine
421421
.public_keys
422422
.signers
423423
.get(&signer.signer_id)

stacks-signer/src/signer.rs

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ use wsts::state_machine::coordinator::fire::Coordinator as FireCoordinator;
4444
use wsts::state_machine::coordinator::{
4545
Config as CoordinatorConfig, Coordinator, State as CoordinatorState,
4646
};
47-
use wsts::state_machine::signer::Signer as WSTSSigner;
47+
use wsts::state_machine::signer::Signer as SignerStateMachine;
4848
use wsts::state_machine::{OperationResult, SignError};
49+
use wsts::traits::Signer as _;
4950
use wsts::v2;
5051

5152
use crate::client::{retry_with_exponential_backoff, ClientError, StackerDB, StacksClient};
@@ -137,7 +138,7 @@ pub struct Signer {
137138
/// The coordinator for inbound messages for a specific reward cycle
138139
pub coordinator: FireCoordinator<v2::Aggregator>,
139140
/// The signing round used to sign messages for a specific reward cycle
140-
pub signing_round: WSTSSigner<v2::Signer>,
141+
pub state_machine: SignerStateMachine<v2::Signer>,
141142
/// the state of the signer
142143
pub state: State,
143144
/// Received Commands that need to be processed
@@ -243,17 +244,8 @@ impl From<SignerConfig> for Signer {
243244
};
244245

245246
let coordinator = FireCoordinator::new(coordinator_config);
246-
let signing_round = WSTSSigner::new(
247-
threshold,
248-
num_signers,
249-
num_keys,
250-
signer_config.signer_id,
251-
signer_config.key_ids,
252-
signer_config.ecdsa_private_key,
253-
signer_config.signer_entries.public_keys.clone(),
254-
);
255247
let coordinator_selector =
256-
CoordinatorSelector::from(signer_config.signer_entries.public_keys);
248+
CoordinatorSelector::from(signer_config.signer_entries.public_keys.clone());
257249

258250
debug!(
259251
"Reward cycle #{} Signer #{}: initial coordinator is signer {}",
@@ -263,9 +255,31 @@ impl From<SignerConfig> for Signer {
263255
);
264256
let signer_db =
265257
SignerDb::new(&signer_config.db_path).expect("Failed to connect to signer Db");
258+
259+
let mut state_machine = SignerStateMachine::new(
260+
threshold,
261+
num_signers,
262+
num_keys,
263+
signer_config.signer_id,
264+
signer_config.key_ids,
265+
signer_config.ecdsa_private_key,
266+
signer_config.signer_entries.public_keys,
267+
);
268+
269+
if let Some(state) = signer_db
270+
.get_signer_state(signer_config.reward_cycle)
271+
.expect("Failed to load signer state")
272+
{
273+
debug!(
274+
"Reward cycle #{} Signer #{}: Loading signer",
275+
signer_config.reward_cycle, signer_config.signer_id
276+
);
277+
state_machine.signer = v2::Signer::load(&state);
278+
}
279+
266280
Self {
267281
coordinator,
268-
signing_round,
282+
state_machine,
269283
state: State::Idle,
270284
commands: VecDeque::new(),
271285
stackerdb,
@@ -639,7 +653,7 @@ impl Signer {
639653
current_reward_cycle: u64,
640654
) {
641655
let signer_outbound_messages = self
642-
.signing_round
656+
.state_machine
643657
.process_inbound_messages(packets)
644658
.unwrap_or_else(|e| {
645659
error!("{self}: Failed to process inbound messages as a signer: {e:?}",);
@@ -670,6 +684,9 @@ impl Signer {
670684
// We have received a message and are in the middle of an operation. Update our state accordingly
671685
self.update_operation();
672686
}
687+
688+
debug!("{self}: Saving signer state");
689+
self.save_signer_state();
673690
self.send_outbound_messages(signer_outbound_messages);
674691
self.send_outbound_messages(coordinator_outbound_messages);
675692
}
@@ -907,7 +924,7 @@ impl Signer {
907924
coordinator_public_key: &PublicKey,
908925
) -> Option<Packet> {
909926
// We only care about verified wsts packets. Ignore anything else.
910-
if packet.verify(&self.signing_round.public_keys, coordinator_public_key) {
927+
if packet.verify(&self.state_machine.public_keys, coordinator_public_key) {
911928
match &mut packet.msg {
912929
Message::SignatureShareRequest(request) => {
913930
if !self.validate_signature_share_request(request) {
@@ -1173,6 +1190,18 @@ impl Signer {
11731190
}
11741191
}
11751192

1193+
/// Persist state needed to ensure the signer can continue to perform
1194+
/// DKG and participate in signing rounds accross crashes
1195+
///
1196+
/// # Panics
1197+
/// Panics if the insertion fails
1198+
fn save_signer_state(&self) {
1199+
let state = self.state_machine.signer.save();
1200+
self.signer_db
1201+
.insert_signer_state(self.reward_cycle, &state)
1202+
.expect("Failed to persist signer state");
1203+
}
1204+
11761205
/// Send any operation results across the provided channel
11771206
fn send_operation_results(
11781207
&mut self,

0 commit comments

Comments
 (0)