From ea0766efe331ecc9f257cb1e0cccce5feb2aa00c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Blankfors?= Date: Fri, 15 Mar 2024 14:13:28 +0100 Subject: [PATCH] feat: Integration test with signer reboot --- Cargo.lock | 115 +------------------ Cargo.toml | 3 +- stacks-signer/src/config.rs | 29 +++-- stacks-signer/src/main.rs | 1 + testnet/stacks-node/src/tests/signer.rs | 142 ++++++++++++++++++++++-- 5 files changed, 151 insertions(+), 139 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70b20ecf4b..483244d1b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -498,28 +498,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "bindgen" -version = "0.64.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 1.0.109", - "which", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -632,15 +610,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" version = "0.1.10" @@ -692,17 +661,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clang-sys" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "2.34.0" @@ -1499,12 +1457,6 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "gloo-timers" version = "0.2.6" @@ -1925,12 +1877,6 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.153" @@ -1957,16 +1903,6 @@ dependencies = [ "rle-decode-fast", ] -[[package]] -name = "libloading" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" -dependencies = [ - "cfg-if 1.0.0", - "windows-sys 0.48.0", -] - [[package]] name = "libredox" version = "0.0.1" @@ -2089,12 +2025,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.7.2" @@ -2194,16 +2124,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2287,7 +2207,6 @@ version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a64d160b891178fb9d43d1a58ddcafb6502daeb54d810e5e92a7c3c9bfacc07" dependencies = [ - "bindgen", "bitvec", "bs58 0.4.0", "cc", @@ -2336,12 +2255,6 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "percent-encoding" version = "2.3.1" @@ -2977,12 +2890,6 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hex" version = "2.1.0" @@ -3344,12 +3251,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "signature" version = "2.2.0" @@ -4470,18 +4371,6 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.31", -] - [[package]] name = "winapi" version = "0.2.8" @@ -4706,8 +4595,8 @@ dependencies = [ [[package]] name = "wsts" -version = "8.1.0" -source = "git+https://github.com/stacks-network/wsts.git?branch=feat/public-sign-ids#99c1ed3d528d98585ba4b50084e8a6c37f8f5793" +version = "9.0.0" +source = "git+https://github.com/stacks-network/wsts.git?rev=af2b907#af2b907e00fdd840d1a9abeb84bd0ddb4fc2a528" dependencies = [ "aes-gcm 0.10.3", "bs58 0.5.0", diff --git a/Cargo.toml b/Cargo.toml index dc344554e1..13e16cb0c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,7 @@ rand_core = "0.6" rand = "0.8" rand_chacha = "0.3.1" tikv-jemallocator = "0.5.4" -# wsts = { version = "8.1", default-features = false } -wsts = { git = "https://github.com/stacks-network/wsts.git", branch = "feat/public-sign-ids" } +wsts = { git = "https://github.com/stacks-network/wsts.git", default-features = false, rev = "af2b907" } # Use a bit more than default optimization for # dev builds to speed up test execution diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index 3706d1ddce..483940d9af 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -338,19 +338,23 @@ pub fn build_signer_config_tomls( timeout: Option, network: &Network, password: &str, + mut port_start: usize, ) -> Vec { let mut signer_config_tomls = vec![]; - let mut port = 30000; - let run_stamp = rand::random::(); - let db_dir = format!( - "/tmp/stacks-node-tests/integrations-signers/{:#X}", - run_stamp, - ); - fs::create_dir_all(&db_dir).unwrap(); - for (ix, stacks_private_key) in stacks_private_keys.iter().enumerate() { - let endpoint = format!("localhost:{}", port); - port += 1; + for stacks_private_key in stacks_private_keys { + let endpoint = format!("localhost:{}", port_start); + port_start += 1; + + let stacks_public_key = StacksPublicKey::from_private(stacks_private_key).to_hex(); + let db_dir = std::env::temp_dir().join(format!( + "stacks-node-tests/integrations-signers/signer_{stacks_public_key}" + )); + let db_path = db_dir.join("signerdb.sqlite"); + let db_path = db_path.display(); + + fs::create_dir_all(&db_dir).unwrap(); + let stacks_private_key = stacks_private_key.to_hex(); let mut signer_config_toml = format!( r#" @@ -359,7 +363,7 @@ node_host = "{node_host}" endpoint = "{endpoint}" network = "{network}" auth_password = "{password}" -db_path = "{db_dir}/{ix}.sqlite" +db_path = "{db_path}" "# ); @@ -394,7 +398,8 @@ mod tests { let network = Network::Testnet; let password = "melon"; - let config_tomls = build_signer_config_tomls(&[pk], node_host, None, &network, password); + let config_tomls = + build_signer_config_tomls(&[pk], node_host, None, &network, password, 3000); let config = RawConfigFile::load_from_str(&config_tomls[0]).expect("Failed to parse config file"); diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs index b1d154a9fa..001c0e7b4f 100644 --- a/stacks-signer/src/main.rs +++ b/stacks-signer/src/main.rs @@ -292,6 +292,7 @@ fn handle_generate_files(args: GenerateFilesArgs) { args.timeout.map(Duration::from_millis), &args.network, &args.password, + 3000, ); debug!("Built {:?} signer config tomls.", signer_config_tomls.len()); for (i, file_contents) in signer_config_tomls.iter().enumerate() { diff --git a/testnet/stacks-node/src/tests/signer.rs b/testnet/stacks-node/src/tests/signer.rs index 0d8b69e84c..ed2787fa14 100644 --- a/testnet/stacks-node/src/tests/signer.rs +++ b/testnet/stacks-node/src/tests/signer.rs @@ -114,6 +114,7 @@ impl SignerTest { Some(Duration::from_millis(128)), // Timeout defaults to 5 seconds. Let's override it to 128 milliseconds. &Network::Testnet, password, + 3000, ); let mut running_signers = Vec::new(); @@ -426,22 +427,11 @@ impl SignerTest { .expect("failed to recv dkg results"); for result in results { match result { - OperationResult::Sign(sig) => { - panic!("Received Signature ({},{})", &sig.R, &sig.z); - } - OperationResult::SignTaproot(proof) => { - panic!("Received SchnorrProof ({},{})", &proof.r, &proof.s); - } - OperationResult::DkgError(dkg_error) => { - panic!("Received DkgError {:?}", dkg_error); - } - OperationResult::SignError(sign_error) => { - panic!("Received SignError {}", sign_error); - } OperationResult::Dkg(point) => { info!("Received aggregate_group_key {point}"); aggregate_public_key = Some(point); } + other => panic!("{}", operation_panic_message(&other)), } } if aggregate_public_key.is_some() || dkg_now.elapsed() > timeout { @@ -703,6 +693,45 @@ impl SignerTest { ] } + /// Kills the signer runloop at index `signer_idx` + /// and returns the private key of the killed signer. + /// + /// # Panics + /// Panics if `signer_idx` is out of bounds + fn stop_signer(&mut self, signer_idx: usize) -> StacksPrivateKey { + let running_signer = self.running_signers.remove(signer_idx); + self.signer_cmd_senders.remove(signer_idx); + self.result_receivers.remove(signer_idx); + let signer_key = self.signer_stacks_private_keys.remove(signer_idx); + + running_signer.stop(); + signer_key + } + + /// (Re)starts a new signer runloop with the given private key + fn restart_signer(&mut self, signer_idx: usize, signer_private_key: StacksPrivateKey) { + let signer_config = build_signer_config_tomls( + &[signer_private_key], + &self.running_nodes.conf.node.rpc_bind, + Some(Duration::from_millis(128)), // Timeout defaults to 5 seconds. Let's override it to 128 milliseconds. + &Network::Testnet, + "12345", // It worked sir, we have the combination! -Great, what's the combination? + 3000 + signer_idx, + ) + .pop() + .unwrap(); + + let (cmd_send, cmd_recv) = channel(); + let (res_send, res_recv) = channel(); + + info!("Restarting signer"); + let signer = spawn_signer(&signer_config, cmd_recv, res_send); + + self.result_receivers.insert(signer_idx, res_recv); + self.signer_cmd_senders.insert(signer_idx, cmd_send); + self.running_signers.insert(signer_idx, signer); + } + fn shutdown(self) { self.running_nodes .coord_channel @@ -851,6 +880,26 @@ fn setup_stx_btc_node( } } +fn operation_panic_message(result: &OperationResult) -> String { + match result { + OperationResult::Sign(sig) => { + format!("Received Signature ({},{})", sig.R, sig.z) + } + OperationResult::SignTaproot(proof) => { + format!("Received SchnorrProof ({},{})", proof.r, proof.s) + } + OperationResult::DkgError(dkg_error) => { + format!("Received DkgError {:?}", dkg_error) + } + OperationResult::SignError(sign_error) => { + format!("Received SignError {}", sign_error) + } + OperationResult::Dkg(point) => { + format!("Received aggregate_group_key {point}") + } + } +} + #[test] #[ignore] /// Test the signer can respond to external commands to perform DKG @@ -1266,3 +1315,72 @@ fn stackerdb_filter_bad_transactions() { } signer_test.shutdown(); } + +#[test] +#[ignore] +/// Test that signers will be able to continue their operations even if one signer is restarted. +/// +/// Test Setup: +/// The test spins up three stacks signers, one miner Nakamoto node, and a corresponding bitcoind. +/// The stacks node is advanced to epoch 2.5, triggering a DKG round. The stacks node is then advanced +/// to Epoch 3.0 boundary to allow block signing. +/// +/// Test Execution: +/// The signers sign one block as usual. +/// Then, one of the signers is restarted. +/// Finally, the signers sign another block with the restarted signer. +/// +/// Test Assertion: +/// The signers are able to produce a valid signature after one of them is restarted. +fn stackerdb_sign_after_signer_reboot() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + info!("------------------------- Test Setup -------------------------"); + let mut signer_test = SignerTest::new(3); + let timeout = Duration::from_secs(200); + let short_timeout = Duration::from_secs(30); + + let key = signer_test.boot_to_epoch_3(timeout); + + info!("------------------------- Test Mine Block -------------------------"); + + signer_test.mine_nakamoto_block(timeout); + let proposed_signer_signature_hash = signer_test.wait_for_validate_ok_response(short_timeout); + let signature = + signer_test.wait_for_confirmed_block(&proposed_signer_signature_hash, short_timeout); + + assert!( + signature.verify(&key, proposed_signer_signature_hash.0.as_slice()), + "Signature verification failed" + ); + + info!("------------------------- Restart one Signer -------------------------"); + let signer_key = signer_test.stop_signer(2); + debug!( + "Removed signer 2 with key: {:?}, {}", + signer_key, + signer_key.to_hex() + ); + signer_test.restart_signer(2, signer_key); + + info!("------------------------- Test Mine Block after restart -------------------------"); + + signer_test.mine_nakamoto_block(timeout); + let proposed_signer_signature_hash = signer_test.wait_for_validate_ok_response(short_timeout); + let frost_signature = + signer_test.wait_for_confirmed_block(&proposed_signer_signature_hash, short_timeout); + + assert!( + frost_signature.verify(&key, proposed_signer_signature_hash.0.as_slice()), + "Signature verification failed" + ); + + signer_test.shutdown(); +}