From d4ad769aae018b20ce07011b1880523a5f98ac8d Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 8 Jan 2025 13:20:57 -0600 Subject: [PATCH 1/2] feat: implement dry-run mode for signer --- stacks-signer/src/client/stackerdb.rs | 69 +++++++++++++++++++-------- stacks-signer/src/config.rs | 39 +++++++++++++-- stacks-signer/src/runloop.rs | 56 +++++++++++++++------- stacks-signer/src/v0/signer.rs | 44 +++++++++++++---- 4 files changed, 158 insertions(+), 50 deletions(-) diff --git a/stacks-signer/src/client/stackerdb.rs b/stacks-signer/src/client/stackerdb.rs index 0316976a4c..1da6618711 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,42 @@ 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( + /// Create a new StackerDB client running in normal operation + fn new( host: &str, stacks_private_key: StacksPrivateKey, is_mainnet: bool, reward_cycle: u64, - signer_slot_id: SignerSlotID, + signer_mode: StackerDBMode, ) -> Self { let mut signers_message_stackerdb_sessions = HashMap::new(); for msg_id in M::all() { @@ -84,7 +101,7 @@ impl StackerDB { signers_message_stackerdb_sessions, stacks_private_key, slot_versions: HashMap::new(), - signer_slot_id, + mode: signer_mode, reward_cycle, } } @@ -110,18 +127,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 +175,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 +197,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 +248,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 c100703fc9..a1ff94ef2f 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -40,6 +40,7 @@ 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 TENURE_IDLE_TIMEOUT_SECS: u64 = 300; +const DEFAULT_DRY_RUN: bool = false; #[derive(thiserror::Error, Debug)] /// An error occurred parsing the provided configuration @@ -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 69dc2dd843..f718373c47 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 fb52394771..f7b7db084c 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::{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(), From 57d796b4d1ae5f3652787fb992c5f453093e63d2 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 14 Jan 2025 13:01:53 -0600 Subject: [PATCH 2/2] fix test build --- stacks-signer/src/client/mod.rs | 8 +++++--- stacks-signer/src/client/stackerdb.rs | 20 +++++++++++++++++++- testnet/stacks-node/src/tests/signer/mod.rs | 20 +++----------------- testnet/stacks-node/src/tests/signer/v0.rs | 8 ++++---- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index bdaa368567..3c549f4706 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 1da6618711..bfd7697245 100644 --- a/stacks-signer/src/client/stackerdb.rs +++ b/stacks-signer/src/client/stackerdb.rs @@ -82,7 +82,25 @@ impl From<&SignerConfig> for StackerDB { } impl StackerDB { - /// Create a new StackerDB client running in normal operation + #[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, diff --git a/testnet/stacks-node/src/tests/signer/mod.rs b/testnet/stacks-node/src/tests/signer/mod.rs index ebb0990411..c7dc9f2a1b 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; @@ -696,7 +682,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::new(), // We are just reading so don't care what the key is false, @@ -764,7 +750,7 @@ impl + Send + 'static, T: SignerEventTrait + 'static> SignerTest