Skip to content

Commit

Permalink
feat: implement dry-run mode for signer
Browse files Browse the repository at this point in the history
  • Loading branch information
kantai committed Jan 10, 2025
1 parent f4db3c9 commit b5b27a3
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 50 deletions.
69 changes: 48 additions & 21 deletions stacks-signer/src/client/stackerdb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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<M: MessageSlotID + std::cmp::Eq> {
Expand All @@ -46,32 +53,42 @@ pub struct StackerDB<M: MessageSlotID + std::cmp::Eq> {
stacks_private_key: StacksPrivateKey,
/// A map of a message ID to last chunk version for each session
slot_versions: HashMap<M, HashMap<SignerSlotID, u32>>,
/// 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<M: MessageSlotID + 'static> From<&SignerConfig> for StackerDB<M> {
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<M: MessageSlotID + 'static> StackerDB<M> {
/// 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() {
Expand All @@ -84,7 +101,7 @@ impl<M: MessageSlotID + 'static> StackerDB<M> {
signers_message_stackerdb_sessions,
stacks_private_key,
slot_versions: HashMap::new(),
signer_slot_id,
mode: signer_mode,
reward_cycle,
}
}
Expand All @@ -110,18 +127,33 @@ impl<M: MessageSlotID + 'static> StackerDB<M> {
msg_id: &M,
message_bytes: Vec<u8>,
) -> Result<StackerDBChunkAckData, ClientError> {
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
};
Expand All @@ -143,7 +175,7 @@ impl<M: MessageSlotID + 'static> StackerDB<M> {

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);
}
Expand All @@ -165,7 +197,7 @@ impl<M: MessageSlotID + 'static> StackerDB<M> {
}
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);
}
Expand Down Expand Up @@ -216,11 +248,6 @@ impl<M: MessageSlotID + 'static> StackerDB<M> {
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)
Expand Down
39 changes: 35 additions & 4 deletions stacks-signer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -220,6 +246,8 @@ struct RawConfigFile {
pub tenure_idle_timeout_secs: Option<u64>,
/// The maximum age of a block proposal (in secs) that will be processed by the signer.
pub block_proposal_max_age_secs: Option<u64>,
/// Is this signer binary going to be running in dry-run mode?
pub dry_run: Option<bool>,
}

impl RawConfigFile {
Expand Down Expand Up @@ -321,6 +349,8 @@ impl TryFrom<RawConfigFile> 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,
Expand All @@ -338,6 +368,7 @@ impl TryFrom<RawConfigFile> for GlobalConfig {
block_proposal_validation_timeout,
tenure_idle_timeout,
block_proposal_max_age_secs,
dry_run,
})
}
}
Expand Down
56 changes: 40 additions & 16 deletions stacks-signer/src/runloop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
use crate::Signer as SignerTrait;

#[derive(thiserror::Error, Debug)]
Expand All @@ -37,6 +37,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
Expand Down Expand Up @@ -254,27 +257,48 @@ impl<Signer: SignerTrait<T>, 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,
Expand All @@ -295,9 +319,9 @@ impl<Signer: SignerTrait<T>, 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) => {
Expand Down
Loading

0 comments on commit b5b27a3

Please sign in to comment.