Skip to content

Commit 3e54697

Browse files
authored
Merge branch 'develop' into feat/better-win-detection
2 parents a1e5dee + 2ba3483 commit 3e54697

File tree

8 files changed

+218
-96
lines changed

8 files changed

+218
-96
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
77

88
## [Unreleased]
99

10+
### Added
11+
- Add `dry_run` configuration option to `stacks-signer` config toml. Dry run mode will
12+
run the signer binary as if it were a registered signer. Instead of broadcasting
13+
`StackerDB` messages, it logs `INFO` messages. Other interactions with the `stacks-node`
14+
behave normally (e.g., submitting validation requests, submitting finished blocks). A
15+
dry run signer will error out if the supplied key is actually a registered signer.
16+
1017
### Changed
1118

1219
- Miner will include other transactions in blocks with tenure extend transactions (#5760)

stacks-signer/src/client/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ pub(crate) mod tests {
144144
use stacks_common::util::hash::{Hash160, Sha256Sum};
145145

146146
use super::*;
147-
use crate::config::{GlobalConfig, SignerConfig};
147+
use crate::config::{GlobalConfig, SignerConfig, SignerConfigMode};
148148

149149
pub struct MockServerClient {
150150
pub server: TcpListener,
@@ -393,8 +393,10 @@ pub(crate) mod tests {
393393
}
394394
SignerConfig {
395395
reward_cycle,
396-
signer_id: 0,
397-
signer_slot_id: SignerSlotID(rand::thread_rng().gen_range(0..num_signers)), // Give a random signer slot id between 0 and num_signers
396+
signer_mode: SignerConfigMode::Normal {
397+
signer_id: 0,
398+
signer_slot_id: SignerSlotID(rand::thread_rng().gen_range(0..num_signers)), // Give a random signer slot id between 0 and num_signers
399+
},
398400
signer_entries: SignerEntries {
399401
signer_addr_to_id,
400402
signer_id_to_pk,

stacks-signer/src/client/stackerdb.rs

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ use clarity::codec::read_next;
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_info, slog_warn};
2323
use stacks_common::types::chainstate::StacksPrivateKey;
24-
use stacks_common::{debug, warn};
24+
use stacks_common::util::hash::to_hex;
25+
use stacks_common::{debug, info, warn};
2526

2627
use crate::client::{retry_with_exponential_backoff, ClientError};
27-
use crate::config::SignerConfig;
28+
use crate::config::{SignerConfig, SignerConfigMode};
2829

2930
/// The signer StackerDB slot ID, purposefully wrapped to prevent conflation with SignerID
3031
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, PartialOrd, Ord)]
@@ -36,6 +37,12 @@ impl std::fmt::Display for SignerSlotID {
3637
}
3738
}
3839

40+
#[derive(Debug)]
41+
enum StackerDBMode {
42+
DryRun,
43+
Normal { signer_slot_id: SignerSlotID },
44+
}
45+
3946
/// The StackerDB client for communicating with the .signers contract
4047
#[derive(Debug)]
4148
pub struct StackerDB<M: MessageSlotID + std::cmp::Eq> {
@@ -46,32 +53,60 @@ pub struct StackerDB<M: MessageSlotID + std::cmp::Eq> {
4653
stacks_private_key: StacksPrivateKey,
4754
/// A map of a message ID to last chunk version for each session
4855
slot_versions: HashMap<M, HashMap<SignerSlotID, u32>>,
49-
/// The signer slot ID -- the index into the signer list for this signer daemon's signing key.
50-
signer_slot_id: SignerSlotID,
56+
/// The running mode of the stackerdb (whether the signer is running in dry-run or
57+
/// normal operation)
58+
mode: StackerDBMode,
5159
/// The reward cycle of the connecting signer
5260
reward_cycle: u64,
5361
}
5462

5563
impl<M: MessageSlotID + 'static> From<&SignerConfig> for StackerDB<M> {
5664
fn from(config: &SignerConfig) -> Self {
65+
let mode = match config.signer_mode {
66+
SignerConfigMode::DryRun => StackerDBMode::DryRun,
67+
SignerConfigMode::Normal {
68+
ref signer_slot_id, ..
69+
} => StackerDBMode::Normal {
70+
signer_slot_id: *signer_slot_id,
71+
},
72+
};
73+
5774
Self::new(
5875
&config.node_host,
5976
config.stacks_private_key,
6077
config.mainnet,
6178
config.reward_cycle,
62-
config.signer_slot_id,
79+
mode,
6380
)
6481
}
6582
}
6683

6784
impl<M: MessageSlotID + 'static> StackerDB<M> {
68-
/// Create a new StackerDB client
69-
pub fn new(
85+
#[cfg(any(test, feature = "testing"))]
86+
/// Create a StackerDB client in normal operation (i.e., not a dry-run signer)
87+
pub fn new_normal(
7088
host: &str,
7189
stacks_private_key: StacksPrivateKey,
7290
is_mainnet: bool,
7391
reward_cycle: u64,
7492
signer_slot_id: SignerSlotID,
93+
) -> Self {
94+
Self::new(
95+
host,
96+
stacks_private_key,
97+
is_mainnet,
98+
reward_cycle,
99+
StackerDBMode::Normal { signer_slot_id },
100+
)
101+
}
102+
103+
/// Create a new StackerDB client
104+
fn new(
105+
host: &str,
106+
stacks_private_key: StacksPrivateKey,
107+
is_mainnet: bool,
108+
reward_cycle: u64,
109+
signer_mode: StackerDBMode,
75110
) -> Self {
76111
let mut signers_message_stackerdb_sessions = HashMap::new();
77112
for msg_id in M::all() {
@@ -84,7 +119,7 @@ impl<M: MessageSlotID + 'static> StackerDB<M> {
84119
signers_message_stackerdb_sessions,
85120
stacks_private_key,
86121
slot_versions: HashMap::new(),
87-
signer_slot_id,
122+
mode: signer_mode,
88123
reward_cycle,
89124
}
90125
}
@@ -110,18 +145,33 @@ impl<M: MessageSlotID + 'static> StackerDB<M> {
110145
msg_id: &M,
111146
message_bytes: Vec<u8>,
112147
) -> Result<StackerDBChunkAckData, ClientError> {
113-
let slot_id = self.signer_slot_id;
148+
let StackerDBMode::Normal {
149+
signer_slot_id: slot_id,
150+
} = &self.mode
151+
else {
152+
info!(
153+
"Dry-run signer would have sent a stackerdb message";
154+
"message_id" => ?msg_id,
155+
"message_bytes" => to_hex(&message_bytes)
156+
);
157+
return Ok(StackerDBChunkAckData {
158+
accepted: true,
159+
reason: None,
160+
metadata: None,
161+
code: None,
162+
});
163+
};
114164
loop {
115165
let mut slot_version = if let Some(versions) = self.slot_versions.get_mut(msg_id) {
116-
if let Some(version) = versions.get(&slot_id) {
166+
if let Some(version) = versions.get(slot_id) {
117167
*version
118168
} else {
119-
versions.insert(slot_id, 0);
169+
versions.insert(*slot_id, 0);
120170
1
121171
}
122172
} else {
123173
let mut versions = HashMap::new();
124-
versions.insert(slot_id, 0);
174+
versions.insert(*slot_id, 0);
125175
self.slot_versions.insert(*msg_id, versions);
126176
1
127177
};
@@ -143,7 +193,7 @@ impl<M: MessageSlotID + 'static> StackerDB<M> {
143193

144194
if let Some(versions) = self.slot_versions.get_mut(msg_id) {
145195
// NOTE: per the above, this is always executed
146-
versions.insert(slot_id, slot_version.saturating_add(1));
196+
versions.insert(*slot_id, slot_version.saturating_add(1));
147197
} else {
148198
return Err(ClientError::NotConnected);
149199
}
@@ -165,7 +215,7 @@ impl<M: MessageSlotID + 'static> StackerDB<M> {
165215
}
166216
if let Some(versions) = self.slot_versions.get_mut(msg_id) {
167217
// NOTE: per the above, this is always executed
168-
versions.insert(slot_id, slot_version.saturating_add(1));
218+
versions.insert(*slot_id, slot_version.saturating_add(1));
169219
} else {
170220
return Err(ClientError::NotConnected);
171221
}
@@ -216,11 +266,6 @@ impl<M: MessageSlotID + 'static> StackerDB<M> {
216266
u32::try_from(self.reward_cycle % 2).expect("FATAL: reward cycle % 2 exceeds u32::MAX")
217267
}
218268

219-
/// Retrieve the signer slot ID
220-
pub fn get_signer_slot_id(&self) -> SignerSlotID {
221-
self.signer_slot_id
222-
}
223-
224269
/// Get the session corresponding to the given message ID if it exists
225270
pub fn get_session_mut(&mut self, msg_id: &M) -> Option<&mut StackerDBSession> {
226271
self.signers_message_stackerdb_sessions.get_mut(msg_id)

stacks-signer/src/config.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const BLOCK_PROPOSAL_TIMEOUT_MS: u64 = 600_000;
3939
const BLOCK_PROPOSAL_VALIDATION_TIMEOUT_MS: u64 = 120_000;
4040
const DEFAULT_FIRST_PROPOSAL_BURN_BLOCK_TIMING_SECS: u64 = 60;
4141
const DEFAULT_TENURE_LAST_BLOCK_PROPOSAL_TIMEOUT_SECS: u64 = 30;
42+
const DEFAULT_DRY_RUN: bool = false;
4243
const TENURE_IDLE_TIMEOUT_SECS: u64 = 120;
4344

4445
#[derive(thiserror::Error, Debug)]
@@ -106,15 +107,36 @@ impl Network {
106107
}
107108
}
108109

110+
/// Signer config mode (whether dry-run or real)
111+
#[derive(Debug, Clone)]
112+
pub enum SignerConfigMode {
113+
/// Dry run operation: signer is not actually registered, the signer
114+
/// will not submit stackerdb messages, etc.
115+
DryRun,
116+
/// Normal signer operation: if registered, the signer will submit
117+
/// stackerdb messages, etc.
118+
Normal {
119+
/// The signer ID assigned to this signer (may be different from signer_slot_id)
120+
signer_id: u32,
121+
/// The signer stackerdb slot id (may be different from signer_id)
122+
signer_slot_id: SignerSlotID,
123+
},
124+
}
125+
126+
impl std::fmt::Display for SignerConfigMode {
127+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128+
match self {
129+
SignerConfigMode::DryRun => write!(f, "Dry-Run signer"),
130+
SignerConfigMode::Normal { signer_id, .. } => write!(f, "signer #{signer_id}"),
131+
}
132+
}
133+
}
134+
109135
/// The Configuration info needed for an individual signer per reward cycle
110136
#[derive(Debug, Clone)]
111137
pub struct SignerConfig {
112138
/// The reward cycle of the configuration
113139
pub reward_cycle: u64,
114-
/// The signer ID assigned to this signer (may be different from signer_slot_id)
115-
pub signer_id: u32,
116-
/// The signer stackerdb slot id (may be different from signer_id)
117-
pub signer_slot_id: SignerSlotID,
118140
/// The registered signers for this reward cycle
119141
pub signer_entries: SignerEntries,
120142
/// The signer slot ids of all signers registered for this reward cycle
@@ -141,6 +163,8 @@ pub struct SignerConfig {
141163
pub tenure_idle_timeout: Duration,
142164
/// The maximum age of a block proposal in seconds that will be processed by the signer
143165
pub block_proposal_max_age_secs: u64,
166+
/// The running mode for the signer (dry-run or normal)
167+
pub signer_mode: SignerConfigMode,
144168
}
145169

146170
/// The parsed configuration for the signer
@@ -181,6 +205,8 @@ pub struct GlobalConfig {
181205
pub tenure_idle_timeout: Duration,
182206
/// The maximum age of a block proposal that will be processed by the signer
183207
pub block_proposal_max_age_secs: u64,
208+
/// Is this signer binary going to be running in dry-run mode?
209+
pub dry_run: bool,
184210
}
185211

186212
/// Internal struct for loading up the config file
@@ -220,6 +246,8 @@ struct RawConfigFile {
220246
pub tenure_idle_timeout_secs: Option<u64>,
221247
/// The maximum age of a block proposal (in secs) that will be processed by the signer.
222248
pub block_proposal_max_age_secs: Option<u64>,
249+
/// Is this signer binary going to be running in dry-run mode?
250+
pub dry_run: Option<bool>,
223251
}
224252

225253
impl RawConfigFile {
@@ -321,6 +349,8 @@ impl TryFrom<RawConfigFile> for GlobalConfig {
321349
.block_proposal_max_age_secs
322350
.unwrap_or(DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS);
323351

352+
let dry_run = raw_data.dry_run.unwrap_or(DEFAULT_DRY_RUN);
353+
324354
Ok(Self {
325355
node_host: raw_data.node_host,
326356
endpoint,
@@ -338,6 +368,7 @@ impl TryFrom<RawConfigFile> for GlobalConfig {
338368
block_proposal_validation_timeout,
339369
tenure_idle_timeout,
340370
block_proposal_max_age_secs,
371+
dry_run,
341372
})
342373
}
343374
}

stacks-signer/src/runloop.rs

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use stacks_common::{debug, error, info, warn};
2525

2626
use crate::chainstate::SortitionsView;
2727
use crate::client::{retry_with_exponential_backoff, ClientError, StacksClient};
28-
use crate::config::{GlobalConfig, SignerConfig};
28+
use crate::config::{GlobalConfig, SignerConfig, SignerConfigMode};
2929
#[cfg(any(test, feature = "testing"))]
3030
use crate::v0::tests::TEST_SKIP_SIGNER_CLEANUP;
3131
use crate::Signer as SignerTrait;
@@ -39,6 +39,9 @@ pub enum ConfigurationError {
3939
/// The stackerdb signer config is not yet updated
4040
#[error("The stackerdb config is not yet updated")]
4141
StackerDBNotUpdated,
42+
/// The signer binary is configured as dry-run, but is also registered for this cycle
43+
#[error("The signer binary is configured as dry-run, but is also registered for this cycle")]
44+
DryRunStackerIsRegistered,
4245
}
4346

4447
/// The internal signer state info
@@ -258,27 +261,48 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug> RunLo
258261
warn!("Error while fetching stackerdb slots {reward_cycle}: {e:?}");
259262
e
260263
})?;
264+
265+
let dry_run = self.config.dry_run;
261266
let current_addr = self.stacks_client.get_signer_address();
262267

263-
let Some(signer_slot_id) = signer_slot_ids.get(current_addr) else {
264-
warn!(
268+
let signer_config_mode = if !dry_run {
269+
let Some(signer_slot_id) = signer_slot_ids.get(current_addr) else {
270+
warn!(
265271
"Signer {current_addr} was not found in stacker db. Must not be registered for this reward cycle {reward_cycle}."
266272
);
267-
return Ok(None);
268-
};
269-
let Some(signer_id) = signer_entries.signer_addr_to_id.get(current_addr) else {
270-
warn!(
271-
"Signer {current_addr} was found in stacker db but not the reward set for reward cycle {reward_cycle}."
273+
return Ok(None);
274+
};
275+
let Some(signer_id) = signer_entries.signer_addr_to_id.get(current_addr) else {
276+
warn!(
277+
"Signer {current_addr} was found in stacker db but not the reward set for reward cycle {reward_cycle}."
278+
);
279+
return Ok(None);
280+
};
281+
info!(
282+
"Signer #{signer_id} ({current_addr}) is registered for reward cycle {reward_cycle}."
272283
);
273-
return Ok(None);
284+
SignerConfigMode::Normal {
285+
signer_slot_id: *signer_slot_id,
286+
signer_id: *signer_id,
287+
}
288+
} else {
289+
if signer_slot_ids.contains_key(current_addr) {
290+
error!(
291+
"Signer is configured for dry-run, but the signer address {current_addr} was found in stacker db."
292+
);
293+
return Err(ConfigurationError::DryRunStackerIsRegistered);
294+
};
295+
if signer_entries.signer_addr_to_id.contains_key(current_addr) {
296+
warn!(
297+
"Signer {current_addr} was found in stacker db but not the reward set for reward cycle {reward_cycle}."
298+
);
299+
return Ok(None);
300+
};
301+
SignerConfigMode::DryRun
274302
};
275-
info!(
276-
"Signer #{signer_id} ({current_addr}) is registered for reward cycle {reward_cycle}."
277-
);
278303
Ok(Some(SignerConfig {
279304
reward_cycle,
280-
signer_id: *signer_id,
281-
signer_slot_id: *signer_slot_id,
305+
signer_mode: signer_config_mode,
282306
signer_entries,
283307
signer_slot_ids: signer_slot_ids.into_values().collect(),
284308
first_proposal_burn_block_timing: self.config.first_proposal_burn_block_timing,
@@ -299,9 +323,9 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug> RunLo
299323
let reward_index = reward_cycle % 2;
300324
let new_signer_config = match self.get_signer_config(reward_cycle) {
301325
Ok(Some(new_signer_config)) => {
302-
let signer_id = new_signer_config.signer_id;
326+
let signer_mode = new_signer_config.signer_mode.clone();
303327
let new_signer = Signer::new(new_signer_config);
304-
info!("{new_signer} Signer is registered for reward cycle {reward_cycle} as signer #{signer_id}. Initialized signer state.");
328+
info!("{new_signer} Signer is registered for reward cycle {reward_cycle} as {signer_mode}. Initialized signer state.");
305329
ConfiguredSigner::RegisteredSigner(new_signer)
306330
}
307331
Ok(None) => {

0 commit comments

Comments
 (0)