From e667b82476cccfe88ee98f1ad720536bc03719f9 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Wed, 22 Jan 2025 16:36:07 -0500 Subject: [PATCH 01/27] fix: only shut off the epoch2x state machines once the Stacks tip is a Nakamoto tip --- stackslib/src/net/p2p.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs index 78c8982106..57908ca6af 100644 --- a/stackslib/src/net/p2p.rs +++ b/stackslib/src/net/p2p.rs @@ -3606,12 +3606,7 @@ impl PeerNetwork { // in Nakamoto epoch, but we might still be doing epoch 2.x things since Nakamoto does // not begin on a reward cycle boundary. - if cur_epoch.epoch_id == StacksEpochId::Epoch30 - && (self.burnchain_tip.block_height - <= cur_epoch.start_height - + u64::from(self.burnchain.pox_constants.reward_cycle_length) - || self.connection_opts.force_nakamoto_epoch_transition) - { + if cur_epoch.epoch_id >= StacksEpochId::Epoch30 && !self.stacks_tip.is_nakamoto { debug!( "{:?}: run Epoch 2.x work loop in Nakamoto epoch", self.get_local_peer() From f035175149f74ebd071823e0d2a1e1b318e4cf02 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Thu, 23 Jan 2025 17:16:13 -0500 Subject: [PATCH 02/27] chore: make the epoch2x state machine check testable, and extend TestPeer to check it --- stackslib/src/net/mod.rs | 34 ++++++++++++++++++++++++++++++++++ stackslib/src/net/p2p.rs | 23 ++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index cfefa2c5fe..ecbca3301f 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -3314,6 +3314,10 @@ pub mod test { let old_tip = self.network.stacks_tip.clone(); + // make sure the right state machines run + let epoch2_passes = self.network.epoch2_state_machine_passes; + let nakamoto_passes = self.network.nakamoto_state_machine_passes; + let ret = self.network.run( &indexer, &sortdb, @@ -3326,6 +3330,19 @@ pub mod test { &RPCHandlerArgs::default(), ); + if self.network.get_current_epoch().epoch_id >= StacksEpochId::Epoch30 { + assert_eq!( + self.network.nakamoto_state_machine_passes, + nakamoto_passes + 1 + ); + } + if self + .network + .need_epoch2_state_machines(self.network.get_current_epoch().epoch_id) + { + assert_eq!(self.network.epoch2_state_machine_passes, epoch2_passes + 1); + } + self.sortdb = Some(sortdb); self.stacks_node = Some(stacks_node); self.mempool = Some(mempool); @@ -3394,6 +3411,10 @@ pub mod test { let old_tip = self.network.stacks_tip.clone(); + // make sure the right state machines run + let epoch2_passes = self.network.epoch2_state_machine_passes; + let nakamoto_passes = self.network.nakamoto_state_machine_passes; + let ret = self.network.run( &indexer, &sortdb, @@ -3406,6 +3427,19 @@ pub mod test { &RPCHandlerArgs::default(), ); + if self.network.get_current_epoch().epoch_id >= StacksEpochId::Epoch30 { + assert_eq!( + self.network.nakamoto_state_machine_passes, + nakamoto_passes + 1 + ); + } + if self + .network + .need_epoch2_state_machines(self.network.get_current_epoch().epoch_id) + { + assert_eq!(self.network.epoch2_state_machine_passes, epoch2_passes + 1); + } + self.sortdb = Some(sortdb); self.stacks_node = Some(stacks_node); self.mempool = Some(mempool); diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs index 57908ca6af..c5b1afa33e 100644 --- a/stackslib/src/net/p2p.rs +++ b/stackslib/src/net/p2p.rs @@ -423,6 +423,12 @@ pub struct PeerNetwork { // how many downloader passes have we done? pub num_downloader_passes: u64, + // number of epoch2 state machine passes + pub(crate) epoch2_state_machine_passes: u128, + + // number of nakamoto state machine passes + pub(crate) nakamoto_state_machine_passes: u128, + // to whom did we send a block or microblock stream as part of our anti-entropy protocol, and // when did we send it? antientropy_blocks: HashMap>, @@ -593,6 +599,8 @@ impl PeerNetwork { num_state_machine_passes: 0, num_inv_sync_passes: 0, num_downloader_passes: 0, + epoch2_state_machine_passes: 0, + nakamoto_state_machine_passes: 0, antientropy_blocks: HashMap::new(), antientropy_microblocks: HashMap::new(), @@ -3578,6 +3586,15 @@ impl PeerNetwork { } } + /// Check to see if we need to run the epoch 2.x state machines. + /// This will be true if we're either in epoch 2.5 or lower, OR, if we're in epoch 3.0 or + /// higher AND the Stacks tip is not yet a Nakamoto block. This latter condition indicates + /// that the epoch 2.x state machines are still needed to download the final epoch 2.x blocks. + pub(crate) fn need_epoch2_state_machines(&self, epoch_id: StacksEpochId) -> bool { + epoch_id < StacksEpochId::Epoch30 + || (epoch_id >= StacksEpochId::Epoch30 && !self.stacks_tip.is_nakamoto) + } + /// Do the actual work in the state machine. /// Return true if we need to prune connections. /// This will call the epoch-appropriate network worker @@ -3606,7 +3623,7 @@ impl PeerNetwork { // in Nakamoto epoch, but we might still be doing epoch 2.x things since Nakamoto does // not begin on a reward cycle boundary. - if cur_epoch.epoch_id >= StacksEpochId::Epoch30 && !self.stacks_tip.is_nakamoto { + if self.need_epoch2_state_machines(cur_epoch.epoch_id) { debug!( "{:?}: run Epoch 2.x work loop in Nakamoto epoch", self.get_local_peer() @@ -3655,6 +3672,8 @@ impl PeerNetwork { ibd: bool, network_result: &mut NetworkResult, ) { + self.nakamoto_state_machine_passes += 1; + // always do an inv sync let learned = self.do_network_inv_sync_nakamoto(sortdb, ibd); debug!( @@ -3704,6 +3723,8 @@ impl PeerNetwork { ibd: bool, network_result: &mut NetworkResult, ) -> bool { + self.epoch2_state_machine_passes += 1; + // do some Actual Work(tm) let mut do_prune = false; let mut did_cycle = false; From 56e0581ed770ab666366c6968a759180126a37ec Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Fri, 24 Jan 2025 00:47:14 -0500 Subject: [PATCH 03/27] chore: address PR feedback --- stackslib/src/net/mod.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index 2c37b5285a..27054ed329 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -3334,6 +3334,15 @@ pub mod test { self.network.nakamoto_state_machine_passes, nakamoto_passes + 1 ); + let epoch2_expected_passes = if self.network.stacks_tip.is_nakamoto { + epoch2_passes + } else { + epoch2_passes + 1 + }; + assert_eq!( + self.network.epoch2_state_machine_passes, + epoch2_expected_passes + ); } if self .network @@ -3431,6 +3440,15 @@ pub mod test { self.network.nakamoto_state_machine_passes, nakamoto_passes + 1 ); + let epoch2_expected_passes = if self.network.stacks_tip.is_nakamoto { + epoch2_passes + } else { + epoch2_passes + 1 + }; + assert_eq!( + self.network.epoch2_state_machine_passes, + epoch2_expected_passes + ); } if self .network From 953388b10f5214a2c67617e71970d523880dde37 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 27 Jan 2025 11:43:48 -0800 Subject: [PATCH 04/27] Update signerdb to have tenure_activity table and use in is_timed_out Signed-off-by: Jacinta Ferrant --- stacks-signer/src/chainstate.rs | 18 +++--- stacks-signer/src/signerdb.rs | 106 ++++++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 19 deletions(-) diff --git a/stacks-signer/src/chainstate.rs b/stacks-signer/src/chainstate.rs index 3e59e58850..5e899d54fe 100644 --- a/stacks-signer/src/chainstate.rs +++ b/stacks-signer/src/chainstate.rs @@ -89,9 +89,9 @@ impl SortitionState { if self.miner_status != SortitionMinerStatus::Valid { return Ok(false); } - // 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 { + // if we've already signed a block in this tenure, the miner can't have timed out. + let has_block = signer_db.has_signed_block_in_tenure(&self.consensus_hash)?; + if has_block { return Ok(false); } let Some(received_ts) = signer_db.get_burn_block_receive_time(&self.burn_block_hash)? @@ -99,13 +99,15 @@ impl SortitionState { return Ok(false); }; let received_time = UNIX_EPOCH + Duration::from_secs(received_ts); - let Ok(elapsed) = std::time::SystemTime::now().duration_since(received_time) else { + let last_activity = signer_db + .get_last_activity_time(&self.consensus_hash)? + .map(|time| UNIX_EPOCH + Duration::from_secs(time)) + .unwrap_or(received_time); + + let Ok(elapsed) = std::time::SystemTime::now().duration_since(last_activity) else { return Ok(false); }; - if elapsed > timeout { - return Ok(true); - } - Ok(false) + Ok(elapsed > timeout) } } diff --git a/stacks-signer/src/signerdb.rs b/stacks-signer/src/signerdb.rs index a2b7c7fe37..b7f43dcf7a 100644 --- a/stacks-signer/src/signerdb.rs +++ b/stacks-signer/src/signerdb.rs @@ -480,6 +480,12 @@ CREATE TABLE IF NOT EXISTS block_validations_pending ( PRIMARY KEY (signer_signature_hash) ) STRICT;"#; +static CREATE_TENURE_ACTIVTY_TABLE: &str = r#" +CREATE TABLE IF NOT EXISTS tenure_activity ( + consensus_hash TEXT NOT NULL PRIMARY KEY, + last_activity_time INTEGER NOT NULL +) STRICT;"#; + static SCHEMA_1: &[&str] = &[ DROP_SCHEMA_0, CREATE_DB_CONFIG, @@ -534,9 +540,14 @@ static SCHEMA_6: &[&str] = &[ "INSERT OR REPLACE INTO db_config (version) VALUES (6);", ]; +static SCHEMA_7: &[&str] = &[ + CREATE_TENURE_ACTIVTY_TABLE, + "INSERT OR REPLACE INTO db_config (version) VALUES (7);", +]; + impl SignerDb { /// The current schema version used in this build of the signer binary. - pub const SCHEMA_VERSION: u32 = 6; + pub const SCHEMA_VERSION: u32 = 7; /// Create a new `SignerState` instance. /// This will create a new SQLite database at the given path @@ -650,6 +661,20 @@ impl SignerDb { Ok(()) } + /// Migrate from schema 6 to schema 7 + fn schema_7_migration(tx: &Transaction) -> Result<(), DBError> { + if Self::get_schema_version(tx)? >= 7 { + // no migration necessary + return Ok(()); + } + + for statement in SCHEMA_7.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 @@ -689,7 +714,8 @@ impl SignerDb { 3 => Self::schema_4_migration(&sql_tx)?, 4 => Self::schema_5_migration(&sql_tx)?, 5 => Self::schema_6_migration(&sql_tx)?, - 6 => break, + 6 => Self::schema_7_migration(&sql_tx)?, + 7 => break, x => return Err(DBError::Other(format!( "Database schema is newer than supported by this binary. Expected version = {}, Database version = {x}", Self::SCHEMA_VERSION, @@ -746,10 +772,9 @@ impl SignerDb { try_deserialize(result) } - /// Return whether a block proposal has been stored for a tenure (identified by its consensus hash) - /// Does not consider the block's state. - pub fn has_proposed_block_in_tenure(&self, tenure: &ConsensusHash) -> Result { - let query = "SELECT block_info FROM blocks WHERE consensus_hash = ? LIMIT 1"; + /// Return whether there was signed block in a tenure (identified by its consensus hash) + pub fn has_signed_block_in_tenure(&self, tenure: &ConsensusHash) -> Result { + let query = "SELECT block_info FROM blocks WHERE consensus_hash = ? AND signed_over = 1 ORDER BY stacks_height DESC LIMIT 1"; let result: Option = query_row(&self.db, query, [tenure])?; Ok(result.is_some()) @@ -1112,6 +1137,30 @@ impl SignerDb { self.remove_pending_block_validation(&block_info.signer_signature_hash())?; Ok(()) } + /// Update the tenure (identified by consensus_hash) last activity timestamp + pub fn update_last_activity_time( + &mut self, + tenure: &ConsensusHash, + last_activity_time: u64, + ) -> Result<(), DBError> { + debug!("Updating last activity for tenure"; "consensus_hash" => %tenure, "last_activity_time" => last_activity_time); + self.db.execute("INSERT OR REPLACE INTO tenure_activity (consensus_hash, last_activity_time) VALUES (?1, ?2)", params![tenure, u64_to_sql(last_activity_time)?])?; + Ok(()) + } + + /// Get the last activity timestamp for a tenure (identified by consensus_hash) + pub fn get_last_activity_time(&self, tenure: &ConsensusHash) -> Result, DBError> { + let query = + "SELECT last_activity_time FROM tenure_activity WHERE consensus_hash = ? LIMIT 1"; + let Some(last_activity_time_i64) = query_row::(&self.db, query, &[tenure])? else { + return Ok(None); + }; + let last_activity_time = u64::try_from(last_activity_time_i64).map_err(|e| { + error!("Failed to parse db last_activity_time as u64: {e}"); + DBError::Corruption + })?; + Ok(Some(last_activity_time)) + } } fn try_deserialize(s: Option) -> Result, DBError> @@ -1903,7 +1952,7 @@ mod tests { } #[test] - fn has_proposed_block() { + fn has_signed_block() { let db_path = tmp_db_path(); let consensus_hash_1 = ConsensusHash([0x01; 20]); let consensus_hash_2 = ConsensusHash([0x02; 20]); @@ -1913,16 +1962,51 @@ mod tests { b.block.header.chain_length = 1; }); - assert!(!db.has_proposed_block_in_tenure(&consensus_hash_1).unwrap()); - assert!(!db.has_proposed_block_in_tenure(&consensus_hash_2).unwrap()); + assert!(!db.has_signed_block_in_tenure(&consensus_hash_1).unwrap()); + assert!(!db.has_signed_block_in_tenure(&consensus_hash_2).unwrap()); + block_info.signed_over = true; db.insert_block(&block_info).unwrap(); + assert!(db.has_signed_block_in_tenure(&consensus_hash_1).unwrap()); + assert!(!db.has_signed_block_in_tenure(&consensus_hash_2).unwrap()); + block_info.block.header.chain_length = 2; + block_info.signed_over = false; db.insert_block(&block_info).unwrap(); - assert!(db.has_proposed_block_in_tenure(&consensus_hash_1).unwrap()); - assert!(!db.has_proposed_block_in_tenure(&consensus_hash_2).unwrap()); + assert!(db.has_signed_block_in_tenure(&consensus_hash_1).unwrap()); + assert!(!db.has_signed_block_in_tenure(&consensus_hash_2).unwrap()); + } + + #[test] + fn update_last_activity() { + let db_path = tmp_db_path(); + let consensus_hash_1 = ConsensusHash([0x01; 20]); + let consensus_hash_2 = ConsensusHash([0x02; 20]); + let mut db = SignerDb::new(db_path).expect("Failed to create signer db"); + + assert!(db + .get_last_activity_time(&consensus_hash_1) + .unwrap() + .is_none()); + assert!(db + .get_last_activity_time(&consensus_hash_2) + .unwrap() + .is_none()); + + let time = get_epoch_time_secs(); + db.update_last_activity_time(&consensus_hash_1, time) + .unwrap(); + let retrieved_time = db + .get_last_activity_time(&consensus_hash_1) + .unwrap() + .unwrap(); + assert_eq!(time, retrieved_time); + assert!(db + .get_last_activity_time(&consensus_hash_2) + .unwrap() + .is_none()); } } From e668895a757611497fc0bd8e3df21785c51d8f5c Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Mon, 27 Jan 2025 14:39:23 -0800 Subject: [PATCH 05/27] Add reorg_attempts_activity_timeout_exceeded test and add config option reorg_attempts_activity_timeout_ms Signed-off-by: Jacinta Ferrant --- .github/workflows/bitcoin-tests.yml | 3 +- stacks-signer/src/chainstate.rs | 23 ++ stacks-signer/src/client/mod.rs | 1 + stacks-signer/src/config.rs | 17 ++ stacks-signer/src/runloop.rs | 1 + stacks-signer/src/tests/chainstate.rs | 1 + stacks-signer/src/v0/signer.rs | 1 + .../src/tests/nakamoto_integrations.rs | 3 + testnet/stacks-node/src/tests/signer/v0.rs | 202 +++++++++++++++++- 9 files changed, 248 insertions(+), 4 deletions(-) diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index 363e02044f..5c7fcb9bde 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -146,7 +146,8 @@ jobs: - tests::signer::v0::single_miner_empty_sortition - tests::signer::v0::multiple_miners_empty_sortition - tests::signer::v0::block_proposal_timeout - - tests::signer::v0::rejected_blocks_count_towards_miner_validity + - tests::signer::v0::reorg_attempts_count_towards_miner_validity + - tests::signer::v0::late_reorg_attempts_do_not_count_towards_miner_validity - tests::signer::v0::allow_reorg_within_first_proposal_burn_block_timing_secs - tests::nakamoto_integrations::burn_ops_integration_test - tests::nakamoto_integrations::check_block_heights diff --git a/stacks-signer/src/chainstate.rs b/stacks-signer/src/chainstate.rs index 5e899d54fe..5eeb6c50ad 100644 --- a/stacks-signer/src/chainstate.rs +++ b/stacks-signer/src/chainstate.rs @@ -124,6 +124,9 @@ pub struct ProposalEvalConfig { pub tenure_last_block_proposal_timeout: Duration, /// How much idle time must pass before allowing a tenure extend pub tenure_idle_timeout: Duration, + /// Time following a block's global acceptance that a signer will consider an attempt by a miner to reorg the block + /// as valid towards miner activity + pub reorg_attempts_activity_timeout: Duration, } impl From<&SignerConfig> for ProposalEvalConfig { @@ -133,6 +136,7 @@ impl From<&SignerConfig> for ProposalEvalConfig { block_proposal_timeout: value.block_proposal_timeout, tenure_last_block_proposal_timeout: value.tenure_last_block_proposal_timeout, tenure_idle_timeout: value.tenure_idle_timeout, + reorg_attempts_activity_timeout: value.reorg_attempts_activity_timeout, } } } @@ -547,8 +551,10 @@ impl SortitionsView { signer_db: &mut SignerDb, client: &StacksClient, tenure_last_block_proposal_timeout: Duration, + reorg_attempts_activity_timeout: Duration, ) -> Result { // If the tenure change block confirms the expected parent block, it should confirm at least one more block than the last accepted block in the parent tenure. + // NOTE: returns the locally accepted block if it is not timed out, otherwise it will return the last globally accepted block. let last_block_info = Self::get_tenure_last_block_info( &tenure_change.prev_tenure_consensus_hash, signer_db, @@ -568,6 +574,22 @@ impl SortitionsView { "proposed_chain_length" => block.header.chain_length, "expected_at_least" => info.block.header.chain_length + 1, ); + if info.signed_group.unwrap_or(get_epoch_time_secs()) + + reorg_attempts_activity_timeout.as_secs() + > get_epoch_time_secs() + { + // Note if there is no signed_group time, this is a locally accepted block (i.e. tenure_last_block_proposal_timeout has not been exceeded). + // Treat any attempt to reorg a locally accepted block as valid miner activity. + // If the call returns a globally accepted block, check its globally accepted time against a quarter of the block_proposal_timeout + // to give the miner some extra buffer time to wait for its chain tip to advance + // The miner may just be slow, so count this invalid block proposal towards valid miner activity. + if let Err(e) = signer_db.update_last_activity_time( + &tenure_change.tenure_consensus_hash, + get_epoch_time_secs(), + ) { + warn!("Failed to update last activity time: {e}"); + } + } return Ok(false); } } @@ -633,6 +655,7 @@ impl SortitionsView { signer_db, client, self.config.tenure_last_block_proposal_timeout, + self.config.reorg_attempts_activity_timeout, )?; if !confirms_expected_parent { return Ok(false); diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index bdaa368567..8d6a339dec 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -415,6 +415,7 @@ pub(crate) mod tests { block_proposal_validation_timeout: config.block_proposal_validation_timeout, tenure_idle_timeout: config.tenure_idle_timeout, block_proposal_max_age_secs: config.block_proposal_max_age_secs, + reorg_attempts_activity_timeout: config.reorg_attempts_activity_timeout, } } diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index a50ca7ecf8..d91909a6d1 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 = 120; +const DEFAULT_REORG_ATTEMPTS_ACTIVITY_TIMEOUT_MS: u64 = 200_000; #[derive(thiserror::Error, Debug)] /// An error occurred parsing the provided configuration @@ -141,6 +142,9 @@ 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, + /// Time following a block's global acceptance that a signer will consider an attempt by a miner to reorg the block + /// as valid towards miner activity + pub reorg_attempts_activity_timeout: Duration, } /// The parsed configuration for the signer @@ -181,6 +185,9 @@ 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, + /// Time following a block's global acceptance that a signer will consider an attempt by a miner to reorg the block + /// as valid towards miner activity + pub reorg_attempts_activity_timeout: Duration, } /// Internal struct for loading up the config file @@ -220,6 +227,9 @@ 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, + /// Time (in millisecs) following a block's global acceptance that a signer will consider an attempt by a miner + /// to reorg the block as valid towards miner activity + pub reorg_attempts_activity_timeout_ms: Option, } impl RawConfigFile { @@ -321,6 +331,12 @@ impl TryFrom for GlobalConfig { .block_proposal_max_age_secs .unwrap_or(DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS); + let reorg_attempts_activity_timeout = Duration::from_millis( + raw_data + .reorg_attempts_activity_timeout_ms + .unwrap_or(DEFAULT_REORG_ATTEMPTS_ACTIVITY_TIMEOUT_MS), + ); + Ok(Self { node_host: raw_data.node_host, endpoint, @@ -338,6 +354,7 @@ impl TryFrom for GlobalConfig { block_proposal_validation_timeout, tenure_idle_timeout, block_proposal_max_age_secs, + reorg_attempts_activity_timeout, }) } } diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index 69dc2dd843..ca5219b574 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -291,6 +291,7 @@ impl, T: StacksMessageCodec + Clone + Send + Debug> RunLo block_proposal_validation_timeout: self.config.block_proposal_validation_timeout, tenure_idle_timeout: self.config.tenure_idle_timeout, block_proposal_max_age_secs: self.config.block_proposal_max_age_secs, + reorg_attempts_activity_timeout: self.config.reorg_attempts_activity_timeout, })) } diff --git a/stacks-signer/src/tests/chainstate.rs b/stacks-signer/src/tests/chainstate.rs index 92b7a6ed53..dffa719d11 100644 --- a/stacks-signer/src/tests/chainstate.rs +++ b/stacks-signer/src/tests/chainstate.rs @@ -91,6 +91,7 @@ fn setup_test_environment( block_proposal_timeout: Duration::from_secs(5), tenure_last_block_proposal_timeout: Duration::from_secs(30), tenure_idle_timeout: Duration::from_secs(300), + reorg_attempts_activity_timeout: Duration::from_secs(3), }, }; diff --git a/stacks-signer/src/v0/signer.rs b/stacks-signer/src/v0/signer.rs index 70253f8258..17953e4978 100644 --- a/stacks-signer/src/v0/signer.rs +++ b/stacks-signer/src/v0/signer.rs @@ -613,6 +613,7 @@ impl Signer { &mut self.signer_db, stacks_client, self.proposal_config.tenure_last_block_proposal_timeout, + self.proposal_config.reorg_attempts_activity_timeout, ) { Ok(true) => {} Ok(false) => { diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index be3a4213f6..f68fc0b574 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -6468,6 +6468,7 @@ fn signer_chainstate() { block_proposal_timeout: Duration::from_secs(100), tenure_last_block_proposal_timeout: Duration::from_secs(30), tenure_idle_timeout: Duration::from_secs(300), + reorg_attempts_activity_timeout: Duration::from_secs(30), }; let mut sortitions_view = SortitionsView::fetch_view(proposal_conf, &signer_client).unwrap(); @@ -6599,6 +6600,7 @@ fn signer_chainstate() { block_proposal_timeout: Duration::from_secs(100), tenure_last_block_proposal_timeout: Duration::from_secs(30), tenure_idle_timeout: Duration::from_secs(300), + reorg_attempts_activity_timeout: Duration::from_secs(30), }; let burn_block_height = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) .unwrap() @@ -6678,6 +6680,7 @@ fn signer_chainstate() { block_proposal_timeout: Duration::from_secs(100), tenure_last_block_proposal_timeout: Duration::from_secs(30), tenure_idle_timeout: Duration::from_secs(300), + reorg_attempts_activity_timeout: Duration::from_secs(30), }; let mut sortitions_view = SortitionsView::fetch_view(proposal_conf, &signer_client).unwrap(); assert!( diff --git a/testnet/stacks-node/src/tests/signer/v0.rs b/testnet/stacks-node/src/tests/signer/v0.rs index 6869b598d7..7057ab1fca 100644 --- a/testnet/stacks-node/src/tests/signer/v0.rs +++ b/testnet/stacks-node/src/tests/signer/v0.rs @@ -493,6 +493,7 @@ fn block_proposal_rejection() { block_proposal_timeout: Duration::from_secs(100), tenure_last_block_proposal_timeout: Duration::from_secs(30), tenure_idle_timeout: Duration::from_secs(300), + reorg_attempts_activity_timeout: Duration::from_secs(30), }; let mut block = NakamotoBlock { header: NakamotoBlockHeader::empty(), @@ -7702,6 +7703,7 @@ fn block_validation_response_timeout() { tenure_last_block_proposal_timeout: Duration::from_secs(30), block_proposal_timeout: Duration::from_secs(100), tenure_idle_timeout: Duration::from_secs(300), + reorg_attempts_activity_timeout: Duration::from_secs(30), }; let mut block = NakamotoBlock { header: NakamotoBlockHeader::empty(), @@ -7871,6 +7873,7 @@ fn block_validation_pending_table() { block_proposal_timeout: Duration::from_secs(100), tenure_last_block_proposal_timeout: Duration::from_secs(30), tenure_idle_timeout: Duration::from_secs(300), + reorg_attempts_activity_timeout: Duration::from_secs(30), }; let mut block = NakamotoBlock { header: NakamotoBlockHeader::empty(), @@ -10563,6 +10566,7 @@ fn incoming_signers_ignore_block_proposals() { block_proposal_timeout: Duration::from_secs(100), tenure_last_block_proposal_timeout: Duration::from_secs(30), tenure_idle_timeout: Duration::from_secs(300), + reorg_attempts_activity_timeout: Duration::from_secs(30), }; let mut block = NakamotoBlock { header: NakamotoBlockHeader::empty(), @@ -10738,6 +10742,7 @@ fn outgoing_signers_ignore_block_proposals() { block_proposal_timeout: Duration::from_secs(100), tenure_last_block_proposal_timeout: Duration::from_secs(30), tenure_idle_timeout: Duration::from_secs(300), + reorg_attempts_activity_timeout: Duration::from_secs(30), }; let mut block = NakamotoBlock { header: NakamotoBlockHeader::empty(), @@ -11179,7 +11184,8 @@ fn injected_signatures_are_ignored_across_boundaries() { #[test] #[ignore] -/// Test that signers count any block for a given tenure in its database towards a miner tenure activity. +/// Test that signers count a block proposal that was rejected due to a reorg towards miner activity since it showed up BEFORE +/// the reorg_attempts_activity_timeout /// /// Test Setup: /// The test spins up five stacks signers, one miner Nakamoto node, and a corresponding bitcoind. @@ -11199,7 +11205,7 @@ fn injected_signatures_are_ignored_across_boundaries() { /// /// Test Assertion: /// Stacks tip advances to N+1 -fn rejected_blocks_count_towards_miner_validity() { +fn reorg_attempts_count_towards_miner_validity() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; } @@ -11216,12 +11222,14 @@ fn rejected_blocks_count_towards_miner_validity() { let send_amt = 100; let send_fee = 180; let recipient = PrincipalData::from(StacksAddress::burn_address(false)); - let block_proposal_timeout = Duration::from_secs(20); + let block_proposal_timeout = Duration::from_secs(30); + let reorg_attempts_activity_timeout = Duration::from_secs(20); let mut signer_test: SignerTest = SignerTest::new_with_config_modifications( num_signers, vec![(sender_addr, send_amt + send_fee)], |config| { config.block_proposal_timeout = block_proposal_timeout; + config.reorg_attempts_activity_timeout = reorg_attempts_activity_timeout; }, |_| {}, None, @@ -11363,6 +11371,194 @@ fn rejected_blocks_count_towards_miner_validity() { signer_test.shutdown(); } +#[test] +#[ignore] +/// Test that signers do not count a block proposal that was rejected due to a reorg towards miner activity since it showed up AFTER +/// the reorg_attempts_activity_timeout +/// +/// Test Setup: +/// The test spins up five stacks signers, one miner Nakamoto node, and a corresponding bitcoind. +/// The stacks node is then advanced to Epoch 3.0 boundary to allow block signing. The block proposal timeout is set to 20 seconds. +/// +/// Test Execution: +/// Test validation endpoint is stalled. +/// The miner proposes a block N. +/// Block proposals are stalled. +/// A new tenure is started. +/// The test waits for reorg_attempts_activity_timeout + 1 second. +/// The miner proposes a block N'. +/// The test waits for block proposal timeout + 1 second. +/// The validation endpoint is resumed. +/// The signers accept block N. +/// The signers reject block N'. +/// The miner proposes block N+1. +/// The signers reject block N+1. +/// +/// Test Assertion: +/// Stacks tip advances to N. +fn reorg_attempts_activity_timeout_exceeded() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + info!("------------------------- Test Setup -------------------------"); + let num_signers = 5; + let sender_sk = Secp256k1PrivateKey::new(); + let sender_addr = tests::to_addr(&sender_sk); + let send_amt = 100; + let send_fee = 180; + let recipient = PrincipalData::from(StacksAddress::burn_address(false)); + let block_proposal_timeout = Duration::from_secs(30); + let reorg_attempts_activity_timeout = Duration::from_secs(20); + let mut signer_test: SignerTest = SignerTest::new_with_config_modifications( + num_signers, + vec![(sender_addr, send_amt + send_fee)], + |config| { + config.block_proposal_timeout = block_proposal_timeout; + config.reorg_attempts_activity_timeout = reorg_attempts_activity_timeout; + }, + |_| {}, + None, + None, + ); + let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind); + + signer_test.boot_to_epoch_3(); + + let wait_for_block_proposal = || { + let mut block_proposal = None; + let _ = wait_for(30, || { + block_proposal = test_observer::get_stackerdb_chunks() + .into_iter() + .flat_map(|chunk| chunk.modified_slots) + .find_map(|chunk| { + let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) + .expect("Failed to deserialize SignerMessage"); + if let SignerMessage::BlockProposal(proposal) = message { + return Some(proposal); + } + None + }); + Ok(block_proposal.is_some()) + }); + block_proposal + }; + + info!("------------------------- Test Mine Block N -------------------------"); + let chain_before = get_chain_info(&signer_test.running_nodes.conf); + // Stall validation so signers will be unable to process the tenure change block for Tenure B. + TEST_VALIDATE_STALL.set(true); + test_observer::clear(); + // submit a tx so that the miner will mine an extra block + let sender_nonce = 0; + let transfer_tx = make_stacks_transfer( + &sender_sk, + sender_nonce, + send_fee, + signer_test.running_nodes.conf.burnchain.chain_id, + &recipient, + send_amt, + ); + submit_tx(&http_origin, &transfer_tx); + + let block_proposal_n = wait_for_block_proposal().expect("Failed to get block proposal N"); + let chain_after = get_chain_info(&signer_test.running_nodes.conf); + assert_eq!(chain_after, chain_before); + TEST_BROADCAST_STALL.set(true); + + info!("------------------------- Start Tenure B -------------------------"); + let commits_before = signer_test + .running_nodes + .commits_submitted + .load(Ordering::SeqCst); + + next_block_and( + &mut signer_test.running_nodes.btc_regtest_controller, + 60, + || { + let commits_count = signer_test + .running_nodes + .commits_submitted + .load(Ordering::SeqCst); + Ok(commits_count > commits_before) + }, + ) + .unwrap(); + + std::thread::sleep(reorg_attempts_activity_timeout.add(Duration::from_secs(1))); + test_observer::clear(); + TEST_BROADCAST_STALL.set(false); + let block_proposal_n_prime = + wait_for_block_proposal().expect("Failed to get block proposal N'"); + std::thread::sleep(block_proposal_timeout.add(Duration::from_secs(1))); + + assert_ne!(block_proposal_n, block_proposal_n_prime); + let chain_before = get_chain_info(&signer_test.running_nodes.conf); + TEST_VALIDATE_STALL.set(false); + + wait_for(30, || { + let chain_info = get_chain_info(&signer_test.running_nodes.conf); + Ok(chain_info.stacks_tip_height > chain_before.stacks_tip_height) + }) + .expect("Timed out waiting for stacks tip to advance to block N"); + + let chain_after = get_chain_info(&signer_test.running_nodes.conf); + assert_eq!( + chain_after.stacks_tip_height, + block_proposal_n.block.header.chain_length + ); + + let chain_before = chain_after; + info!("------------------------- Wait for Block N' Rejection -------------------------"); + wait_for(30, || { + let stackerdb_events = test_observer::get_stackerdb_chunks(); + let block_rejections = stackerdb_events + .into_iter() + .flat_map(|chunk| chunk.modified_slots) + .filter_map(|chunk| { + let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) + .expect("Failed to deserialize SignerMessage"); + match message { + SignerMessage::BlockResponse(BlockResponse::Rejected(rejection)) => { + if rejection.signer_signature_hash + == block_proposal_n_prime.block.header.signer_signature_hash() + { + assert_eq!(rejection.reason_code, RejectCode::SortitionViewMismatch); + Some(rejection) + } else { + None + } + } + _ => None, + } + }) + .collect::>(); + Ok(block_rejections.len() >= num_signers * 3 / 10) + }) + .expect("FAIL: Timed out waiting for block proposal rejections of N'"); + + info!("------------------------- Ensure chain halts -------------------------"); + assert!(wait_for(30, || { + let chain_info = get_chain_info(&signer_test.running_nodes.conf); + Ok(chain_info.stacks_tip_height > chain_before.stacks_tip_height) + }) + .is_err()); + + // The signer should automatically attempt to mine a new block once the signers eventually tell it to abandon the previous block + // It will accept reject it though because the block proposal timeout is exceeded and its first block proposal arrived AFTER the reorg activity timeout + let chain_after = get_chain_info(&signer_test.running_nodes.conf); + assert_eq!( + chain_after.stacks_tip_height, + block_proposal_n.block.header.chain_length + ); + signer_test.shutdown(); +} + #[test] #[ignore] fn fast_sortition() { From faebfdafc61cdf80a1e2cf59f45d6f7406659fce Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 28 Jan 2025 08:51:15 -0800 Subject: [PATCH 06/27] Rename test to reorg_attempts_activity_timeout_exceeded Signed-off-by: Jacinta Ferrant --- .github/workflows/bitcoin-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index 5c7fcb9bde..27e0c53100 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -146,7 +146,7 @@ jobs: - tests::signer::v0::single_miner_empty_sortition - tests::signer::v0::multiple_miners_empty_sortition - tests::signer::v0::block_proposal_timeout - - tests::signer::v0::reorg_attempts_count_towards_miner_validity + - tests::signer::v0::reorg_attempts_activity_timeout_exceeded - tests::signer::v0::late_reorg_attempts_do_not_count_towards_miner_validity - tests::signer::v0::allow_reorg_within_first_proposal_burn_block_timing_secs - tests::nakamoto_integrations::burn_ops_integration_test From e85e8c3729fe2350aa0be43a8d87b37b53ed9144 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 28 Jan 2025 18:10:30 -0800 Subject: [PATCH 07/27] CRC and fix test compilatation after merge Signed-off-by: Jacinta Ferrant --- .github/workflows/bitcoin-tests.yml | 2 +- stacks-signer/src/chainstate.rs | 11 +++++------ stacks-signer/src/config.rs | 8 ++++---- stacks-signer/src/signerdb.rs | 2 +- testnet/stacks-node/src/tests/signer/v0.rs | 2 +- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index 27e0c53100..43e46318dc 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -146,8 +146,8 @@ jobs: - tests::signer::v0::single_miner_empty_sortition - tests::signer::v0::multiple_miners_empty_sortition - tests::signer::v0::block_proposal_timeout + - tests::signer::v0::reorg_attempts_count_towards_miner_validity - tests::signer::v0::reorg_attempts_activity_timeout_exceeded - - tests::signer::v0::late_reorg_attempts_do_not_count_towards_miner_validity - tests::signer::v0::allow_reorg_within_first_proposal_burn_block_timing_secs - tests::nakamoto_integrations::burn_ops_integration_test - tests::nakamoto_integrations::check_block_heights diff --git a/stacks-signer/src/chainstate.rs b/stacks-signer/src/chainstate.rs index 5eeb6c50ad..0fd81022e5 100644 --- a/stacks-signer/src/chainstate.rs +++ b/stacks-signer/src/chainstate.rs @@ -124,8 +124,8 @@ pub struct ProposalEvalConfig { pub tenure_last_block_proposal_timeout: Duration, /// How much idle time must pass before allowing a tenure extend pub tenure_idle_timeout: Duration, - /// Time following a block's global acceptance that a signer will consider an attempt by a miner to reorg the block - /// as valid towards miner activity + /// Time following the last block of the previous tenure's global acceptance that a signer will consider an attempt by + /// the new miner to reorg it as valid towards miner activity pub reorg_attempts_activity_timeout: Duration, } @@ -574,10 +574,9 @@ impl SortitionsView { "proposed_chain_length" => block.header.chain_length, "expected_at_least" => info.block.header.chain_length + 1, ); - if info.signed_group.unwrap_or(get_epoch_time_secs()) - + reorg_attempts_activity_timeout.as_secs() - > get_epoch_time_secs() - { + if info.signed_group.map_or(true, |signed_time| { + signed_time + reorg_attempts_activity_timeout.as_secs() > get_epoch_time_secs() + }) { // Note if there is no signed_group time, this is a locally accepted block (i.e. tenure_last_block_proposal_timeout has not been exceeded). // Treat any attempt to reorg a locally accepted block as valid miner activity. // If the call returns a globally accepted block, check its globally accepted time against a quarter of the block_proposal_timeout diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index d91909a6d1..2be607f512 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -142,8 +142,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, - /// Time following a block's global acceptance that a signer will consider an attempt by a miner to reorg the block - /// as valid towards miner activity + /// Time following the last block of the previous tenure's global acceptance that a signer will consider an attempt by + /// the new miner to reorg it as valid towards miner activity pub reorg_attempts_activity_timeout: Duration, } @@ -185,8 +185,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, - /// Time following a block's global acceptance that a signer will consider an attempt by a miner to reorg the block - /// as valid towards miner activity + /// Time following the last block of the previous tenure's global acceptance that a signer will consider an attempt by + /// the new miner to reorg it as valid towards miner activity pub reorg_attempts_activity_timeout: Duration, } diff --git a/stacks-signer/src/signerdb.rs b/stacks-signer/src/signerdb.rs index d186c9a7f2..278242001c 100644 --- a/stacks-signer/src/signerdb.rs +++ b/stacks-signer/src/signerdb.rs @@ -774,7 +774,7 @@ impl SignerDb { /// Return whether there was signed block in a tenure (identified by its consensus hash) pub fn has_signed_block_in_tenure(&self, tenure: &ConsensusHash) -> Result { - let query = "SELECT block_info FROM blocks WHERE consensus_hash = ? AND signed_over = 1 ORDER BY stacks_height DESC LIMIT 1"; + let query = "SELECT block_info FROM blocks WHERE consensus_hash = ? AND signed_over = 1 DESC LIMIT 1"; let result: Option = query_row(&self.db, query, [tenure])?; Ok(result.is_some()) diff --git a/testnet/stacks-node/src/tests/signer/v0.rs b/testnet/stacks-node/src/tests/signer/v0.rs index 846a6117ab..b181107a0c 100644 --- a/testnet/stacks-node/src/tests/signer/v0.rs +++ b/testnet/stacks-node/src/tests/signer/v0.rs @@ -11410,7 +11410,7 @@ fn reorg_attempts_activity_timeout_exceeded() { info!("------------------------- Test Setup -------------------------"); let num_signers = 5; - let sender_sk = Secp256k1PrivateKey::new(); + let sender_sk = Secp256k1PrivateKey::random(); let sender_addr = tests::to_addr(&sender_sk); let send_amt = 100; let send_fee = 180; From c3c03929a2afff49a3cf46162c21f2153e490639 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 29 Jan 2025 08:59:00 -0800 Subject: [PATCH 08/27] Update changelog Signed-off-by: Jacinta Ferrant --- stacks-signer/CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/stacks-signer/CHANGELOG.md b/stacks-signer/CHANGELOG.md index 2697d93508..83ac34c784 100644 --- a/stacks-signer/CHANGELOG.md +++ b/stacks-signer/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to the versioning scheme outlined in the [README.md](README.md). +## [Unreleased] + +## Added + +- Introduced the `reorg_attempts_activity_timeout_ms` configuration option for signers which is used to determine the length of time after the last block of a tenure is confirmed that an incoming miner's attempts to reorg it are considered valid miner activity. + +### Changed + +- Signers no longer view any block proposal by a miner in their DB as indicative of valid miner activity. + ## [3.1.0.0.4.0] ## Added From abff263c3b7896b443e33f4f1d218ab057a83424 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 29 Jan 2025 09:19:13 -0800 Subject: [PATCH 09/27] Fix bad change to database call to get signed blocks in a tenure Signed-off-by: Jacinta Ferrant --- stacks-signer/src/signerdb.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/stacks-signer/src/signerdb.rs b/stacks-signer/src/signerdb.rs index 278242001c..8cd4f93ed6 100644 --- a/stacks-signer/src/signerdb.rs +++ b/stacks-signer/src/signerdb.rs @@ -774,7 +774,8 @@ impl SignerDb { /// Return whether there was signed block in a tenure (identified by its consensus hash) pub fn has_signed_block_in_tenure(&self, tenure: &ConsensusHash) -> Result { - let query = "SELECT block_info FROM blocks WHERE consensus_hash = ? AND signed_over = 1 DESC LIMIT 1"; + let query = + "SELECT block_info FROM blocks WHERE consensus_hash = ? AND signed_over = 1 LIMIT 1"; let result: Option = query_row(&self.db, query, [tenure])?; Ok(result.is_some()) @@ -1971,6 +1972,7 @@ mod tests { assert!(db.has_signed_block_in_tenure(&consensus_hash_1).unwrap()); assert!(!db.has_signed_block_in_tenure(&consensus_hash_2).unwrap()); + block_info.block.header.consensus_hash = consensus_hash_2; block_info.block.header.chain_length = 2; block_info.signed_over = false; @@ -1978,6 +1980,13 @@ mod tests { assert!(db.has_signed_block_in_tenure(&consensus_hash_1).unwrap()); assert!(!db.has_signed_block_in_tenure(&consensus_hash_2).unwrap()); + + block_info.signed_over = true; + + db.insert_block(&block_info).unwrap(); + + assert!(db.has_signed_block_in_tenure(&consensus_hash_1).unwrap()); + assert!(db.has_signed_block_in_tenure(&consensus_hash_2).unwrap()); } #[test] From 9851a2417868465924d551efc2a1ccc9376b5a09 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Wed, 29 Jan 2025 15:36:05 -0500 Subject: [PATCH 10/27] chore: extend the default `block_proposal_timeout` to 4 hours Until #5729 is implemented, then there is no point in rejecting a block from a miner, no matter how late it is, since no other miner will ever try to extend into its tenure. Fixes #5753 --- stacks-signer/CHANGELOG.md | 6 ++++++ stacks-signer/src/config.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/stacks-signer/CHANGELOG.md b/stacks-signer/CHANGELOG.md index 2697d93508..398d4125b3 100644 --- a/stacks-signer/CHANGELOG.md +++ b/stacks-signer/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to the versioning scheme outlined in the [README.md](README.md). +## [Unreleased] + +### Changed + +- Increase default `block_proposal_timeout_ms` from 10 minutes to 4 hours. Until #5729 is implemented, there is no value in rejecting a late block from a miner, since a late block is better than no block at all. + ## [3.1.0.0.4.0] ## Added diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index a50ca7ecf8..dfcafd50e0 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -35,7 +35,7 @@ use stacks_common::util::hash::Hash160; use crate::client::SignerSlotID; const EVENT_TIMEOUT_MS: u64 = 5000; -const BLOCK_PROPOSAL_TIMEOUT_MS: u64 = 600_000; +const BLOCK_PROPOSAL_TIMEOUT_MS: u64 = 14_400_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; From 98fa64777271a3c1b0334aa1fdaf7778c62a9b1a Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 29 Jan 2025 13:26:11 -0800 Subject: [PATCH 11/27] Cleanup reorg_attempts_activity_timeout_exceeded test Signed-off-by: Jacinta Ferrant --- testnet/stacks-node/src/tests/signer/v0.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/testnet/stacks-node/src/tests/signer/v0.rs b/testnet/stacks-node/src/tests/signer/v0.rs index 905f99d982..f88786f9a1 100644 --- a/testnet/stacks-node/src/tests/signer/v0.rs +++ b/testnet/stacks-node/src/tests/signer/v0.rs @@ -11640,19 +11640,14 @@ fn reorg_attempts_activity_timeout_exceeded() { .expect("FAIL: Timed out waiting for block proposal rejections of N'"); info!("------------------------- Ensure chain halts -------------------------"); + // The signer should automatically attempt to mine a new block once the signers eventually tell it to abandon the previous block + // It will reject it though because the block proposal timeout is exceeded and its first block proposal arrived AFTER the reorg activity timeout assert!(wait_for(30, || { let chain_info = get_chain_info(&signer_test.running_nodes.conf); - Ok(chain_info.stacks_tip_height > chain_before.stacks_tip_height) + assert_eq!(chain_info.stacks_tip_height, chain_before.stacks_tip_height); + Ok(false) }) .is_err()); - - // The signer should automatically attempt to mine a new block once the signers eventually tell it to abandon the previous block - // It will accept reject it though because the block proposal timeout is exceeded and its first block proposal arrived AFTER the reorg activity timeout - let chain_after = get_chain_info(&signer_test.running_nodes.conf); - assert_eq!( - chain_after.stacks_tip_height, - block_proposal_n.block.header.chain_length - ); signer_test.shutdown(); } From f09306eb921500ea5ae740e59caa41315610169c Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Thu, 30 Jan 2025 08:40:49 -0800 Subject: [PATCH 12/27] Fix reorg_attempts_activity_timeout_exceeded test Signed-off-by: Jacinta Ferrant --- testnet/stacks-node/src/tests/signer/v0.rs | 121 ++++++++++++++------- 1 file changed, 79 insertions(+), 42 deletions(-) diff --git a/testnet/stacks-node/src/tests/signer/v0.rs b/testnet/stacks-node/src/tests/signer/v0.rs index f88786f9a1..6a24e9f68c 100644 --- a/testnet/stacks-node/src/tests/signer/v0.rs +++ b/testnet/stacks-node/src/tests/signer/v0.rs @@ -11546,9 +11546,39 @@ fn reorg_attempts_activity_timeout_exceeded() { block_proposal }; + let wait_for_block_rejections = |hash: Sha512Trunc256Sum| { + wait_for(30, || { + let stackerdb_events = test_observer::get_stackerdb_chunks(); + let block_rejections = stackerdb_events + .into_iter() + .flat_map(|chunk| chunk.modified_slots) + .filter_map(|chunk| { + let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) + .expect("Failed to deserialize SignerMessage"); + match message { + SignerMessage::BlockResponse(BlockResponse::Rejected(rejection)) => { + if rejection.signer_signature_hash == hash { + assert_eq!( + rejection.reason_code, + RejectCode::SortitionViewMismatch + ); + Some(rejection) + } else { + None + } + } + _ => None, + } + }) + .collect::>(); + Ok(block_rejections.len() >= num_signers * 3 / 10) + }) + }; + info!("------------------------- Test Mine Block N -------------------------"); let chain_before = get_chain_info(&signer_test.running_nodes.conf); // Stall validation so signers will be unable to process the tenure change block for Tenure B. + // And so the incoming miner proposes a block N' (the reorging block). TEST_VALIDATE_STALL.set(true); test_observer::clear(); // submit a tx so that the miner will mine an extra block @@ -11573,7 +11603,7 @@ fn reorg_attempts_activity_timeout_exceeded() { .running_nodes .commits_submitted .load(Ordering::SeqCst); - + let chain_before = get_chain_info(&signer_test.running_nodes.conf); next_block_and( &mut signer_test.running_nodes.btc_regtest_controller, 60, @@ -11582,67 +11612,74 @@ fn reorg_attempts_activity_timeout_exceeded() { .running_nodes .commits_submitted .load(Ordering::SeqCst); - Ok(commits_count > commits_before) + let chain_info = get_chain_info(&signer_test.running_nodes.conf); + Ok(commits_count > commits_before + && chain_info.burn_block_height > chain_before.burn_block_height) }, ) .unwrap(); - std::thread::sleep(reorg_attempts_activity_timeout.add(Duration::from_secs(1))); + info!("------------------------- Wait for block N' to arrive late -------------------------"); test_observer::clear(); - TEST_BROADCAST_STALL.set(false); - let block_proposal_n_prime = - wait_for_block_proposal().expect("Failed to get block proposal N'"); - std::thread::sleep(block_proposal_timeout.add(Duration::from_secs(1))); - - assert_ne!(block_proposal_n, block_proposal_n_prime); - let chain_before = get_chain_info(&signer_test.running_nodes.conf); + // Allow block N validation to finish. TEST_VALIDATE_STALL.set(false); - wait_for(30, || { let chain_info = get_chain_info(&signer_test.running_nodes.conf); Ok(chain_info.stacks_tip_height > chain_before.stacks_tip_height) }) - .expect("Timed out waiting for stacks tip to advance to block N"); - + .expect("Tiemd out waiting for stacks tip to advance to block N"); let chain_after = get_chain_info(&signer_test.running_nodes.conf); + TEST_VALIDATE_STALL.set(true); + // Allow incoming mine to propose block N' + // Make sure to wait the reorg_attempts_activity_timeout AFTER the block is globally signed over + // as this is the point where signers start considering from. + std::thread::sleep(reorg_attempts_activity_timeout.add(Duration::from_secs(1))); + TEST_BROADCAST_STALL.set(false); + let block_proposal_n_prime = + wait_for_block_proposal().expect("Failed to get block proposal N'"); + assert_eq!( + block_proposal_n_prime.block.header.chain_length, + chain_after.stacks_tip_height + ); + // Make sure that no subsequent proposal arrives before the block_proposal_timeout is exceeded + TEST_BROADCAST_STALL.set(true); + TEST_VALIDATE_STALL.set(false); + // We only need to wait the difference between the two timeouts now since we already slept for a min of reorg_attempts_activity_timeout + 1 + std::thread::sleep(block_proposal_timeout.saturating_sub(reorg_attempts_activity_timeout)); + assert_ne!(block_proposal_n, block_proposal_n_prime); assert_eq!( chain_after.stacks_tip_height, block_proposal_n.block.header.chain_length ); - let chain_before = chain_after; + info!("------------------------- Wait for Block N' Rejection -------------------------"); + wait_for_block_rejections(block_proposal_n_prime.block.header.signer_signature_hash()) + .expect("FAIL: Timed out waiting for block proposal rejections of N'"); + + info!("------------------------- Wait for Block N+1 Proposal -------------------------"); + test_observer::clear(); + TEST_BROADCAST_STALL.set(false); wait_for(30, || { - let stackerdb_events = test_observer::get_stackerdb_chunks(); - let block_rejections = stackerdb_events - .into_iter() - .flat_map(|chunk| chunk.modified_slots) - .filter_map(|chunk| { - let message = SignerMessage::consensus_deserialize(&mut chunk.data.as_slice()) - .expect("Failed to deserialize SignerMessage"); - match message { - SignerMessage::BlockResponse(BlockResponse::Rejected(rejection)) => { - if rejection.signer_signature_hash - == block_proposal_n_prime.block.header.signer_signature_hash() - { - assert_eq!(rejection.reason_code, RejectCode::SortitionViewMismatch); - Some(rejection) - } else { - None - } - } - _ => None, - } - }) - .collect::>(); - Ok(block_rejections.len() >= num_signers * 3 / 10) - }) - .expect("FAIL: Timed out waiting for block proposal rejections of N'"); + let block_proposal_n_1 = + wait_for_block_proposal().expect("Failed to get block proposal N+1"); + Ok(block_proposal_n_1.block.header.chain_length + == block_proposal_n.block.header.chain_length + 1) + }) + .expect("Timed out waiting for block N+1 to be proposed"); + + info!("------------------------- Wait for Block N+1 Rejection -------------------------"); + // The miner will automatically reattempt to mine a block N+1 once it sees the stacks tip advance to block N. + // N+1 will still be rejected however as the signers will have already marked the miner as invalid since the reorg + // block N' arrived AFTER the reorg_attempts_activity_timeout and the subsequent block N+1 arrived AFTER the + // block_proposal_timeout. + let block_proposal_n_1 = wait_for_block_proposal().expect("Failed to get block proposal N+1'"); + wait_for_block_rejections(block_proposal_n_1.block.header.signer_signature_hash()) + .expect("FAIL: Timed out waiting for block proposal rejections of N+1'"); info!("------------------------- Ensure chain halts -------------------------"); - // The signer should automatically attempt to mine a new block once the signers eventually tell it to abandon the previous block - // It will reject it though because the block proposal timeout is exceeded and its first block proposal arrived AFTER the reorg activity timeout - assert!(wait_for(30, || { + // Just in case, wait again and ensure that the chain is still halted (once marked invalid, the miner can do nothing to satisfy the signers) + assert!(wait_for(reorg_attempts_activity_timeout.as_secs(), || { let chain_info = get_chain_info(&signer_test.running_nodes.conf); assert_eq!(chain_info.stacks_tip_height, chain_before.stacks_tip_height); Ok(false) From 38dbdcdd54590e143c02035b9577eb19e49e5aa3 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 30 Jan 2025 13:07:47 -0500 Subject: [PATCH 13/27] test: fix flakiness in `tests::nakamoto_integrations::skip_mining_long_tx` --- .../src/tests/nakamoto_integrations.rs | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index f3659d5957..2884e6d1b7 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -1688,22 +1688,22 @@ fn simple_neon_integration() { assert!(tip.anchored_header.as_stacks_nakamoto().is_some()); assert!(tip.stacks_block_height >= block_height_pre_3_0 + 30); - // Check that we aren't missing burn blocks + // Check that we aren't missing burn blocks (except during the Nakamoto transition) + let epoch_3 = &naka_conf.burnchain.epochs.unwrap()[StacksEpochId::Epoch30]; let bhh = u64::from(tip.burn_header_height); let missing = test_observer::get_missing_burn_blocks(220..=bhh).unwrap(); - // This test was flakey because it was sometimes missing burn block 230, which is right at the Nakamoto transition + // This test was flaky because it was sometimes missing burn block 230, which is right at the Nakamoto transition // So it was possible to miss a burn block during the transition - // But I don't it matters at this point since the Nakamoto transition has already happened on mainnet + // But I don't think it matters at this point since the Nakamoto transition has already happened on mainnet // So just print a warning instead, don't count it as an error let missing_is_error: Vec<_> = missing .into_iter() - .filter(|i| match i { - 230 => { - warn!("Missing burn block {i}"); + .filter(|&i| { + (i != epoch_3.start_height - 1) || { + warn!("Missing burn block {} at epoch 3 transition", i); false } - _ => true, }) .collect(); @@ -9885,9 +9885,23 @@ fn skip_mining_long_tx() { assert_eq!(sender_2_nonce, 0); assert_eq!(sender_1_nonce, 4); - // Check that we aren't missing burn blocks + // Check that we aren't missing burn blocks (except during the Nakamoto transition) + let epoch_3 = &naka_conf.burnchain.epochs.unwrap()[StacksEpochId::Epoch30]; let bhh = u64::from(tip.burn_header_height); - test_observer::contains_burn_block_range(220..=bhh).unwrap(); + let missing = test_observer::get_missing_burn_blocks(220..=bhh).unwrap(); + let missing_is_error: Vec<_> = missing + .into_iter() + .filter(|&i| { + (i != epoch_3.start_height - 1) || { + warn!("Missing burn block {} at epoch 3 transition", i); + false + } + }) + .collect(); + + if !missing_is_error.is_empty() { + panic!("Missing the following burn blocks: {missing_is_error:?}"); + } check_nakamoto_empty_block_heuristics(); From 6224b7231fb18b0b34d4339142a0a2a529516ee7 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 30 Jan 2025 13:18:01 -0500 Subject: [PATCH 14/27] test: increase timeout in `block_validation_pending_table` --- testnet/stacks-node/src/tests/signer/v0.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testnet/stacks-node/src/tests/signer/v0.rs b/testnet/stacks-node/src/tests/signer/v0.rs index feb870143f..46aebd7165 100644 --- a/testnet/stacks-node/src/tests/signer/v0.rs +++ b/testnet/stacks-node/src/tests/signer/v0.rs @@ -8024,7 +8024,7 @@ fn block_validation_pending_table() { .expect("Timed out waiting for pending block validation to be submitted"); info!("----- Waiting for pending block validation to be removed -----"); - wait_for(30, || { + wait_for(60, || { let is_pending = signer_db .has_pending_block_validation(&block_signer_signature_hash) .expect("Unexpected DBError"); From fa6a6b70d30f9dc4ae6e548542f218a84a2a12e0 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Fri, 31 Jan 2025 09:50:28 -0500 Subject: [PATCH 15/27] refactor: add `check_nakamoto_no_missing_blocks` func This function can be used to check for missing burn blocks, ignoring a block missing during the transition to epoch 3.0. --- .../src/tests/nakamoto_integrations.rs | 58 ++++++++----------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 682a7b216b..8e7132df49 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . use std::collections::{BTreeMap, HashMap, HashSet}; +use std::ops::RangeBounds; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::{Arc, Mutex}; @@ -1488,6 +1489,26 @@ fn wait_for_first_naka_block_commit(timeout_secs: u64, naka_commits_submitted: & } } +// Check for missing burn blocks in `range`, but allow for a missed block at +// the epoch 3 transition. Panic if any other blocks are missing. +fn check_nakamoto_no_missing_blocks(conf: &Config, range: impl RangeBounds) { + let epoch_3 = &conf.burnchain.epochs.as_ref().unwrap()[StacksEpochId::Epoch30]; + let missing = test_observer::get_missing_burn_blocks(range).unwrap(); + let missing_is_error: Vec<_> = missing + .into_iter() + .filter(|&i| { + (i != epoch_3.start_height - 1) || { + warn!("Missing burn block {} at epoch 3 transition", i); + false + } + }) + .collect(); + + if !missing_is_error.is_empty() { + panic!("Missing the following burn blocks: {missing_is_error:?}"); + } +} + #[test] #[ignore] /// This test spins up a nakamoto-neon node. @@ -1689,27 +1710,8 @@ fn simple_neon_integration() { assert!(tip.stacks_block_height >= block_height_pre_3_0 + 30); // Check that we aren't missing burn blocks (except during the Nakamoto transition) - let epoch_3 = &naka_conf.burnchain.epochs.unwrap()[StacksEpochId::Epoch30]; let bhh = u64::from(tip.burn_header_height); - let missing = test_observer::get_missing_burn_blocks(220..=bhh).unwrap(); - - // This test was flaky because it was sometimes missing burn block 230, which is right at the Nakamoto transition - // So it was possible to miss a burn block during the transition - // But I don't think it matters at this point since the Nakamoto transition has already happened on mainnet - // So just print a warning instead, don't count it as an error - let missing_is_error: Vec<_> = missing - .into_iter() - .filter(|&i| { - (i != epoch_3.start_height - 1) || { - warn!("Missing burn block {} at epoch 3 transition", i); - false - } - }) - .collect(); - - if !missing_is_error.is_empty() { - panic!("Missing the following burn blocks: {missing_is_error:?}"); - } + check_nakamoto_no_missing_blocks(&naka_conf, 220..=bhh); // make sure prometheus returns an updated number of processed blocks #[cfg(feature = "monitoring_prom")] @@ -10106,22 +10108,8 @@ fn skip_mining_long_tx() { assert_eq!(sender_1_nonce, 4); // Check that we aren't missing burn blocks (except during the Nakamoto transition) - let epoch_3 = &naka_conf.burnchain.epochs.unwrap()[StacksEpochId::Epoch30]; let bhh = u64::from(tip.burn_header_height); - let missing = test_observer::get_missing_burn_blocks(220..=bhh).unwrap(); - let missing_is_error: Vec<_> = missing - .into_iter() - .filter(|&i| { - (i != epoch_3.start_height - 1) || { - warn!("Missing burn block {} at epoch 3 transition", i); - false - } - }) - .collect(); - - if !missing_is_error.is_empty() { - panic!("Missing the following burn blocks: {missing_is_error:?}"); - } + check_nakamoto_no_missing_blocks(&naka_conf, 220..=bhh); check_nakamoto_empty_block_heuristics(); From c94f473eef03e2cef8b02b888cc91a3c81696d81 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 3 Feb 2025 22:05:30 -0500 Subject: [PATCH 16/27] chore: make test constant estimator public --- stackslib/src/cost_estimates/tests/fee_rate_fuzzer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/cost_estimates/tests/fee_rate_fuzzer.rs b/stackslib/src/cost_estimates/tests/fee_rate_fuzzer.rs index 8fe0622dc8..2682894e51 100644 --- a/stackslib/src/cost_estimates/tests/fee_rate_fuzzer.rs +++ b/stackslib/src/cost_estimates/tests/fee_rate_fuzzer.rs @@ -15,7 +15,7 @@ use crate::cost_estimates::fee_rate_fuzzer::FeeRateFuzzer; use crate::cost_estimates::tests::common::make_block_receipt; use crate::cost_estimates::{EstimatorError, FeeEstimator, FeeRateEstimate}; -struct ConstantFeeEstimator {} +pub struct ConstantFeeEstimator {} /// Returns a constant fee rate estimate. impl FeeEstimator for ConstantFeeEstimator { From 41a1026ebb71a509721d723a3876e4658010a693 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 3 Feb 2025 22:05:44 -0500 Subject: [PATCH 17/27] chore: fix 4145 by returning the JSON representation of a fee estimator error --- stackslib/src/net/api/postfeerate.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/stackslib/src/net/api/postfeerate.rs b/stackslib/src/net/api/postfeerate.rs index cb012bbc6c..7077649791 100644 --- a/stackslib/src/net/api/postfeerate.rs +++ b/stackslib/src/net/api/postfeerate.rs @@ -18,6 +18,7 @@ use std::io::{Read, Write}; use clarity::vm::costs::ExecutionCost; use regex::{Captures, Regex}; +use serde_json::json; use stacks_common::codec::{StacksMessageCodec, MAX_PAYLOAD_LEN}; use stacks_common::types::chainstate::{ BlockHeaderHash, ConsensusHash, StacksBlockId, StacksPublicKey, @@ -118,13 +119,7 @@ impl RPCPostFeeRateRequestHandler { let scalar_cost = metric.from_cost_and_len(&estimated_cost, &stacks_epoch.block_limit, estimated_len); let fee_rates = fee_estimator.get_rate_estimates().map_err(|e| { - StacksHttpResponse::new_error( - preamble, - &HttpBadRequest::new(format!( - "Estimator RPC endpoint failed to estimate fees for tx: {:?}", - &e - )), - ) + StacksHttpResponse::new_error(preamble, &HttpBadRequest::new_json(e.into_json())) })?; let mut estimations = RPCFeeEstimate::estimate_fees(scalar_cost, fee_rates).to_vec(); @@ -243,11 +238,7 @@ impl RPCRequestHandler for RPCPostFeeRateRequestHandler { .map_err(|e| { StacksHttpResponse::new_error( &preamble, - &HttpBadRequest::new(format!( - "Estimator RPC endpoint failed to estimate tx {}: {:?}", - &tx.name(), - &e - )), + &HttpBadRequest::new_json(e.into_json()), ) })?; @@ -263,9 +254,9 @@ impl RPCRequestHandler for RPCPostFeeRateRequestHandler { debug!("Fee and cost estimation not configured on this stacks node"); Err(StacksHttpResponse::new_error( &preamble, - &HttpBadRequest::new( - "Fee estimation not supported on this node".to_string(), - ), + &HttpBadRequest::new_json(json!( + "Fee estimation not supported on this node" + )), )) } }); From 22297f7ce3d900fa9b4951b708ddad7b217732de Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 3 Feb 2025 22:06:04 -0500 Subject: [PATCH 18/27] chore: instantiate different kinds of RPCHandlerArgs to test fee estimation errors --- stackslib/src/net/api/tests/mod.rs | 46 ++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/stackslib/src/net/api/tests/mod.rs b/stackslib/src/net/api/tests/mod.rs index 14034e3eaf..e85ed7a290 100644 --- a/stackslib/src/net/api/tests/mod.rs +++ b/stackslib/src/net/api/tests/mod.rs @@ -15,6 +15,7 @@ // along with this program. If not, see . use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; use clarity::vm::costs::ExecutionCost; use clarity::vm::types::{QualifiedContractIdentifier, StacksAddressExtensions}; @@ -46,7 +47,7 @@ use crate::net::db::PeerDB; use crate::net::httpcore::{StacksHttpRequest, StacksHttpResponse}; use crate::net::relay::Relayer; use crate::net::rpc::ConversationHttp; -use crate::net::test::{TestEventObserver, TestPeer, TestPeerConfig}; +use crate::net::test::{RPCHandlerArgsType, TestEventObserver, TestPeer, TestPeerConfig}; use crate::net::tests::inv::nakamoto::make_nakamoto_peers_from_invs_ext; use crate::net::{ Attachment, AttachmentInstance, MemPoolEventDispatcher, RPCHandlerArgs, StackerDBConfig, @@ -226,10 +227,28 @@ pub struct TestRPC<'a> { impl<'a> TestRPC<'a> { pub fn setup(test_name: &str) -> TestRPC<'a> { - Self::setup_ex(test_name, true) + Self::setup_ex(test_name, true, None, None) } - pub fn setup_ex(test_name: &str, process_microblock: bool) -> TestRPC<'a> { + pub fn setup_with_rpc_args( + test_name: &str, + rpc_handler_args_opt_1: Option, + rpc_handler_args_opt_2: Option, + ) -> TestRPC<'a> { + Self::setup_ex( + test_name, + true, + rpc_handler_args_opt_1, + rpc_handler_args_opt_2, + ) + } + + pub fn setup_ex( + test_name: &str, + process_microblock: bool, + rpc_handler_args_opt_1: Option, + rpc_handler_args_opt_2: Option, + ) -> TestRPC<'a> { // ST2DS4MSWSGJ3W9FBC6BVT0Y92S345HY8N3T6AV7R let privk1 = StacksPrivateKey::from_hex( "9f1f85a512a96a244e4c0d762788500687feb97481639572e3bffbd6860e6ab001", @@ -317,6 +336,9 @@ impl<'a> TestRPC<'a> { let mut peer_1 = TestPeer::new(peer_1_config); let mut peer_2 = TestPeer::new(peer_2_config); + peer_1.rpc_handler_args = rpc_handler_args_opt_1; + peer_2.rpc_handler_args = rpc_handler_args_opt_2; + // mine one block with a contract in it // first the coinbase // make a coinbase for this miner @@ -976,7 +998,11 @@ impl<'a> TestRPC<'a> { } { - let mut rpc_args = RPCHandlerArgs::default(); + let mut rpc_args = peer_1 + .rpc_handler_args + .as_ref() + .map(|args_type| args_type.instantiate()) + .unwrap_or(RPCHandlerArgsType::make_default()); rpc_args.event_observer = event_observer; let mut node_state = StacksNodeState::new( &mut peer_1.network, @@ -1020,7 +1046,11 @@ impl<'a> TestRPC<'a> { } { - let mut rpc_args = RPCHandlerArgs::default(); + let mut rpc_args = peer_2 + .rpc_handler_args + .as_ref() + .map(|args_type| args_type.instantiate()) + .unwrap_or(RPCHandlerArgsType::make_default()); rpc_args.event_observer = event_observer; let mut node_state = StacksNodeState::new( &mut peer_2.network, @@ -1076,7 +1106,11 @@ impl<'a> TestRPC<'a> { let mut peer_1_stacks_node = peer_1.stacks_node.take().unwrap(); let mut peer_1_mempool = peer_1.mempool.take().unwrap(); - let rpc_args = RPCHandlerArgs::default(); + let rpc_args = peer_1 + .rpc_handler_args + .as_ref() + .map(|args_type| args_type.instantiate()) + .unwrap_or(RPCHandlerArgsType::make_default()); let mut node_state = StacksNodeState::new( &mut peer_1.network, &peer_1_sortdb, From f85611ca003aaaed5631402410689b6f9d80c704 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 3 Feb 2025 22:06:31 -0500 Subject: [PATCH 19/27] chore: test 4145 fix by supplying different fee estimators --- stackslib/src/net/api/tests/postfeerate.rs | 88 +++++++++++++++++++++- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/stackslib/src/net/api/tests/postfeerate.rs b/stackslib/src/net/api/tests/postfeerate.rs index b762264731..753684d483 100644 --- a/stackslib/src/net/api/tests/postfeerate.rs +++ b/stackslib/src/net/api/tests/postfeerate.rs @@ -15,23 +15,29 @@ // along with this program. If not, see . use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::sync::Arc; use clarity::vm::types::{PrincipalData, QualifiedContractIdentifier, StacksAddressExtensions}; use clarity::vm::{ClarityName, ContractName, Value}; use stacks_common::types::chainstate::StacksAddress; use stacks_common::types::net::PeerHost; use stacks_common::types::Address; -use stacks_common::util::hash::to_hex; +use stacks_common::util::hash::{to_hex, Sha256Sum}; use super::test_rpc; use crate::chainstate::stacks::TransactionPayload; use crate::core::BLOCK_LIMIT_MAINNET_21; +use crate::cost_estimates::metrics::UnitMetric; +use crate::cost_estimates::tests::fee_rate_fuzzer::ConstantFeeEstimator; +use crate::cost_estimates::UnitEstimator; +use crate::net::api::tests::TestRPC; use crate::net::api::*; use crate::net::connection::ConnectionOptions; use crate::net::httpcore::{ HttpRequestContentsExtensions, RPCRequestHandler, StacksHttp, StacksHttpRequest, }; -use crate::net::{ProtocolFamily, TipRequest}; +use crate::net::test::RPCHandlerArgsType; +use crate::net::{ProtocolFamily, RPCHandlerArgs, TipRequest}; #[test] fn test_try_parse_request() { @@ -89,9 +95,37 @@ fn test_try_make_response() { TransactionPayload::new_contract_call(sender_addr, "hello-world", "add-unit", vec![]) .unwrap(); + // case 1: no fee estimates let mut requests = vec![]; let request = StacksHttpRequest::new_post_fee_rate( - addr.into(), + addr.clone().into(), + postfeerate::FeeRateEstimateRequestBody { + estimated_len: Some(123), + transaction_payload: to_hex(&tx_payload.serialize_to_vec()), + }, + ); + requests.push(request); + + let test_rpc = TestRPC::setup(function_name!()); + let mut responses = test_rpc.run(requests); + + let response = responses.remove(0); + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() + ); + + let (preamble, body) = response.destruct(); + let body_json: serde_json::Value = body.try_into().unwrap(); + + // get back a JSON string and a 400 + assert_eq!(preamble.status_code, 400); + debug!("Response JSON no estimator: {}", &body_json); + + // case 2: no estimate avaialable + let mut requests = vec![]; + let request = StacksHttpRequest::new_post_fee_rate( + addr.clone().into(), postfeerate::FeeRateEstimateRequestBody { estimated_len: Some(123), transaction_payload: to_hex(&tx_payload.serialize_to_vec()), @@ -99,7 +133,12 @@ fn test_try_make_response() { ); requests.push(request); - let mut responses = test_rpc(function_name!(), requests); + let test_rpc = TestRPC::setup_with_rpc_args( + function_name!(), + Some(RPCHandlerArgsType::Null), + Some(RPCHandlerArgsType::Null), + ); + let mut responses = test_rpc.run(requests); let response = responses.remove(0); debug!( @@ -108,5 +147,46 @@ fn test_try_make_response() { ); let (preamble, body) = response.destruct(); + let body_json: serde_json::Value = body.try_into().unwrap(); + + // get back a JSON object and a 400 assert_eq!(preamble.status_code, 400); + debug!("Response JSON no estimate fee: {}", &body_json); + assert_eq!( + body_json.get("reason").unwrap().as_str().unwrap(), + "NoEstimateAvailable" + ); + assert!(body_json.get("error").is_some()); + assert!(body_json.get("reason_data").is_some()); + + // case 3: get an estimate + let mut requests = vec![]; + let request = StacksHttpRequest::new_post_fee_rate( + addr.clone().into(), + postfeerate::FeeRateEstimateRequestBody { + estimated_len: Some(123), + transaction_payload: to_hex(&tx_payload.serialize_to_vec()), + }, + ); + requests.push(request); + + let test_rpc = TestRPC::setup_with_rpc_args( + function_name!(), + Some(RPCHandlerArgsType::Unit), + Some(RPCHandlerArgsType::Unit), + ); + let mut responses = test_rpc.run(requests); + + let response = responses.remove(0); + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() + ); + + let (preamble, body) = response.destruct(); + let body_json: serde_json::Value = body.try_into().unwrap(); + + // get back a JSON object and a 200 + assert_eq!(preamble.status_code, 200); + debug!("Response JSON success: {}", &body_json); } From da2ab6b6dc84c51e1376baf9bb00f18b5ea1c151 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 3 Feb 2025 22:06:55 -0500 Subject: [PATCH 20/27] chore: API sync --- stackslib/src/net/api/tests/postmicroblock.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/net/api/tests/postmicroblock.rs b/stackslib/src/net/api/tests/postmicroblock.rs index 92504a5560..90802adfd8 100644 --- a/stackslib/src/net/api/tests/postmicroblock.rs +++ b/stackslib/src/net/api/tests/postmicroblock.rs @@ -102,7 +102,7 @@ fn test_try_parse_request() { fn test_try_make_response() { let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); - let test_rpc = TestRPC::setup_ex(function_name!(), false); + let test_rpc = TestRPC::setup_ex(function_name!(), false, None, None); let mblock = test_rpc.next_microblock.clone().unwrap(); let mut requests = vec![]; From 64bf350672716ed29b65c584528c4300cab0d682 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Mon, 3 Feb 2025 22:07:02 -0500 Subject: [PATCH 21/27] chore: add support for instantiating owned RPCHandlerArgs in TestPeer that don't need to cross thread boundaries --- stackslib/src/net/mod.rs | 97 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index 6e6870cbfb..695574769c 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -619,7 +619,7 @@ impl PeerHostExtensions for PeerHost { } /// Runtime arguments to an RPC handler -#[derive(Default)] +#[derive(Default, Clone)] pub struct RPCHandlerArgs<'a> { /// What height at which this node will terminate (testnet only) pub exit_at_block_height: Option, @@ -2222,7 +2222,7 @@ pub mod test { use std::net::*; use std::ops::{Deref, DerefMut}; use std::sync::mpsc::sync_channel; - use std::sync::Mutex; + use std::sync::{Arc, Mutex}; use std::{fs, io, thread}; use clarity::boot_util::boot_code_id; @@ -2277,6 +2277,9 @@ pub mod test { use crate::chainstate::*; use crate::clarity::vm::clarity::TransactionConnection; use crate::core::{EpochList, StacksEpoch, StacksEpochExtension, NETWORK_P2P_PORT}; + use crate::cost_estimates::metrics::UnitMetric; + use crate::cost_estimates::tests::fee_rate_fuzzer::ConstantFeeEstimator; + use crate::cost_estimates::UnitEstimator; use crate::net::asn::*; use crate::net::atlas::*; use crate::net::chat::*; @@ -2287,7 +2290,7 @@ pub mod test { use crate::net::p2p::*; use crate::net::poll::*; use crate::net::relay::*; - use crate::net::Error as net_error; + use crate::net::{Error as net_error, ProtocolFamily, TipRequest}; use crate::util_lib::boot::boot_code_test_addr; use crate::util_lib::strings::*; @@ -2552,6 +2555,75 @@ pub mod test { } } + const DEFAULT_RPC_HANDLER_ARGS: RPCHandlerArgs<'static> = RPCHandlerArgs { + exit_at_block_height: None, + genesis_chainstate_hash: Sha256Sum([0x00; 32]), + event_observer: None, + cost_estimator: None, + fee_estimator: None, + cost_metric: None, + coord_comms: None, + }; + + const NULL_COST_ESTIMATOR: () = (); + const NULL_FEE_ESTIMATOR: () = (); + const NULL_COST_METRIC: UnitMetric = UnitMetric {}; + const NULL_RPC_HANDLER_ARGS: RPCHandlerArgs<'static> = RPCHandlerArgs { + exit_at_block_height: None, + genesis_chainstate_hash: Sha256Sum([0x00; 32]), + event_observer: None, + cost_estimator: Some(&NULL_COST_ESTIMATOR), + fee_estimator: Some(&NULL_FEE_ESTIMATOR), + cost_metric: Some(&NULL_COST_METRIC), + coord_comms: None, + }; + + const UNIT_COST_ESTIMATOR: UnitEstimator = UnitEstimator {}; + const CONSTANT_FEE_ESTIMATOR: ConstantFeeEstimator = ConstantFeeEstimator {}; + const UNIT_COST_METRIC: UnitMetric = UnitMetric {}; + const UNIT_RPC_HANDLER_ARGS: RPCHandlerArgs<'static> = RPCHandlerArgs { + exit_at_block_height: None, + genesis_chainstate_hash: Sha256Sum([0x00; 32]), + event_observer: None, + cost_estimator: Some(&UNIT_COST_ESTIMATOR), + fee_estimator: Some(&CONSTANT_FEE_ESTIMATOR), + cost_metric: Some(&UNIT_COST_METRIC), + coord_comms: None, + }; + + /// Templates for RPC Handler Args (which must be owned by the TestPeer, and cannot be a bare + /// RPCHandlerArgs since references to the inner members cannot be made thread-safe). + #[derive(Clone, Debug, PartialEq)] + pub enum RPCHandlerArgsType { + Default, + Null, + Unit, + } + + impl RPCHandlerArgsType { + pub fn instantiate(&self) -> RPCHandlerArgs<'static> { + match self { + Self::Default => { + debug!("Default RPC Handler Args"); + DEFAULT_RPC_HANDLER_ARGS.clone() + } + Self::Null => { + debug!("Null RPC Handler Args"); + NULL_RPC_HANDLER_ARGS.clone() + } + Self::Unit => { + debug!("Unit RPC Handler Args"); + UNIT_RPC_HANDLER_ARGS.clone() + } + } + } + + pub fn make_default() -> RPCHandlerArgs<'static> { + debug!("Default RPC Handler Args"); + DEFAULT_RPC_HANDLER_ARGS.clone() + } + } + // describes a peer's initial configuration #[derive(Debug, Clone)] pub struct TestPeerConfig { @@ -2771,6 +2843,8 @@ pub mod test { /// tenure-start block of tenure to mine on. /// gets consumed on the call to begin_nakamoto_tenure pub nakamoto_parent_tenure_opt: Option>, + /// RPC handler args to use + pub rpc_handler_args: Option, } impl<'a> TestPeer<'a> { @@ -3190,6 +3264,7 @@ pub mod test { malleablized_blocks: vec![], mine_malleablized_blocks: true, nakamoto_parent_tenure_opt: None, + rpc_handler_args: None, } } @@ -3301,6 +3376,11 @@ pub mod test { let mut stacks_node = self.stacks_node.take().unwrap(); let mut mempool = self.mempool.take().unwrap(); let indexer = self.indexer.take().unwrap(); + let rpc_handler_args = self + .rpc_handler_args + .as_ref() + .map(|args_type| args_type.instantiate()) + .unwrap_or(RPCHandlerArgsType::make_default()); let old_tip = self.network.stacks_tip.clone(); @@ -3313,14 +3393,13 @@ pub mod test { false, ibd, 100, - &RPCHandlerArgs::default(), + &rpc_handler_args, ); self.sortdb = Some(sortdb); self.stacks_node = Some(stacks_node); self.mempool = Some(mempool); self.indexer = Some(indexer); - ret } @@ -3381,7 +3460,11 @@ pub mod test { burn_tip_height, ); let indexer = BitcoinIndexer::new_unit_test(&self.config.burnchain.working_dir); - + let rpc_handler_args = self + .rpc_handler_args + .as_ref() + .map(|args_type| args_type.instantiate()) + .unwrap_or(RPCHandlerArgsType::make_default()); let old_tip = self.network.stacks_tip.clone(); let ret = self.network.run( @@ -3393,7 +3476,7 @@ pub mod test { false, ibd, 100, - &RPCHandlerArgs::default(), + &rpc_handler_args, ); self.sortdb = Some(sortdb); From b4af50df9fac23336c90c5f8dcc772a4363d1a25 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 4 Feb 2025 13:49:14 -0500 Subject: [PATCH 22/27] chore: address PR feedback and add changelog entry --- CHANGELOG.md | 1 + stackslib/src/net/api/tests/postfeerate.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1644446dd7..a5bf7d7137 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE - Miners who restart their nodes immediately before a winning tenure now correctly detect that they won the tenure after their nodes restart ([#5750](https://github.com/stacks-network/stacks-core/issues/5750)). +- Error responses to /v2/transactions/fees are once again expressed as JSON ([#4145](https://github.com/stacks-network/stacks-core/issues/4145)). ## [3.1.0.0.4] diff --git a/stackslib/src/net/api/tests/postfeerate.rs b/stackslib/src/net/api/tests/postfeerate.rs index 753684d483..85ba4feb6f 100644 --- a/stackslib/src/net/api/tests/postfeerate.rs +++ b/stackslib/src/net/api/tests/postfeerate.rs @@ -122,7 +122,7 @@ fn test_try_make_response() { assert_eq!(preamble.status_code, 400); debug!("Response JSON no estimator: {}", &body_json); - // case 2: no estimate avaialable + // case 2: no estimate available let mut requests = vec![]; let request = StacksHttpRequest::new_post_fee_rate( addr.clone().into(), From 3420de365d1f5171e2f4da68f248afcfce49b622 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 4 Feb 2025 13:59:34 -0500 Subject: [PATCH 23/27] chore: honor config option to force nakamoto state machine transition --- stackslib/src/net/p2p.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/stackslib/src/net/p2p.rs b/stackslib/src/net/p2p.rs index 26d5fd0fb4..4c39bf32bd 100644 --- a/stackslib/src/net/p2p.rs +++ b/stackslib/src/net/p2p.rs @@ -3623,7 +3623,9 @@ impl PeerNetwork { // in Nakamoto epoch, but we might still be doing epoch 2.x things since Nakamoto does // not begin on a reward cycle boundary. - if self.need_epoch2_state_machines(cur_epoch.epoch_id) { + if self.need_epoch2_state_machines(cur_epoch.epoch_id) + || self.connection_opts.force_nakamoto_epoch_transition + { debug!( "{:?}: run Epoch 2.x work loop in Nakamoto epoch", self.get_local_peer() From 0915796360ff162e9aa1173d115b814f0a34dc4d Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 4 Feb 2025 20:45:43 -0500 Subject: [PATCH 24/27] chore: fix failing unit test --- stackslib/src/net/mod.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/stackslib/src/net/mod.rs b/stackslib/src/net/mod.rs index 2a8cf7f523..986352c42c 100644 --- a/stackslib/src/net/mod.rs +++ b/stackslib/src/net/mod.rs @@ -3325,7 +3325,9 @@ pub mod test { self.network.nakamoto_state_machine_passes, nakamoto_passes + 1 ); - let epoch2_expected_passes = if self.network.stacks_tip.is_nakamoto { + let epoch2_expected_passes = if self.network.stacks_tip.is_nakamoto + && !self.network.connection_opts.force_nakamoto_epoch_transition + { epoch2_passes } else { epoch2_passes + 1 @@ -3431,7 +3433,9 @@ pub mod test { self.network.nakamoto_state_machine_passes, nakamoto_passes + 1 ); - let epoch2_expected_passes = if self.network.stacks_tip.is_nakamoto { + let epoch2_expected_passes = if self.network.stacks_tip.is_nakamoto + && !self.network.connection_opts.force_nakamoto_epoch_transition + { epoch2_passes } else { epoch2_passes + 1 From 8ad0a2bce397ee53beaa8e7a5c087cc29cf31017 Mon Sep 17 00:00:00 2001 From: Jude Nelson Date: Tue, 4 Feb 2025 20:47:15 -0500 Subject: [PATCH 25/27] chore: address merge artifact in CHANGELOG --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e08b1b5a4..9fb0772933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE ### Fixed +- Error responses to /v2/transactions/fees are once again expressed as JSON ([#4145](https://github.com/stacks-network/stacks-core/issues/4145)). + ## [3.1.0.0.5] ### Added @@ -29,7 +31,6 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE - Miners who restart their nodes immediately before a winning tenure now correctly detect that they won the tenure after their nodes restart ([#5750](https://github.com/stacks-network/stacks-core/issues/5750)). -- Error responses to /v2/transactions/fees are once again expressed as JSON ([#4145](https://github.com/stacks-network/stacks-core/issues/4145)). ## [3.1.0.0.4] From 1057679b05a87cca11973c06d6befe3256e786b2 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 5 Feb 2025 10:47:49 -0600 Subject: [PATCH 26/27] test: improve flake by reducing only producing one btc block without checking for stacks blocks --- testnet/stacks-node/src/tests/nakamoto_integrations.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 4099ce64f2..aa98575eb9 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -10042,9 +10042,7 @@ fn test_shadow_recovery() { info!("Beginning post-shadow tenures"); // revive ATC-C by waiting for commits - for _i in 0..4 { - next_block_and_commits_only(btc_regtest_controller, 60, &naka_conf, &counters).unwrap(); - } + next_block_and_commits_only(btc_regtest_controller, 60, &naka_conf, &counters).unwrap(); // make another tenure next_block_and_mine_commit(btc_regtest_controller, 60, &naka_conf, &counters).unwrap(); From ac5957df80e385b31a0af1c2e9866ca58e8c4474 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Wed, 5 Feb 2025 11:00:35 -0600 Subject: [PATCH 27/27] ci: disable mutants on PRs --- .github/workflows/pr-differences-mutants.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-differences-mutants.yml b/.github/workflows/pr-differences-mutants.yml index d53e2ca661..ea5ca594c2 100644 --- a/.github/workflows/pr-differences-mutants.yml +++ b/.github/workflows/pr-differences-mutants.yml @@ -1,14 +1,15 @@ name: PR Differences Mutants on: - pull_request: - types: - - opened - - reopened - - synchronize - - ready_for_review - paths: - - '**.rs' + # Disabling PR mutants (issue #5806) + # pull_request: + # types: + # - opened + # - reopened + # - synchronize + # - ready_for_review + # paths: + # - '**.rs' workflow_dispatch: concurrency: