diff --git a/CHANGELOG.md b/CHANGELOG.md index 226f7b5159..043e70fb81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE ## [Unreleased] +### Added +- Add `dry_run` configuration option to `stacks-signer` config toml. Dry run mode will + run the signer binary as if it were a registered signer. Instead of broadcasting + `StackerDB` messages, it logs `INFO` messages. Other interactions with the `stacks-node` + behave normally (e.g., submitting validation requests, submitting finished blocks). A + dry run signer will error out if the supplied key is actually a registered signer. + ### Changed - Miner will include other transactions in blocks with tenure extend transactions (#5760) diff --git a/Cargo.lock b/Cargo.lock index 5569bf1f88..9a39c4c10b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -606,7 +606,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.58", ] [[package]] @@ -620,7 +620,7 @@ name = "clarity" version = "0.0.1" dependencies = [ "assert-json-diff 1.1.0", - "hashbrown 0.14.3", + "hashbrown 0.15.2", "integer-sqrt", "lazy_static", "mutants", @@ -784,16 +784,15 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.2" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if 1.0.0", "cpufeatures", "curve25519-dalek-derive", "digest 0.10.7", "fiat-crypto", - "platforms", "rustc_version 0.4.0", "subtle", "zeroize", @@ -807,7 +806,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.58", ] [[package]] @@ -907,7 +906,7 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ - "curve25519-dalek 4.1.2", + "curve25519-dalek 4.1.3", "ed25519", "rand_core 0.6.4", "serde", @@ -1040,6 +1039,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1082,9 +1087,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1092,9 +1097,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -1109,9 +1114,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -1143,26 +1148,26 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.58", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -1172,9 +1177,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1259,9 +1264,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.24" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -1302,8 +1307,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ "ahash", - "allocator-api2", - "serde", ] [[package]] @@ -1311,6 +1314,12 @@ name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", + "serde", +] [[package]] name = "hashlink" @@ -1486,14 +1495,14 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.3.24", + "h2 0.3.26", "http 0.2.11", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.5", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -1733,7 +1742,7 @@ name = "libsigner" version = "0.0.1" dependencies = [ "clarity", - "hashbrown 0.14.3", + "hashbrown 0.15.2", "lazy_static", "libc", "libstackerdb", @@ -1881,9 +1890,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -2103,7 +2112,7 @@ checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.58", ] [[package]] @@ -2145,12 +2154,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[package]] -name = "platforms" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" - [[package]] name = "polling" version = "2.8.0" @@ -2439,7 +2442,7 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" name = "relay-server" version = "0.0.1" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.15.2", ] [[package]] @@ -2453,7 +2456,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.3.24", + "h2 0.3.26", "http 0.2.11", "http-body 0.4.6", "hyper 0.14.28", @@ -2627,9 +2630,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring", @@ -2762,7 +2765,7 @@ checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.58", ] [[package]] @@ -2831,7 +2834,7 @@ checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.58", ] [[package]] @@ -3042,7 +3045,7 @@ dependencies = [ "chrono", "curve25519-dalek 2.0.0", "ed25519-dalek", - "hashbrown 0.14.3", + "hashbrown 0.15.2", "lazy_static", "libc", "nix", @@ -3073,7 +3076,7 @@ dependencies = [ "base64 0.12.3", "chrono", "clarity", - "hashbrown 0.14.3", + "hashbrown 0.15.2", "http-types", "lazy_static", "libc", @@ -3113,7 +3116,7 @@ dependencies = [ "backoff", "clap", "clarity", - "hashbrown 0.14.3", + "hashbrown 0.15.2", "lazy_static", "libsigner", "libstackerdb", @@ -3150,7 +3153,7 @@ dependencies = [ "clarity", "curve25519-dalek 2.0.0", "ed25519-dalek", - "hashbrown 0.14.3", + "hashbrown 0.15.2", "integer-sqrt", "lazy_static", "libc", @@ -3288,9 +3291,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -3365,7 +3368,7 @@ checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.58", ] [[package]] @@ -3505,7 +3508,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio 0.8.10", + "mio 0.8.11", "num_cpus", "parking_lot", "pin-project-lite", @@ -3595,7 +3598,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.58", ] [[package]] @@ -3841,7 +3844,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.58", "wasm-bindgen-shared", ] @@ -3875,7 +3878,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4114,7 +4117,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.58", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 194e946ef4..3b9486b61d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,8 @@ members = [ # Dependencies we want to keep the same between workspace members [workspace.dependencies] ed25519-dalek = { version = "2.1.1", features = ["serde", "rand_core"] } -hashbrown = { version = "0.14.3", features = ["serde"] } -rand_core = "0.6" +hashbrown = { version = "0.15.2", features = ["serde"] } +rand_core = "0.6.4" rand = "0.8" rand_chacha = "0.3.1" tikv-jemallocator = "0.5.4" diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index a3d9bed159..8e163ac319 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -144,7 +144,7 @@ pub(crate) mod tests { use stacks_common::util::hash::{Hash160, Sha256Sum}; use super::*; - use crate::config::{GlobalConfig, SignerConfig}; + use crate::config::{GlobalConfig, SignerConfig, SignerConfigMode}; pub struct MockServerClient { pub server: TcpListener, @@ -393,8 +393,10 @@ pub(crate) mod tests { } SignerConfig { reward_cycle, - signer_id: 0, - signer_slot_id: SignerSlotID(rand::thread_rng().gen_range(0..num_signers)), // Give a random signer slot id between 0 and num_signers + signer_mode: SignerConfigMode::Normal { + signer_id: 0, + signer_slot_id: SignerSlotID(rand::thread_rng().gen_range(0..num_signers)), // Give a random signer slot id between 0 and num_signers + }, signer_entries: SignerEntries { signer_addr_to_id, signer_id_to_pk, diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index dc6525b144..81799dcc88 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -19,12 +19,13 @@ use clarity::codec::read_next; use hashbrown::HashMap; use libsigner::{MessageSlotID, SignerMessage, SignerSession, StackerDBSession}; use libstackerdb::{StackerDBChunkAckData, StackerDBChunkData}; -use slog::{slog_debug, slog_warn}; +use slog::{slog_debug, slog_info, slog_warn}; use stacks_common::types::chainstate::StacksPrivateKey; -use stacks_common::{debug, warn}; +use stacks_common::util::hash::to_hex; +use stacks_common::{debug, info, warn}; use crate::client::{retry_with_exponential_backoff, ClientError}; -use crate::config::SignerConfig; +use crate::config::{SignerConfig, SignerConfigMode}; /// The signer StackerDB slot ID, purposefully wrapped to prevent conflation with SignerID #[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, PartialOrd, Ord)] @@ -36,6 +37,12 @@ impl std::fmt::Display for SignerSlotID { } } +#[derive(Debug)] +enum StackerDBMode { + DryRun, + Normal { signer_slot_id: SignerSlotID }, +} + /// The StackerDB client for communicating with the .signers contract #[derive(Debug)] pub struct StackerDB { @@ -46,32 +53,60 @@ pub struct StackerDB { stacks_private_key: StacksPrivateKey, /// A map of a message ID to last chunk version for each session slot_versions: HashMap>, - /// The signer slot ID -- the index into the signer list for this signer daemon's signing key. - signer_slot_id: SignerSlotID, + /// The running mode of the stackerdb (whether the signer is running in dry-run or + /// normal operation) + mode: StackerDBMode, /// The reward cycle of the connecting signer reward_cycle: u64, } impl From<&SignerConfig> for StackerDB { fn from(config: &SignerConfig) -> Self { + let mode = match config.signer_mode { + SignerConfigMode::DryRun => StackerDBMode::DryRun, + SignerConfigMode::Normal { + ref signer_slot_id, .. + } => StackerDBMode::Normal { + signer_slot_id: *signer_slot_id, + }, + }; + Self::new( &config.node_host, config.stacks_private_key, config.mainnet, config.reward_cycle, - config.signer_slot_id, + mode, ) } } impl StackerDB { - /// Create a new StackerDB client - pub fn new( + #[cfg(any(test, feature = "testing"))] + /// Create a StackerDB client in normal operation (i.e., not a dry-run signer) + pub fn new_normal( host: &str, stacks_private_key: StacksPrivateKey, is_mainnet: bool, reward_cycle: u64, signer_slot_id: SignerSlotID, + ) -> Self { + Self::new( + host, + stacks_private_key, + is_mainnet, + reward_cycle, + StackerDBMode::Normal { signer_slot_id }, + ) + } + + /// Create a new StackerDB client + fn new( + host: &str, + stacks_private_key: StacksPrivateKey, + is_mainnet: bool, + reward_cycle: u64, + signer_mode: StackerDBMode, ) -> Self { let mut signers_message_stackerdb_sessions = HashMap::new(); for msg_id in M::all() { @@ -84,7 +119,7 @@ impl StackerDB { signers_message_stackerdb_sessions, stacks_private_key, slot_versions: HashMap::new(), - signer_slot_id, + mode: signer_mode, reward_cycle, } } @@ -110,18 +145,33 @@ impl StackerDB { msg_id: &M, message_bytes: Vec, ) -> Result { - let slot_id = self.signer_slot_id; + let StackerDBMode::Normal { + signer_slot_id: slot_id, + } = &self.mode + else { + info!( + "Dry-run signer would have sent a stackerdb message"; + "message_id" => ?msg_id, + "message_bytes" => to_hex(&message_bytes) + ); + return Ok(StackerDBChunkAckData { + accepted: true, + reason: None, + metadata: None, + code: None, + }); + }; loop { let mut slot_version = if let Some(versions) = self.slot_versions.get_mut(msg_id) { - if let Some(version) = versions.get(&slot_id) { + if let Some(version) = versions.get(slot_id) { *version } else { - versions.insert(slot_id, 0); + versions.insert(*slot_id, 0); 1 } } else { let mut versions = HashMap::new(); - versions.insert(slot_id, 0); + versions.insert(*slot_id, 0); self.slot_versions.insert(*msg_id, versions); 1 }; @@ -143,7 +193,7 @@ impl StackerDB { if let Some(versions) = self.slot_versions.get_mut(msg_id) { // NOTE: per the above, this is always executed - versions.insert(slot_id, slot_version.saturating_add(1)); + versions.insert(*slot_id, slot_version.saturating_add(1)); } else { return Err(ClientError::NotConnected); } @@ -165,7 +215,7 @@ impl StackerDB { } if let Some(versions) = self.slot_versions.get_mut(msg_id) { // NOTE: per the above, this is always executed - versions.insert(slot_id, slot_version.saturating_add(1)); + versions.insert(*slot_id, slot_version.saturating_add(1)); } else { return Err(ClientError::NotConnected); } @@ -216,11 +266,6 @@ impl StackerDB { u32::try_from(self.reward_cycle % 2).expect("FATAL: reward cycle % 2 exceeds u32::MAX") } - /// Retrieve the signer slot ID - pub fn get_signer_slot_id(&self) -> SignerSlotID { - self.signer_slot_id - } - /// Get the session corresponding to the given message ID if it exists pub fn get_session_mut(&mut self, msg_id: &M) -> Option<&mut StackerDBSession> { self.signers_message_stackerdb_sessions.get_mut(msg_id) diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index a50ca7ecf8..29ee35c961 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -39,6 +39,7 @@ const BLOCK_PROPOSAL_TIMEOUT_MS: u64 = 600_000; const BLOCK_PROPOSAL_VALIDATION_TIMEOUT_MS: u64 = 120_000; const DEFAULT_FIRST_PROPOSAL_BURN_BLOCK_TIMING_SECS: u64 = 60; const DEFAULT_TENURE_LAST_BLOCK_PROPOSAL_TIMEOUT_SECS: u64 = 30; +const DEFAULT_DRY_RUN: bool = false; const TENURE_IDLE_TIMEOUT_SECS: u64 = 120; #[derive(thiserror::Error, Debug)] @@ -106,15 +107,36 @@ impl Network { } } +/// Signer config mode (whether dry-run or real) +#[derive(Debug, Clone)] +pub enum SignerConfigMode { + /// Dry run operation: signer is not actually registered, the signer + /// will not submit stackerdb messages, etc. + DryRun, + /// Normal signer operation: if registered, the signer will submit + /// stackerdb messages, etc. + Normal { + /// The signer ID assigned to this signer (may be different from signer_slot_id) + signer_id: u32, + /// The signer stackerdb slot id (may be different from signer_id) + signer_slot_id: SignerSlotID, + }, +} + +impl std::fmt::Display for SignerConfigMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SignerConfigMode::DryRun => write!(f, "Dry-Run signer"), + SignerConfigMode::Normal { signer_id, .. } => write!(f, "signer #{signer_id}"), + } + } +} + /// The Configuration info needed for an individual signer per reward cycle #[derive(Debug, Clone)] pub struct SignerConfig { /// The reward cycle of the configuration pub reward_cycle: u64, - /// The signer ID assigned to this signer (may be different from signer_slot_id) - pub signer_id: u32, - /// The signer stackerdb slot id (may be different from signer_id) - pub signer_slot_id: SignerSlotID, /// The registered signers for this reward cycle pub signer_entries: SignerEntries, /// The signer slot ids of all signers registered for this reward cycle @@ -141,6 +163,8 @@ pub struct SignerConfig { pub tenure_idle_timeout: Duration, /// The maximum age of a block proposal in seconds that will be processed by the signer pub block_proposal_max_age_secs: u64, + /// The running mode for the signer (dry-run or normal) + pub signer_mode: SignerConfigMode, } /// The parsed configuration for the signer @@ -181,6 +205,8 @@ pub struct GlobalConfig { pub tenure_idle_timeout: Duration, /// The maximum age of a block proposal that will be processed by the signer pub block_proposal_max_age_secs: u64, + /// Is this signer binary going to be running in dry-run mode? + pub dry_run: bool, } /// Internal struct for loading up the config file @@ -220,6 +246,8 @@ struct RawConfigFile { pub tenure_idle_timeout_secs: Option, /// The maximum age of a block proposal (in secs) that will be processed by the signer. pub block_proposal_max_age_secs: Option, + /// Is this signer binary going to be running in dry-run mode? + pub dry_run: Option, } impl RawConfigFile { @@ -321,6 +349,8 @@ impl TryFrom for GlobalConfig { .block_proposal_max_age_secs .unwrap_or(DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS); + let dry_run = raw_data.dry_run.unwrap_or(DEFAULT_DRY_RUN); + Ok(Self { node_host: raw_data.node_host, endpoint, @@ -338,6 +368,7 @@ impl TryFrom for GlobalConfig { block_proposal_validation_timeout, tenure_idle_timeout, block_proposal_max_age_secs, + dry_run, }) } } diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 84c1c592f5..96223b39a0 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -25,7 +25,7 @@ use stacks_common::{debug, error, info, warn}; use crate::chainstate::SortitionsView; use crate::client::{retry_with_exponential_backoff, ClientError, StacksClient}; -use crate::config::{GlobalConfig, SignerConfig}; +use crate::config::{GlobalConfig, SignerConfig, SignerConfigMode}; #[cfg(any(test, feature = "testing"))] use crate::v0::tests::TEST_SKIP_SIGNER_CLEANUP; use crate::Signer as SignerTrait; @@ -39,6 +39,9 @@ pub enum ConfigurationError { /// The stackerdb signer config is not yet updated #[error("The stackerdb config is not yet updated")] StackerDBNotUpdated, + /// The signer binary is configured as dry-run, but is also registered for this cycle + #[error("The signer binary is configured as dry-run, but is also registered for this cycle")] + DryRunStackerIsRegistered, } /// The internal signer state info @@ -258,27 +261,48 @@ impl, T: StacksMessageCodec + Clone + Send + Debug> RunLo warn!("Error while fetching stackerdb slots {reward_cycle}: {e:?}"); e })?; + + let dry_run = self.config.dry_run; let current_addr = self.stacks_client.get_signer_address(); - let Some(signer_slot_id) = signer_slot_ids.get(current_addr) else { - warn!( + let signer_config_mode = if !dry_run { + let Some(signer_slot_id) = signer_slot_ids.get(current_addr) else { + warn!( "Signer {current_addr} was not found in stacker db. Must not be registered for this reward cycle {reward_cycle}." ); - return Ok(None); - }; - let Some(signer_id) = signer_entries.signer_addr_to_id.get(current_addr) else { - warn!( - "Signer {current_addr} was found in stacker db but not the reward set for reward cycle {reward_cycle}." + return Ok(None); + }; + let Some(signer_id) = signer_entries.signer_addr_to_id.get(current_addr) else { + warn!( + "Signer {current_addr} was found in stacker db but not the reward set for reward cycle {reward_cycle}." + ); + return Ok(None); + }; + info!( + "Signer #{signer_id} ({current_addr}) is registered for reward cycle {reward_cycle}." ); - return Ok(None); + SignerConfigMode::Normal { + signer_slot_id: *signer_slot_id, + signer_id: *signer_id, + } + } else { + if signer_slot_ids.contains_key(current_addr) { + error!( + "Signer is configured for dry-run, but the signer address {current_addr} was found in stacker db." + ); + return Err(ConfigurationError::DryRunStackerIsRegistered); + }; + if signer_entries.signer_addr_to_id.contains_key(current_addr) { + warn!( + "Signer {current_addr} was found in stacker db but not the reward set for reward cycle {reward_cycle}." + ); + return Ok(None); + }; + SignerConfigMode::DryRun }; - info!( - "Signer #{signer_id} ({current_addr}) is registered for reward cycle {reward_cycle}." - ); Ok(Some(SignerConfig { reward_cycle, - signer_id: *signer_id, - signer_slot_id: *signer_slot_id, + signer_mode: signer_config_mode, signer_entries, signer_slot_ids: signer_slot_ids.into_values().collect(), first_proposal_burn_block_timing: self.config.first_proposal_burn_block_timing, @@ -299,9 +323,9 @@ impl, T: StacksMessageCodec + Clone + Send + Debug> RunLo let reward_index = reward_cycle % 2; let new_signer_config = match self.get_signer_config(reward_cycle) { Ok(Some(new_signer_config)) => { - let signer_id = new_signer_config.signer_id; + let signer_mode = new_signer_config.signer_mode.clone(); let new_signer = Signer::new(new_signer_config); - info!("{new_signer} Signer is registered for reward cycle {reward_cycle} as signer #{signer_id}. Initialized signer state."); + info!("{new_signer} Signer is registered for reward cycle {reward_cycle} as {signer_mode}. Initialized signer state."); ConfiguredSigner::RegisteredSigner(new_signer) } Ok(None) => { diff --git a/stacks-signer/src/v0/signer.rs b/stacks-signer/src/v0/signer.rs index 70253f8258..4cabbe7da1 100644 --- a/stacks-signer/src/v0/signer.rs +++ b/stacks-signer/src/v0/signer.rs @@ -39,11 +39,25 @@ use stacks_common::{debug, error, info, warn}; use crate::chainstate::{ProposalEvalConfig, SortitionsView}; use crate::client::{ClientError, SignerSlotID, StackerDB, StacksClient}; -use crate::config::SignerConfig; +use crate::config::{SignerConfig, SignerConfigMode}; use crate::runloop::SignerResult; use crate::signerdb::{BlockInfo, BlockState, SignerDb}; use crate::Signer as SignerTrait; +/// Signer running mode (whether dry-run or real) +#[derive(Debug)] +pub enum SignerMode { + /// Dry run operation: signer is not actually registered, the signer + /// will not submit stackerdb messages, etc. + DryRun, + /// Normal signer operation: if registered, the signer will submit + /// stackerdb messages, etc. + Normal { + /// The signer ID assigned to this signer (may be different from signer_slot_id) + signer_id: u32, + }, +} + /// The stacks signer registered for the reward cycle #[derive(Debug)] pub struct Signer { @@ -57,8 +71,8 @@ pub struct Signer { pub stackerdb: StackerDB, /// Whether the signer is a mainnet signer or not pub mainnet: bool, - /// The signer id - pub signer_id: u32, + /// The running mode of the signer (whether dry-run or normal) + pub mode: SignerMode, /// The signer slot ids for the signers in the reward cycle pub signer_slot_ids: Vec, /// The addresses of other signers @@ -80,9 +94,18 @@ pub struct Signer { pub block_proposal_max_age_secs: u64, } +impl std::fmt::Display for SignerMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SignerMode::DryRun => write!(f, "Dry-Run signer"), + SignerMode::Normal { signer_id } => write!(f, "Signer #{signer_id}"), + } + } +} + impl std::fmt::Display for Signer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Cycle #{} Signer #{}", self.reward_cycle, self.signer_id,) + write!(f, "Cycle #{} {}", self.reward_cycle, self.mode) } } @@ -275,10 +298,13 @@ impl SignerTrait for Signer { impl From for Signer { fn from(signer_config: SignerConfig) -> Self { let stackerdb = StackerDB::from(&signer_config); - debug!( - "Reward cycle #{} Signer #{}", - signer_config.reward_cycle, signer_config.signer_id, - ); + let mode = match signer_config.signer_mode { + SignerConfigMode::DryRun => SignerMode::DryRun, + SignerConfigMode::Normal { signer_id, .. } => SignerMode::Normal { signer_id }, + }; + + debug!("Reward cycle #{} {mode}", signer_config.reward_cycle); + let signer_db = SignerDb::new(&signer_config.db_path).expect("Failed to connect to signer Db"); let proposal_config = ProposalEvalConfig::from(&signer_config); @@ -287,7 +313,7 @@ impl From for Signer { private_key: signer_config.stacks_private_key, stackerdb, mainnet: signer_config.mainnet, - signer_id: signer_config.signer_id, + mode, signer_addresses: signer_config.signer_entries.signer_addresses.clone(), signer_weights: signer_config.signer_entries.signer_addr_to_weight.clone(), signer_slot_ids: signer_config.signer_slot_ids.clone(), @@ -818,31 +844,32 @@ impl Signer { .remove_pending_block_validation(&signer_sig_hash) .unwrap_or_else(|e| warn!("{self}: Failed to remove pending block validation: {e:?}")); - let Some(response) = block_response else { - return; - }; - // Submit a proposal response to the .signers contract for miners - info!( - "{self}: Broadcasting a block response to stacks node: {response:?}"; - ); - let accepted = matches!(response, BlockResponse::Accepted(..)); - match self - .stackerdb - .send_message_with_retry::(response.into()) - { - Ok(_) => { - crate::monitoring::actions::increment_block_responses_sent(accepted); - if let Ok(Some(block_info)) = self - .signer_db - .block_lookup(&block_validate_response.signer_signature_hash()) - { - crate::monitoring::actions::record_block_response_latency(&block_info.block); + if let Some(response) = block_response { + // Submit a proposal response to the .signers contract for miners + info!( + "{self}: Broadcasting a block response to stacks node: {response:?}"; + ); + let accepted = matches!(response, BlockResponse::Accepted(..)); + match self + .stackerdb + .send_message_with_retry::(response.into()) + { + Ok(_) => { + crate::monitoring::actions::increment_block_responses_sent(accepted); + if let Ok(Some(block_info)) = self + .signer_db + .block_lookup(&block_validate_response.signer_signature_hash()) + { + crate::monitoring::actions::record_block_response_latency( + &block_info.block, + ); + } + } + Err(e) => { + warn!("{self}: Failed to send block rejection to stacker-db: {e:?}",); } } - Err(e) => { - warn!("{self}: Failed to send block rejection to stacker-db: {e:?}",); - } - } + }; // Check if there is a pending block validation that we need to submit to the node match self.signer_db.get_and_remove_pending_block_validation() { diff --git a/testnet/stacks-node/src/tests/signer/mod.rs b/testnet/stacks-node/src/tests/signer/mod.rs index fba2194edb..1bf57444ed 100644 --- a/testnet/stacks-node/src/tests/signer/mod.rs +++ b/testnet/stacks-node/src/tests/signer/mod.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2024 Stacks Open Internet Foundation +// Copyright (C) 2020-2025 Stacks Open Internet Foundation // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -15,20 +15,6 @@ mod v0; use std::collections::HashSet; -// Copyright (C) 2020-2024 Stacks Open Internet Foundation -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; @@ -732,7 +718,7 @@ impl + Send + 'static, T: SignerEventTrait + 'static> SignerTest BlockResponse { - let mut stackerdb = StackerDB::new( + let mut stackerdb = StackerDB::new_normal( &self.running_nodes.conf.node.rpc_bind, StacksPrivateKey::random(), // We are just reading so don't care what the key is false, @@ -819,7 +805,7 @@ impl + Send + 'static, T: SignerEventTrait + 'static> SignerTest