{
#[cfg(feature = "slog_json")]
fn make_json_logger() -> Logger {
+ use std::sync::Mutex;
+
+ use slog::FnValue;
+
let def_keys = o!("file" => FnValue(move |info| {
info.file()
}),
diff --git a/stacks-common/src/util/mod.rs b/stacks-common/src/util/mod.rs
index 95ca7eeec0..46158d2f4f 100644
--- a/stacks-common/src/util/mod.rs
+++ b/stacks-common/src/util/mod.rs
@@ -28,15 +28,15 @@ pub mod secp256k1;
pub mod uint;
pub mod vrf;
-use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, BufWriter, Write};
-use std::path::{Path, PathBuf};
+use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{error, fmt, thread, time};
/// Given a relative path inside the Cargo workspace, return the absolute path
-pub fn cargo_workspace(relative_path: P) -> PathBuf
+#[cfg(any(test, feature = "testing"))]
+pub fn cargo_workspace
(relative_path: P) -> std::path::PathBuf
where
P: AsRef,
{
diff --git a/stacks-common/src/util/pipe.rs b/stacks-common/src/util/pipe.rs
index 86d92abd61..4407fee71f 100644
--- a/stacks-common/src/util/pipe.rs
+++ b/stacks-common/src/util/pipe.rs
@@ -21,8 +21,6 @@ use std::io;
use std::io::{Read, Write};
use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TryRecvError, TrySendError};
-use crate::util::log;
-
/// Inter-thread pipe for streaming messages, built on channels.
/// Used mainly in conjunction with networking.
///
@@ -316,7 +314,6 @@ impl Write for PipeWrite {
#[cfg(test)]
mod test {
- use std::io::prelude::*;
use std::io::{Read, Write};
use std::{io, thread};
@@ -324,7 +321,6 @@ mod test {
use rand::RngCore;
use super::*;
- use crate::util::*;
#[test]
fn test_connection_pipe_oneshot() {
diff --git a/stacks-common/src/util/retry.rs b/stacks-common/src/util/retry.rs
index e7f6c0b140..47801289a3 100644
--- a/stacks-common/src/util/retry.rs
+++ b/stacks-common/src/util/retry.rs
@@ -18,11 +18,7 @@
*/
use std::io;
-use std::io::prelude::*;
-use std::io::{Read, Write};
-
-use crate::util::hash::to_hex;
-use crate::util::log;
+use std::io::Read;
/// Wrap a Read so that we store a copy of what was read.
/// Used for re-trying reads when we don't know what to expect from the stream.
diff --git a/stacks-common/src/util/secp256k1.rs b/stacks-common/src/util/secp256k1.rs
index 5c64838855..5d99b2c663 100644
--- a/stacks-common/src/util/secp256k1.rs
+++ b/stacks-common/src/util/secp256k1.rs
@@ -13,7 +13,7 @@
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-use rand::{thread_rng, RngCore};
+use rand::RngCore;
use secp256k1;
use secp256k1::ecdsa::{
RecoverableSignature as LibSecp256k1RecoverableSignature, RecoveryId as LibSecp256k1RecoveryID,
@@ -24,11 +24,9 @@ use secp256k1::{
PublicKey as LibSecp256k1PublicKey, Secp256k1, SecretKey as LibSecp256k1PrivateKey,
};
use serde::de::{Deserialize, Error as de_Error};
-use serde::ser::Error as ser_Error;
use serde::Serialize;
use super::hash::Sha256Sum;
-use crate::impl_byte_array_message_codec;
use crate::types::{PrivateKey, PublicKey};
use crate::util::hash::{hex_bytes, to_hex};
@@ -123,7 +121,7 @@ impl Default for Secp256k1PublicKey {
impl Secp256k1PublicKey {
#[cfg(any(test, feature = "testing"))]
pub fn new() -> Secp256k1PublicKey {
- Secp256k1PublicKey::from_private(&Secp256k1PrivateKey::new())
+ Secp256k1PublicKey::from_private(&Secp256k1PrivateKey::random())
}
pub fn from_hex(hex_string: &str) -> Result {
@@ -249,14 +247,8 @@ impl PublicKey for Secp256k1PublicKey {
}
}
-impl Default for Secp256k1PrivateKey {
- fn default() -> Self {
- Self::new()
- }
-}
-
impl Secp256k1PrivateKey {
- pub fn new() -> Secp256k1PrivateKey {
+ pub fn random() -> Secp256k1PrivateKey {
let mut rng = rand::thread_rng();
loop {
// keep trying to generate valid bytes
@@ -442,8 +434,8 @@ mod tests {
use secp256k1::{PublicKey as LibSecp256k1PublicKey, Secp256k1};
use super::*;
+ use crate::util::get_epoch_time_ms;
use crate::util::hash::hex_bytes;
- use crate::util::{get_epoch_time_ms, log};
struct KeyFixture {
input: I,
@@ -460,7 +452,7 @@ mod tests {
#[test]
fn test_parse_serialize_compressed() {
- let mut t1 = Secp256k1PrivateKey::new();
+ let mut t1 = Secp256k1PrivateKey::random();
t1.set_compress_public(true);
let h_comp = t1.to_hex();
t1.set_compress_public(false);
@@ -654,7 +646,7 @@ mod tests {
let mut rng = rand::thread_rng();
for i in 0..100 {
- let privk = Secp256k1PrivateKey::new();
+ let privk = Secp256k1PrivateKey::random();
let pubk = Secp256k1PublicKey::from_private(&privk);
let mut msg = [0u8; 32];
diff --git a/stacks-common/src/util/tests.rs b/stacks-common/src/util/tests.rs
index b87e913718..1b01a449be 100644
--- a/stacks-common/src/util/tests.rs
+++ b/stacks-common/src/util/tests.rs
@@ -94,6 +94,6 @@ impl TestFlag {
/// assert_eq!(test_flag.get(), 123);
/// ```
pub fn get(&self) -> T {
- self.0.lock().unwrap().clone().unwrap_or_default().clone()
+ self.0.lock().unwrap().clone().unwrap_or_default()
}
}
diff --git a/stacks-common/src/util/vrf.rs b/stacks-common/src/util/vrf.rs
index 0c2b2c3dad..5c7439daf9 100644
--- a/stacks-common/src/util/vrf.rs
+++ b/stacks-common/src/util/vrf.rs
@@ -22,16 +22,11 @@ use std::fmt::Debug;
use std::hash::{Hash, Hasher};
/// This codebase is based on routines defined in the IETF draft for verifiable random functions
/// over elliptic curves (https://tools.ietf.org/id/draft-irtf-cfrg-vrf-02.html).
-use std::ops::Deref;
-use std::ops::DerefMut;
use std::{error, fmt};
use curve25519_dalek::constants::ED25519_BASEPOINT_POINT;
use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
use curve25519_dalek::scalar::Scalar as ed25519_Scalar;
-use ed25519_dalek::{
- SecretKey as EdDalekSecretKeyBytes, SigningKey as EdPrivateKey, VerifyingKey as EdPublicKey,
-};
use rand;
use sha2::{Digest, Sha512};
@@ -535,10 +530,8 @@ impl VRF {
#[cfg(test)]
mod tests {
- use curve25519_dalek::scalar::Scalar as ed25519_Scalar;
use rand;
use rand::RngCore;
- use sha2::Sha512;
use super::*;
use crate::util::hash::hex_bytes;
diff --git a/stacks-signer/CHANGELOG.md b/stacks-signer/CHANGELOG.md
index e634d73172..2e801d680d 100644
--- a/stacks-signer/CHANGELOG.md
+++ b/stacks-signer/CHANGELOG.md
@@ -7,39 +7,60 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
## [Unreleased]
-## Added
+### Added
+
+### Changed
+
+## [3.1.0.0.5.0]
+
+### 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.
+
+## [3.1.0.0.4.0]
+
+### Added
-## Changed
+- When a new block proposal is received while the signer is waiting for an existing proposal to be validated, the signer will wait until the existing block is done validating before submitting the new one for validating. ([#5453](https://github.com/stacks-network/stacks-core/pull/5453))
+- Introduced two new prometheus metrics:
+ - `stacks_signer_block_validation_latencies_histogram`: the validation_time_ms reported by the node when validating a block proposal
+ - `stacks_signer_block_response_latencies_histogram`: the "end-to-end" time it takes for the signer to issue a block response
+
+### Changed
## [3.1.0.0.3.0]
-## Added
+### Added
- Introduced the `block_proposal_max_age_secs` configuration option for signers, enabling them to automatically ignore block proposals that exceed the specified age in seconds.
-## Changed
+### Changed
- Improvements to the stale signer cleanup logic: deletes the prior signer if it has no remaining unprocessed blocks in its database
- Signers now listen to new block events from the stacks node to determine whether a block has been successfully appended to the chain tip
-# [3.1.0.0.2.1]
+## [3.1.0.0.2.1]
-## Added
+### Added
-## Changed
+### Changed
- Prevent old reward cycle signers from processing block validation response messages that do not apply to blocks from their cycle.
-# [3.1.0.0.2.1]
+## [3.1.0.0.2.1]
-## Added
+### Added
-## Changed
+### Changed
- Prevent old reward cycle signers from processing block validation response messages that do not apply to blocks from their cycle.
## [3.1.0.0.2.0]
-## Added
+### Added
- **SIP-029 consensus rules, activating in epoch 3.1 at block 875,000** (see [SIP-029](https://github.com/will-corcoran/sips/blob/feat/sip-029-halving-alignment/sips/sip-029/sip-029-halving-alignment.md) for details)
diff --git a/stacks-signer/src/chainstate.rs b/stacks-signer/src/chainstate.rs
index 31454c96b6..3e59e58850 100644
--- a/stacks-signer/src/chainstate.rs
+++ b/stacks-signer/src/chainstate.rs
@@ -89,10 +89,8 @@ impl SortitionState {
if self.miner_status != SortitionMinerStatus::Valid {
return Ok(false);
}
- // if we've already signed a block in this tenure, the miner can't have timed out.
- let has_blocks = signer_db
- .get_last_signed_block_in_tenure(&self.consensus_hash)?
- .is_some();
+ // if we've already seen a proposed block from this miner. It cannot have timed out.
+ let has_blocks = signer_db.has_proposed_block_in_tenure(&self.consensus_hash)?;
if has_blocks {
return Ok(false);
}
@@ -202,6 +200,7 @@ impl SortitionsView {
info!(
"Current miner timed out, marking as invalid.";
"block_height" => block.header.chain_length,
+ "block_proposal_timeout" => ?self.config.block_proposal_timeout,
"current_sortition_consensus_hash" => ?self.cur_sortition.consensus_hash,
);
self.cur_sortition.miner_status = SortitionMinerStatus::InvalidatedBeforeFirstBlock;
@@ -322,7 +321,7 @@ impl SortitionsView {
return Ok(false);
}
}
- ProposedBy::LastSortition(_last_sortition) => {
+ ProposedBy::LastSortition(last_sortition) => {
// should only consider blocks from the last sortition if the new sortition was invalidated
// before we signed their first block.
if self.cur_sortition.miner_status
@@ -333,6 +332,7 @@ impl SortitionsView {
"proposed_block_consensus_hash" => %block.header.consensus_hash,
"proposed_block_signer_sighash" => %block.header.signer_signature_hash(),
"current_sortition_miner_status" => ?self.cur_sortition.miner_status,
+ "last_sortition" => %last_sortition.consensus_hash
);
return Ok(false);
}
@@ -589,8 +589,8 @@ impl SortitionsView {
signer_db.block_lookup(&nakamoto_tip.signer_signature_hash())
{
if block_info.state != BlockState::GloballyAccepted {
- if let Err(e) = block_info.mark_globally_accepted() {
- warn!("Failed to update block info in db: {e}");
+ if let Err(e) = signer_db.mark_block_globally_accepted(&mut block_info) {
+ warn!("Failed to mark block as globally accepted: {e}");
} else if let Err(e) = signer_db.insert_block(&block_info) {
warn!("Failed to update block info in db: {e}");
}
diff --git a/stacks-signer/src/cli.rs b/stacks-signer/src/cli.rs
index 7b666d3762..5d5b8806e7 100644
--- a/stacks-signer/src/cli.rs
+++ b/stacks-signer/src/cli.rs
@@ -340,14 +340,14 @@ pub fn parse_pox_addr(pox_address_literal: &str) -> Result {
Ok,
);
match parsed_addr {
- Ok(PoxAddress::Standard(addr, None)) => match addr.version {
+ Ok(PoxAddress::Standard(addr, None)) => match addr.version() {
C32_ADDRESS_VERSION_MAINNET_MULTISIG | C32_ADDRESS_VERSION_TESTNET_MULTISIG => Ok(
PoxAddress::Standard(addr, Some(AddressHashMode::SerializeP2SH)),
),
C32_ADDRESS_VERSION_MAINNET_SINGLESIG | C32_ADDRESS_VERSION_TESTNET_SINGLESIG => Ok(
PoxAddress::Standard(addr, Some(AddressHashMode::SerializeP2PKH)),
),
- _ => Err(format!("Invalid address version: {}", addr.version)),
+ _ => Err(format!("Invalid address version: {}", addr.version())),
},
_ => parsed_addr,
}
@@ -451,7 +451,7 @@ mod tests {
);
match pox_addr {
PoxAddress::Standard(stacks_addr, hash_mode) => {
- assert_eq!(stacks_addr.version, 22);
+ assert_eq!(stacks_addr.version(), 22);
assert_eq!(hash_mode, Some(AddressHashMode::SerializeP2PKH));
}
_ => panic!("Invalid parsed address"),
@@ -467,7 +467,7 @@ mod tests {
make_message_hash(&pox_addr);
match pox_addr {
PoxAddress::Standard(stacks_addr, hash_mode) => {
- assert_eq!(stacks_addr.version, 20);
+ assert_eq!(stacks_addr.version(), 20);
assert_eq!(hash_mode, Some(AddressHashMode::SerializeP2SH));
}
_ => panic!("Invalid parsed address"),
@@ -483,7 +483,7 @@ mod tests {
make_message_hash(&pox_addr);
match pox_addr {
PoxAddress::Standard(stacks_addr, hash_mode) => {
- assert_eq!(stacks_addr.version, C32_ADDRESS_VERSION_TESTNET_SINGLESIG);
+ assert_eq!(stacks_addr.version(), C32_ADDRESS_VERSION_TESTNET_SINGLESIG);
assert_eq!(hash_mode, Some(AddressHashMode::SerializeP2PKH));
}
_ => panic!("Invalid parsed address"),
diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs
index bdaa368567..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,
@@ -302,7 +302,7 @@ pub(crate) mod tests {
pox_consensus_hash: Option,
) -> (String, RPCPeerInfoData) {
// Generate some random info
- let private_key = StacksPrivateKey::new();
+ let private_key = StacksPrivateKey::random();
let public_key = StacksPublicKey::from_private(&private_key);
let public_key_buf = StacksPublicKeyBuffer::from_public_key(&public_key);
let public_key_hash = Hash160::from_node_public_key(&public_key);
@@ -376,7 +376,7 @@ pub(crate) mod tests {
let private_key = if signer_id == 0 {
config.stacks_private_key
} else {
- StacksPrivateKey::new()
+ StacksPrivateKey::random()
};
let public_key = StacksPublicKey::from_private(&private_key);
@@ -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 0316976a4c..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)
@@ -248,7 +293,7 @@ mod tests {
#[test]
fn send_signer_message_should_succeed() {
let signer_config = build_signer_config_tomls(
- &[StacksPrivateKey::new()],
+ &[StacksPrivateKey::random()],
"localhost:20443",
Some(Duration::from_millis(128)), // Timeout defaults to 5 seconds. Let's override it to 128 milliseconds.
&Network::Testnet,
diff --git a/stacks-signer/src/client/stacks_client.rs b/stacks-signer/src/client/stacks_client.rs
index 4676738629..db0b356fb4 100644
--- a/stacks-signer/src/client/stacks_client.rs
+++ b/stacks-signer/src/client/stacks_client.rs
@@ -323,8 +323,10 @@ impl StacksClient {
block,
chain_id: self.chain_id,
};
- let timer =
- crate::monitoring::new_rpc_call_timer(&self.block_proposal_path(), &self.http_origin);
+ let timer = crate::monitoring::actions::new_rpc_call_timer(
+ &self.block_proposal_path(),
+ &self.http_origin,
+ );
let send_request = || {
self.stacks_node_client
.post(self.block_proposal_path())
@@ -399,7 +401,8 @@ impl StacksClient {
"{}{RPC_TENURE_FORKING_INFO_PATH}/:start/:stop",
self.http_origin
);
- let timer = crate::monitoring::new_rpc_call_timer(&metrics_path, &self.http_origin);
+ let timer =
+ crate::monitoring::actions::new_rpc_call_timer(&metrics_path, &self.http_origin);
let send_request = || {
self.stacks_node_client
.get(&path)
@@ -420,7 +423,7 @@ impl StacksClient {
pub fn get_current_and_last_sortition(&self) -> Result {
debug!("StacksClient: Getting current and prior sortition");
let path = format!("{}/latest_and_last", self.sortition_info_path());
- let timer = crate::monitoring::new_rpc_call_timer(&path, &self.http_origin);
+ let timer = crate::monitoring::actions::new_rpc_call_timer(&path, &self.http_origin);
let send_request = || {
self.stacks_node_client.get(&path).send().map_err(|e| {
warn!("Signer failed to request latest sortition"; "err" => ?e);
@@ -460,8 +463,10 @@ impl StacksClient {
/// Get the current peer info data from the stacks node
pub fn get_peer_info(&self) -> Result {
debug!("StacksClient: Getting peer info");
- let timer =
- crate::monitoring::new_rpc_call_timer(&self.core_info_path(), &self.http_origin);
+ let timer = crate::monitoring::actions::new_rpc_call_timer(
+ &self.core_info_path(),
+ &self.http_origin,
+ );
let send_request = || {
self.stacks_node_client
.get(self.core_info_path())
@@ -485,7 +490,7 @@ impl StacksClient {
debug!("StacksClient: Getting reward set signers";
"reward_cycle" => reward_cycle,
);
- let timer = crate::monitoring::new_rpc_call_timer(
+ let timer = crate::monitoring::actions::new_rpc_call_timer(
&format!("{}/v3/stacker_set/:reward_cycle", self.http_origin),
&self.http_origin,
);
@@ -521,7 +526,8 @@ impl StacksClient {
/// Retrieve the current pox data from the stacks node
pub fn get_pox_data(&self) -> Result {
debug!("StacksClient: Getting pox data");
- let timer = crate::monitoring::new_rpc_call_timer(&self.pox_path(), &self.http_origin);
+ let timer =
+ crate::monitoring::actions::new_rpc_call_timer(&self.pox_path(), &self.http_origin);
let send_request = || {
self.stacks_node_client
.get(self.pox_path())
@@ -572,7 +578,7 @@ impl StacksClient {
"address" => %address,
);
let timer_label = format!("{}/v2/accounts/:principal", self.http_origin);
- let timer = crate::monitoring::new_rpc_call_timer(&timer_label, &self.http_origin);
+ let timer = crate::monitoring::actions::new_rpc_call_timer(&timer_label, &self.http_origin);
let send_request = || {
self.stacks_node_client
.get(self.accounts_path(address))
@@ -628,7 +634,7 @@ impl StacksClient {
"block_height" => %block.header.chain_length,
);
let path = format!("{}{}?broadcast=1", self.http_origin, postblock_v3::PATH);
- let timer = crate::monitoring::new_rpc_call_timer(&path, &self.http_origin);
+ let timer = crate::monitoring::actions::new_rpc_call_timer(&path, &self.http_origin);
let send_request = || {
self.stacks_node_client
.post(&path)
@@ -678,7 +684,7 @@ impl StacksClient {
"{}/v2/contracts/call-read/:principal/{contract_name}/{function_name}",
self.http_origin
);
- let timer = crate::monitoring::new_rpc_call_timer(&timer_label, &self.http_origin);
+ let timer = crate::monitoring::actions::new_rpc_call_timer(&timer_label, &self.http_origin);
let response = self
.stacks_node_client
.post(path)
@@ -1191,7 +1197,7 @@ mod tests {
#[test]
fn get_reward_set_should_succeed() {
let mock = MockServerClient::new();
- let private_key = StacksPrivateKey::new();
+ let private_key = StacksPrivateKey::random();
let public_key = StacksPublicKey::from_private(&private_key);
let mut bytes = [0u8; 33];
bytes.copy_from_slice(&public_key.to_bytes_compressed());
diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs
index c100703fc9..29ee35c961 100644
--- a/stacks-signer/src/config.rs
+++ b/stacks-signer/src/config.rs
@@ -39,7 +39,8 @@ 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 TENURE_IDLE_TIMEOUT_SECS: u64 = 300;
+const DEFAULT_DRY_RUN: bool = false;
+const TENURE_IDLE_TIMEOUT_SECS: u64 = 120;
#[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/lib.rs b/stacks-signer/src/lib.rs
index 244675c65c..9f2df12534 100644
--- a/stacks-signer/src/lib.rs
+++ b/stacks-signer/src/lib.rs
@@ -125,7 +125,7 @@ impl + Send + 'static, T: SignerEventTrait + 'static> SpawnedSigner
);
let (res_send, res_recv) = channel();
let ev = SignerEventReceiver::new(config.network.is_mainnet());
- crate::monitoring::start_serving_monitoring_metrics(config.clone()).ok();
+ crate::monitoring::actions::start_serving_monitoring_metrics(config.clone()).ok();
let runloop = RunLoop::new(config.clone());
let mut signer: RunLoopSigner = libsigner::Signer::new(runloop, ev, res_send);
let running_signer = signer.spawn(endpoint).expect("Failed to spawn signer");
diff --git a/stacks-signer/src/main.rs b/stacks-signer/src/main.rs
index eac60cc53f..821f2e1c6e 100644
--- a/stacks-signer/src/main.rs
+++ b/stacks-signer/src/main.rs
@@ -409,10 +409,10 @@ pub mod tests {
#[test]
fn test_verify_vote() {
let mut rand = rand::thread_rng();
- let private_key = Secp256k1PrivateKey::new();
+ let private_key = Secp256k1PrivateKey::random();
let public_key = StacksPublicKey::from_private(&private_key);
- let invalid_private_key = Secp256k1PrivateKey::new();
+ let invalid_private_key = Secp256k1PrivateKey::random();
let invalid_public_key = StacksPublicKey::from_private(&invalid_private_key);
let sip = rand.next_u32();
diff --git a/stacks-signer/src/monitor_signers.rs b/stacks-signer/src/monitor_signers.rs
index 4bc017fa27..65b4fdda3e 100644
--- a/stacks-signer/src/monitor_signers.rs
+++ b/stacks-signer/src/monitor_signers.rs
@@ -55,7 +55,7 @@ impl SignerMonitor {
pub fn new(args: MonitorSignersArgs) -> Self {
url::Url::parse(&format!("http://{}", args.host)).expect("Failed to parse node host");
let stacks_client = StacksClient::try_from_host(
- StacksPrivateKey::new(), // We don't need a private key to read
+ StacksPrivateKey::random(), // We don't need a private key to read
args.host.clone(),
"FOO".to_string(), // We don't care about authorized paths. Just accessing public info
)
diff --git a/stacks-signer/src/monitoring/mod.rs b/stacks-signer/src/monitoring/mod.rs
index 400541d0e7..60a530acab 100644
--- a/stacks-signer/src/monitoring/mod.rs
+++ b/stacks-signer/src/monitoring/mod.rs
@@ -14,139 +14,176 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-#[cfg(feature = "monitoring_prom")]
-use ::prometheus::HistogramTimer;
-#[cfg(feature = "monitoring_prom")]
-use slog::slog_error;
-#[cfg(not(feature = "monitoring_prom"))]
-use slog::slog_info;
-#[cfg(feature = "monitoring_prom")]
-use stacks_common::error;
-#[cfg(not(feature = "monitoring_prom"))]
-use stacks_common::info;
-
-use crate::config::GlobalConfig;
-
#[cfg(feature = "monitoring_prom")]
mod prometheus;
#[cfg(feature = "monitoring_prom")]
mod server;
-/// Update stacks tip height gauge
-#[allow(unused_variables)]
-pub fn update_stacks_tip_height(height: i64) {
- #[cfg(feature = "monitoring_prom")]
- prometheus::STACKS_TIP_HEIGHT_GAUGE.set(height);
-}
+/// Actions for updating metrics
+#[cfg(feature = "monitoring_prom")]
+pub mod actions {
+ use ::prometheus::HistogramTimer;
+ use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
+ use slog::slog_error;
+ use stacks_common::error;
+
+ use crate::config::GlobalConfig;
+ use crate::monitoring::prometheus::*;
+
+ /// Update stacks tip height gauge
+ pub fn update_stacks_tip_height(height: i64) {
+ STACKS_TIP_HEIGHT_GAUGE.set(height);
+ }
-/// Update the current reward cycle
-#[allow(unused_variables)]
-pub fn update_reward_cycle(reward_cycle: i64) {
- #[cfg(feature = "monitoring_prom")]
- prometheus::CURRENT_REWARD_CYCLE.set(reward_cycle);
-}
+ /// Update the current reward cycle
+ pub fn update_reward_cycle(reward_cycle: i64) {
+ CURRENT_REWARD_CYCLE.set(reward_cycle);
+ }
-/// Increment the block validation responses counter
-#[allow(unused_variables)]
-pub fn increment_block_validation_responses(accepted: bool) {
- #[cfg(feature = "monitoring_prom")]
- {
+ /// Increment the block validation responses counter
+ pub fn increment_block_validation_responses(accepted: bool) {
let label_value = if accepted { "accepted" } else { "rejected" };
- prometheus::BLOCK_VALIDATION_RESPONSES
+ BLOCK_VALIDATION_RESPONSES
.with_label_values(&[label_value])
.inc();
}
-}
-/// Increment the block responses sent counter
-#[allow(unused_variables)]
-pub fn increment_block_responses_sent(accepted: bool) {
- #[cfg(feature = "monitoring_prom")]
- {
+ /// Increment the block responses sent counter
+ pub fn increment_block_responses_sent(accepted: bool) {
let label_value = if accepted { "accepted" } else { "rejected" };
- prometheus::BLOCK_RESPONSES_SENT
- .with_label_values(&[label_value])
- .inc();
+ BLOCK_RESPONSES_SENT.with_label_values(&[label_value]).inc();
}
-}
-/// Increment the number of block proposals received
-#[allow(unused_variables)]
-pub fn increment_block_proposals_received() {
- #[cfg(feature = "monitoring_prom")]
- prometheus::BLOCK_PROPOSALS_RECEIVED.inc();
-}
-
-/// Update the stx balance of the signer
-#[allow(unused_variables)]
-pub fn update_signer_stx_balance(balance: i64) {
- #[cfg(feature = "monitoring_prom")]
- prometheus::SIGNER_STX_BALANCE.set(balance);
-}
+ /// Increment the number of block proposals received
+ pub fn increment_block_proposals_received() {
+ BLOCK_PROPOSALS_RECEIVED.inc();
+ }
-/// Update the signer nonce metric
-#[allow(unused_variables)]
-pub fn update_signer_nonce(nonce: u64) {
- #[cfg(feature = "monitoring_prom")]
- prometheus::SIGNER_NONCE.set(nonce as i64);
-}
+ /// Update the stx balance of the signer
+ pub fn update_signer_stx_balance(balance: i64) {
+ SIGNER_STX_BALANCE.set(balance);
+ }
-// Allow dead code because this is only used in the `monitoring_prom` feature
-// but we want to run it in a test
-#[allow(dead_code)]
-/// Remove the origin from the full path to avoid duplicate metrics for different origins
-fn remove_origin_from_path(full_path: &str, origin: &str) -> String {
- full_path.replace(origin, "")
-}
+ /// Update the signer nonce metric
+ pub fn update_signer_nonce(nonce: u64) {
+ SIGNER_NONCE.set(nonce as i64);
+ }
-/// Start a new RPC call timer.
-/// The `origin` parameter is the base path of the RPC call, e.g. `http://node.com`.
-/// The `origin` parameter is removed from `full_path` when storing in prometheus.
-#[cfg(feature = "monitoring_prom")]
-pub fn new_rpc_call_timer(full_path: &str, origin: &str) -> HistogramTimer {
- let path = remove_origin_from_path(full_path, origin);
- let histogram = prometheus::SIGNER_RPC_CALL_LATENCIES_HISTOGRAM.with_label_values(&[&path]);
- histogram.start_timer()
-}
+ /// Start a new RPC call timer.
+ /// The `origin` parameter is the base path of the RPC call, e.g. `http://node.com`.
+ /// The `origin` parameter is removed from `full_path` when storing in prometheus.
+ pub fn new_rpc_call_timer(full_path: &str, origin: &str) -> HistogramTimer {
+ let path = super::remove_origin_from_path(full_path, origin);
+ let histogram = SIGNER_RPC_CALL_LATENCIES_HISTOGRAM.with_label_values(&[&path]);
+ histogram.start_timer()
+ }
-/// NoOp timer uses for monitoring when the monitoring feature is not enabled.
-pub struct NoOpTimer;
-impl NoOpTimer {
- /// NoOp method to stop recording when the monitoring feature is not enabled.
- pub fn stop_and_record(&self) {}
-}
+ /// Record the time taken to issue a block response for
+ /// a given block. The block's timestamp is used to calculate the latency.
+ ///
+ /// Call this right after broadcasting a BlockResponse
+ pub fn record_block_response_latency(block: &NakamotoBlock) {
+ use clarity::util::get_epoch_time_ms;
+
+ let diff =
+ get_epoch_time_ms().saturating_sub(block.header.timestamp.saturating_mul(1000).into());
+ SIGNER_BLOCK_RESPONSE_LATENCIES_HISTOGRAM
+ .with_label_values(&[])
+ .observe(diff as f64 / 1000.0);
+ }
-/// Stop and record the no-op timer.
-#[cfg(not(feature = "monitoring_prom"))]
-pub fn new_rpc_call_timer(_full_path: &str, _origin: &str) -> NoOpTimer {
- NoOpTimer
-}
+ /// Record the time taken to validate a block, as reported by the Stacks node.
+ pub fn record_block_validation_latency(latency_ms: u64) {
+ SIGNER_BLOCK_VALIDATION_LATENCIES_HISTOGRAM
+ .with_label_values(&[])
+ .observe(latency_ms as f64 / 1000.0);
+ }
-/// Start serving monitoring metrics.
-/// This will only serve the metrics if the `monitoring_prom` feature is enabled.
-#[allow(unused_variables)]
-pub fn start_serving_monitoring_metrics(config: GlobalConfig) -> Result<(), String> {
- #[cfg(feature = "monitoring_prom")]
- {
+ /// Start serving monitoring metrics.
+ /// This will only serve the metrics if the `monitoring_prom` feature is enabled.
+ pub fn start_serving_monitoring_metrics(config: GlobalConfig) -> Result<(), String> {
if config.metrics_endpoint.is_none() {
return Ok(());
}
- let thread = std::thread::Builder::new()
+ let _ = std::thread::Builder::new()
.name("signer_metrics".to_string())
.spawn(move || {
- if let Err(monitoring_err) = server::MonitoringServer::start(&config) {
+ if let Err(monitoring_err) = super::server::MonitoringServer::start(&config) {
error!("Monitoring: Error in metrics server: {:?}", monitoring_err);
}
});
+ Ok(())
+ }
+}
+
+/// No-op actions for updating metrics
+#[cfg(not(feature = "monitoring_prom"))]
+pub mod actions {
+ use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
+ use slog::slog_info;
+ use stacks_common::info;
+
+ use crate::GlobalConfig;
+
+ /// Update stacks tip height gauge
+ pub fn update_stacks_tip_height(_height: i64) {}
+
+ /// Update the current reward cycle
+ pub fn update_reward_cycle(_reward_cycle: i64) {}
+
+ /// Increment the block validation responses counter
+ pub fn increment_block_validation_responses(_accepted: bool) {}
+
+ /// Increment the block responses sent counter
+ pub fn increment_block_responses_sent(_accepted: bool) {}
+
+ /// Increment the number of block proposals received
+ pub fn increment_block_proposals_received() {}
+
+ /// Update the stx balance of the signer
+ pub fn update_signer_stx_balance(_balance: i64) {}
+
+ /// Update the signer nonce metric
+ pub fn update_signer_nonce(_nonce: u64) {}
+
+ /// NoOp timer uses for monitoring when the monitoring feature is not enabled.
+ pub struct NoOpTimer;
+ impl NoOpTimer {
+ /// NoOp method to stop recording when the monitoring feature is not enabled.
+ pub fn stop_and_record(&self) {}
+ }
+
+ /// Stop and record the no-op timer.
+ pub fn new_rpc_call_timer(_full_path: &str, _origin: &str) -> NoOpTimer {
+ NoOpTimer
}
- #[cfg(not(feature = "monitoring_prom"))]
- {
+
+ /// Record the time taken to issue a block response for
+ /// a given block. The block's timestamp is used to calculate the latency.
+ ///
+ /// Call this right after broadcasting a BlockResponse
+ pub fn record_block_response_latency(_block: &NakamotoBlock) {}
+
+ /// Record the time taken to validate a block, as reported by the Stacks node.
+ pub fn record_block_validation_latency(_latency_ms: u64) {}
+
+ /// Start serving monitoring metrics.
+ /// This will only serve the metrics if the `monitoring_prom` feature is enabled.
+ pub fn start_serving_monitoring_metrics(config: GlobalConfig) -> Result<(), String> {
if config.metrics_endpoint.is_some() {
info!("`metrics_endpoint` is configured for the signer, but the monitoring_prom feature is not enabled. Not starting monitoring metrics server.");
}
+ Ok(())
}
- Ok(())
+}
+
+// Allow dead code because this is only used in the `monitoring_prom` feature
+// but we want to run it in a test
+#[allow(dead_code)]
+/// Remove the origin from the full path to avoid duplicate metrics for different origins
+fn remove_origin_from_path(full_path: &str, origin: &str) -> String {
+ full_path.replace(origin, "")
}
#[test]
diff --git a/stacks-signer/src/monitoring/prometheus.rs b/stacks-signer/src/monitoring/prometheus.rs
index 247a9f00f5..49f74ba1e8 100644
--- a/stacks-signer/src/monitoring/prometheus.rs
+++ b/stacks-signer/src/monitoring/prometheus.rs
@@ -62,6 +62,18 @@ lazy_static! {
"Time (seconds) measuring round-trip RPC call latency to the Stacks node"
// Will use DEFAULT_BUCKETS = [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0] by default
), &["path"]).unwrap();
+
+ pub static ref SIGNER_BLOCK_VALIDATION_LATENCIES_HISTOGRAM: HistogramVec = register_histogram_vec!(histogram_opts!(
+ "stacks_signer_block_validation_latencies_histogram",
+ "Time (seconds) measuring block validation time reported by the Stacks node",
+ vec![0.005, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 20.0]
+ ), &[]).unwrap();
+
+ pub static ref SIGNER_BLOCK_RESPONSE_LATENCIES_HISTOGRAM: HistogramVec = register_histogram_vec!(histogram_opts!(
+ "stacks_signer_block_response_latencies_histogram",
+ "Time (seconds) measuring end-to-end time to respond to a block",
+ vec![0.005, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 20.0, 30.0, 60.0, 120.0]
+ ), &[]).unwrap();
}
pub fn gather_metrics_string() -> String {
diff --git a/stacks-signer/src/monitoring/server.rs b/stacks-signer/src/monitoring/server.rs
index 15267c44ee..0e584eec58 100644
--- a/stacks-signer/src/monitoring/server.rs
+++ b/stacks-signer/src/monitoring/server.rs
@@ -24,11 +24,11 @@ use slog::{slog_debug, slog_error, slog_info, slog_warn};
use stacks_common::{debug, error, info, warn};
use tiny_http::{Response as HttpResponse, Server as HttpServer};
-use super::{update_reward_cycle, update_signer_stx_balance};
+use super::actions::{update_reward_cycle, update_signer_stx_balance};
use crate::client::{ClientError, StacksClient};
use crate::config::{GlobalConfig, Network};
+use crate::monitoring::actions::{update_signer_nonce, update_stacks_tip_height};
use crate::monitoring::prometheus::gather_metrics_string;
-use crate::monitoring::{update_signer_nonce, update_stacks_tip_height};
#[derive(thiserror::Error, Debug)]
/// Monitoring server errors
diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs
index 69dc2dd843..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) => {
@@ -544,7 +568,8 @@ mod tests {
let weight = 10;
let mut signer_entries = Vec::with_capacity(nmb_signers);
for _ in 0..nmb_signers {
- let key = StacksPublicKey::from_private(&StacksPrivateKey::new()).to_bytes_compressed();
+ let key =
+ StacksPublicKey::from_private(&StacksPrivateKey::random()).to_bytes_compressed();
let mut signing_key = [0u8; 33];
signing_key.copy_from_slice(&key);
signer_entries.push(NakamotoSignerEntry {
diff --git a/stacks-signer/src/signerdb.rs b/stacks-signer/src/signerdb.rs
index 67321c7218..79325d1d13 100644
--- a/stacks-signer/src/signerdb.rs
+++ b/stacks-signer/src/signerdb.rs
@@ -24,6 +24,8 @@ use blockstack_lib::util_lib::db::{
query_row, query_rows, sqlite_open, table_exists, tx_begin_immediate, u64_to_sql,
Error as DBError,
};
+#[cfg(any(test, feature = "testing"))]
+use blockstack_lib::util_lib::db::{FromColumn, FromRow};
use clarity::types::chainstate::{BurnchainHeaderHash, StacksAddress};
use libsigner::BlockProposal;
use rusqlite::functions::FunctionFlags;
@@ -209,7 +211,7 @@ impl BlockInfo {
/// Mark this block as valid, signed over, and records a group timestamp in the block info if it wasn't
/// already set.
- pub fn mark_globally_accepted(&mut self) -> Result<(), String> {
+ fn mark_globally_accepted(&mut self) -> Result<(), String> {
self.move_to(BlockState::GloballyAccepted)?;
self.valid = Some(true);
self.signed_over = true;
@@ -225,7 +227,7 @@ impl BlockInfo {
}
/// Mark the block as globally rejected and invalid
- pub fn mark_globally_rejected(&mut self) -> Result<(), String> {
+ fn mark_globally_rejected(&mut self) -> Result<(), String> {
self.move_to(BlockState::GloballyRejected)?;
self.valid = Some(false);
Ok(())
@@ -342,6 +344,10 @@ CREATE INDEX IF NOT EXISTS blocks_state ON blocks (state);
CREATE INDEX IF NOT EXISTS blocks_signed_group ON blocks (signed_group);
"#;
+static CREATE_INDEXES_6: &str = r#"
+CREATE INDEX IF NOT EXISTS block_validations_pending_on_added_time ON block_validations_pending(added_time ASC);
+"#;
+
static CREATE_SIGNER_STATE_TABLE: &str = "
CREATE TABLE IF NOT EXISTS signer_states (
reward_cycle INTEGER PRIMARY KEY,
@@ -436,15 +442,15 @@ INSERT INTO temp_blocks (
broadcasted,
stacks_height,
burn_block_height,
- valid,
+ valid,
state,
- signed_group,
+ signed_group,
signed_self,
proposed_time,
validation_time_ms,
tenure_change
)
-SELECT
+SELECT
signer_signature_hash,
reward_cycle,
block_info,
@@ -452,7 +458,7 @@ SELECT
signed_over,
broadcasted,
stacks_height,
- burn_block_height,
+ burn_block_height,
json_extract(block_info, '$.valid') AS valid,
json_extract(block_info, '$.state') AS state,
json_extract(block_info, '$.signed_group') AS signed_group,
@@ -466,6 +472,14 @@ DROP TABLE blocks;
ALTER TABLE temp_blocks RENAME TO blocks;"#;
+static CREATE_BLOCK_VALIDATION_PENDING_TABLE: &str = r#"
+CREATE TABLE IF NOT EXISTS block_validations_pending (
+ signer_signature_hash TEXT NOT NULL,
+ -- the time at which the block was added to the pending table
+ added_time INTEGER NOT NULL,
+ PRIMARY KEY (signer_signature_hash)
+) STRICT;"#;
+
static SCHEMA_1: &[&str] = &[
DROP_SCHEMA_0,
CREATE_DB_CONFIG,
@@ -514,9 +528,15 @@ static SCHEMA_5: &[&str] = &[
"INSERT INTO db_config (version) VALUES (5);",
];
+static SCHEMA_6: &[&str] = &[
+ CREATE_BLOCK_VALIDATION_PENDING_TABLE,
+ CREATE_INDEXES_6,
+ "INSERT OR REPLACE INTO db_config (version) VALUES (6);",
+];
+
impl SignerDb {
/// The current schema version used in this build of the signer binary.
- pub const SCHEMA_VERSION: u32 = 5;
+ pub const SCHEMA_VERSION: u32 = 6;
/// Create a new `SignerState` instance.
/// This will create a new SQLite database at the given path
@@ -616,6 +636,20 @@ impl SignerDb {
Ok(())
}
+ /// Migrate from schema 5 to schema 6
+ fn schema_6_migration(tx: &Transaction) -> Result<(), DBError> {
+ if Self::get_schema_version(tx)? >= 6 {
+ // no migration necessary
+ return Ok(());
+ }
+
+ for statement in SCHEMA_6.iter() {
+ tx.execute_batch(statement)?;
+ }
+
+ Ok(())
+ }
+
/// Register custom scalar functions used by the database
fn register_scalar_functions(&self) -> Result<(), DBError> {
// Register helper function for determining if a block is a tenure change transaction
@@ -654,7 +688,8 @@ impl SignerDb {
2 => Self::schema_3_migration(&sql_tx)?,
3 => Self::schema_4_migration(&sql_tx)?,
4 => Self::schema_5_migration(&sql_tx)?,
- 5 => break,
+ 5 => Self::schema_6_migration(&sql_tx)?,
+ 6 => break,
x => return Err(DBError::Other(format!(
"Database schema is newer than supported by this binary. Expected version = {}, Database version = {x}",
Self::SCHEMA_VERSION,
@@ -711,15 +746,13 @@ impl SignerDb {
try_deserialize(result)
}
- /// Return the last signed block in a tenure (identified by its consensus hash)
- pub fn get_last_signed_block_in_tenure(
- &self,
- tenure: &ConsensusHash,
- ) -> Result