diff --git a/stacks-signer/CHANGELOG.md b/stacks-signer/CHANGELOG.md index b0abb74a2d..45352dfad0 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 + +### Added + +- When a transaction replay set has been active for a configurable number of burn blocks (which defaults to `2`), and the replay set still hasn't been cleared, the replay set is automatically cleared. This is provided as a "failsafe" to ensure chain liveness as transaction replay is rolled out. + ## [3.1.0.0.13.0] ### Changed diff --git a/stacks-signer/src/chainstate.rs b/stacks-signer/src/chainstate.rs index 28173daa4c..a94434dcbd 100644 --- a/stacks-signer/src/chainstate.rs +++ b/stacks-signer/src/chainstate.rs @@ -143,6 +143,9 @@ pub struct ProposalEvalConfig { pub reorg_attempts_activity_timeout: Duration, /// Time to wait before submitting a block proposal to the stacks-node pub proposal_wait_for_parent_time: Duration, + /// How many blocks after a fork should we reset the replay set, + /// as a failsafe mechanism + pub reset_replay_set_after_fork_blocks: u64, } impl From<&SignerConfig> for ProposalEvalConfig { @@ -155,6 +158,7 @@ impl From<&SignerConfig> for ProposalEvalConfig { reorg_attempts_activity_timeout: value.reorg_attempts_activity_timeout, tenure_idle_timeout_buffer: value.tenure_idle_timeout_buffer, proposal_wait_for_parent_time: value.proposal_wait_for_parent_time, + reset_replay_set_after_fork_blocks: value.reset_replay_set_after_fork_blocks, } } } diff --git a/stacks-signer/src/client/mod.rs b/stacks-signer/src/client/mod.rs index ee4d3c7f1b..7cf47f28e1 100644 --- a/stacks-signer/src/client/mod.rs +++ b/stacks-signer/src/client/mod.rs @@ -432,6 +432,7 @@ pub(crate) mod tests { reorg_attempts_activity_timeout: config.reorg_attempts_activity_timeout, proposal_wait_for_parent_time: config.proposal_wait_for_parent_time, validate_with_replay_tx: config.validate_with_replay_tx, + reset_replay_set_after_fork_blocks: config.reset_replay_set_after_fork_blocks, } } diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index c7a7cd8ecc..6de3cd160d 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -49,6 +49,9 @@ const DEFAULT_TENURE_IDLE_TIMEOUT_BUFFER_SECS: u64 = 2; /// cannot determine that our stacks-node has processed the parent /// block const DEFAULT_PROPOSAL_WAIT_TIME_FOR_PARENT_SECS: u64 = 15; +/// Default number of blocks after a fork to reset the replay set, +/// as a failsafe mechanism +pub const DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS: u64 = 2; #[derive(thiserror::Error, Debug)] /// An error occurred parsing the provided configuration @@ -184,6 +187,9 @@ pub struct SignerConfig { pub proposal_wait_for_parent_time: Duration, /// Whether or not to validate blocks with replay transactions pub validate_with_replay_tx: bool, + /// How many blocks after a fork should we reset the replay set, + /// as a failsafe mechanism + pub reset_replay_set_after_fork_blocks: u64, } /// The parsed configuration for the signer @@ -237,6 +243,9 @@ pub struct GlobalConfig { pub dry_run: bool, /// Whether or not to validate blocks with replay transactions pub validate_with_replay_tx: bool, + /// How many blocks after a fork should we reset the replay set, + /// as a failsafe mechanism + pub reset_replay_set_after_fork_blocks: u64, } /// Internal struct for loading up the config file @@ -288,6 +297,9 @@ struct RawConfigFile { pub dry_run: Option, /// Whether or not to validate blocks with replay transactions pub validate_with_replay_tx: Option, + /// How many blocks after a fork should we reset the replay set, + /// as a failsafe mechanism + pub reset_replay_set_after_fork_blocks: Option, } impl RawConfigFile { @@ -413,6 +425,10 @@ impl TryFrom for GlobalConfig { // https://github.com/stacks-network/stacks-core/issues/6087 let validate_with_replay_tx = raw_data.validate_with_replay_tx.unwrap_or(false); + let reset_replay_set_after_fork_blocks = raw_data + .reset_replay_set_after_fork_blocks + .unwrap_or(DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS); + Ok(Self { node_host: raw_data.node_host, endpoint, @@ -435,6 +451,7 @@ impl TryFrom for GlobalConfig { tenure_idle_timeout_buffer, proposal_wait_for_parent_time, validate_with_replay_tx, + reset_replay_set_after_fork_blocks, }) } } @@ -714,12 +731,14 @@ network = "mainnet" auth_password = "abcd" db_path = ":memory:" validate_with_replay_tx = true +reset_replay_set_after_fork_blocks = 100 "# ); let config = GlobalConfig::load_from_str(&config_toml).unwrap(); assert_eq!(config.stacks_address.to_string(), expected_addr); assert_eq!(config.to_chain_id(), CHAIN_ID_MAINNET); assert!(config.validate_with_replay_tx); + assert_eq!(config.reset_replay_set_after_fork_blocks, 100); } #[test] diff --git a/stacks-signer/src/runloop.rs b/stacks-signer/src/runloop.rs index c6f3cf3cfe..0c4da280ec 100644 --- a/stacks-signer/src/runloop.rs +++ b/stacks-signer/src/runloop.rs @@ -329,6 +329,7 @@ impl, T: StacksMessageCodec + Clone + Send + Debug> RunLo reorg_attempts_activity_timeout: self.config.reorg_attempts_activity_timeout, proposal_wait_for_parent_time: self.config.proposal_wait_for_parent_time, validate_with_replay_tx: self.config.validate_with_replay_tx, + reset_replay_set_after_fork_blocks: self.config.reset_replay_set_after_fork_blocks, })) } diff --git a/stacks-signer/src/tests/chainstate.rs b/stacks-signer/src/tests/chainstate.rs index 4569343ea1..f3520e6b66 100644 --- a/stacks-signer/src/tests/chainstate.rs +++ b/stacks-signer/src/tests/chainstate.rs @@ -44,6 +44,7 @@ use stacks_common::util::secp256k1::MessageSignature; use crate::chainstate::{ProposalEvalConfig, SortitionMinerStatus, SortitionState, SortitionsView}; use crate::client::tests::MockServerClient; use crate::client::StacksClient; +use crate::config::DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS; use crate::signerdb::{BlockInfo, SignerDb}; fn setup_test_environment( @@ -92,6 +93,7 @@ fn setup_test_environment( tenure_idle_timeout_buffer: Duration::from_secs(2), reorg_attempts_activity_timeout: Duration::from_secs(3), proposal_wait_for_parent_time: Duration::from_secs(0), + reset_replay_set_after_fork_blocks: DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS, }, }; diff --git a/stacks-signer/src/v0/signer.rs b/stacks-signer/src/v0/signer.rs index 7b200b3821..4fb66e14aa 100644 --- a/stacks-signer/src/v0/signer.rs +++ b/stacks-signer/src/v0/signer.rs @@ -127,6 +127,8 @@ pub struct Signer { pub validate_with_replay_tx: bool, /// Scope of Tx Replay in terms of Burn block boundaries pub tx_replay_scope: ReplayScopeOpt, + /// The number of blocks after the past tip to reset the replay set + pub reset_replay_set_after_fork_blocks: u64, } impl std::fmt::Display for SignerMode { @@ -244,6 +246,7 @@ impl SignerTrait for Signer { global_state_evaluator, validate_with_replay_tx: signer_config.validate_with_replay_tx, tx_replay_scope: None, + reset_replay_set_after_fork_blocks: signer_config.reset_replay_set_after_fork_blocks, } } diff --git a/stacks-signer/src/v0/signer_state.rs b/stacks-signer/src/v0/signer_state.rs index fb5e13776a..1083d897e2 100644 --- a/stacks-signer/src/v0/signer_state.rs +++ b/stacks-signer/src/v0/signer_state.rs @@ -21,6 +21,7 @@ use std::time::{Duration, UNIX_EPOCH}; use blockstack_lib::chainstate::burn::ConsensusHashExtensions; use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader}; use blockstack_lib::chainstate::stacks::{StacksTransaction, TransactionPayload}; +use blockstack_lib::net::api::get_tenures_fork_info::TenureForkingInfo; use blockstack_lib::net::api::postblock_proposal::NakamotoBlockProposal; use clarity::types::chainstate::StacksAddress; #[cfg(any(test, feature = "testing"))] @@ -594,9 +595,11 @@ impl LocalStateMachine { && next_burn_block_hash != expected_burn_block.consensus_hash; if node_behind_expected || node_on_equal_fork { let err_msg = format!( - "Node has not processed the next burn block yet. Expected height = {}, Expected consensus hash = {}", + "Node has not processed the next burn block yet. Expected height = {}, Expected consensus hash = {}, Node height = {}, Node consensus hash = {}", expected_burn_block.burn_block_height, expected_burn_block.consensus_hash, + next_burn_block_height, + next_burn_block_hash, ); *self = Self::Pending { update: StateMachineUpdate::BurnBlock(expected_burn_block), @@ -620,7 +623,7 @@ impl LocalStateMachine { client, &expected_burn_block, &prior_state_machine, - replay_state, + &replay_state, )? { match new_replay_state { ReplayState::Unset => { @@ -632,6 +635,17 @@ impl LocalStateMachine { *tx_replay_scope = Some(new_scope); } } + } else if Self::handle_possible_replay_failsafe( + &replay_state, + &expected_burn_block, + proposal_config.reset_replay_set_after_fork_blocks, + ) { + info!( + "Signer state: replay set is stalled after {} tenures. Clearing the replay set.", + proposal_config.reset_replay_set_after_fork_blocks + ); + tx_replay_set = ReplayTransactionSet::none(); + *tx_replay_scope = None; } } @@ -981,11 +995,24 @@ impl LocalStateMachine { client: &StacksClient, expected_burn_block: &NewBurnBlock, prior_state_machine: &SignerStateMachine, - replay_state: ReplayState, + replay_state: &ReplayState, ) -> Result, SignerChainstateError> { if expected_burn_block.burn_block_height > prior_state_machine.burn_block_height { - // no bitcoin fork, because we're advancing the burn block height - return Ok(None); + if Self::new_burn_block_fork_descendency_check( + db, + expected_burn_block, + prior_state_machine.burn_block_height, + prior_state_machine.burn_block, + )? { + info!("Detected bitcoin fork - prior tip is not parent of new tip."; + "new_tip.burn_block_height" => expected_burn_block.burn_block_height, + "new_tip.consensus_hash" => %expected_burn_block.consensus_hash, + "prior_tip.burn_block_height" => prior_state_machine.burn_block_height, + "prior_tip.consensus_hash" => %prior_state_machine.burn_block, + ); + } else { + return Ok(None); + } } if expected_burn_block.consensus_hash == prior_state_machine.burn_block { // no bitcoin fork, because we're at the same burn block hash as before @@ -1088,7 +1115,7 @@ impl LocalStateMachine { client: &StacksClient, expected_burn_block: &NewBurnBlock, prior_state_machine: &SignerStateMachine, - scope: ReplayScope, + scope: &ReplayScope, ) -> Result, SignerChainstateError> { info!("Tx Replay: detected bitcoin fork while in replay mode. Tryng to handle the fork"; "expected_burn_block.height" => expected_burn_block.burn_block_height, @@ -1182,6 +1209,10 @@ impl LocalStateMachine { return Ok(None); } + Ok(Some(Self::get_forked_txs_from_fork_info(&fork_info))) + } + + fn get_forked_txs_from_fork_info(fork_info: &[TenureForkingInfo]) -> Vec { // Collect transactions to be replayed across the forked blocks let mut forked_blocks = fork_info .iter() @@ -1201,6 +1232,83 @@ impl LocalStateMachine { )) .cloned() .collect::>(); - Ok(Some(forked_txs)) + forked_txs + } + + /// If it has been `reset_replay_set_after_fork_blocks` burn blocks since the origin of our replay set, and + /// we haven't produced any replay blocks since then, we should reset our replay set + /// + /// Returns a `bool` indicating whether the replay set should be reset. + fn handle_possible_replay_failsafe( + replay_state: &ReplayState, + new_burn_block: &NewBurnBlock, + reset_replay_set_after_fork_blocks: u64, + ) -> bool { + match replay_state { + ReplayState::Unset => { + // not in replay - skip + false + } + ReplayState::InProgress(_, replay_scope) => { + let failsafe_height = + replay_scope.past_tip.burn_block_height + reset_replay_set_after_fork_blocks; + new_burn_block.burn_block_height > failsafe_height + } + } + } + + /// Check if the new burn block is a fork, by checking if the new burn block + /// is a descendant of the prior burn block + fn new_burn_block_fork_descendency_check( + db: &SignerDb, + new_burn_block: &NewBurnBlock, + prior_burn_block_height: u64, + prior_burn_block_ch: ConsensusHash, + ) -> Result { + let max_height_delta = 10; + let height_delta = match new_burn_block + .burn_block_height + .checked_sub(prior_burn_block_height) + { + None | Some(0) => return Ok(false), // same height or older + Some(d) if d > max_height_delta => return Ok(false), // too far apart + Some(d) => d, + }; + + let mut parent_burn_block_info = match db + .get_burn_block_by_ch(&new_burn_block.consensus_hash) + .and_then(|burn_block_info| { + db.get_burn_block_by_hash(&burn_block_info.parent_burn_block_hash) + }) { + Ok(info) => info, + Err(e) => { + warn!( + "Failed to get parent burn block info for {}", + new_burn_block.consensus_hash; + "error" => ?e, + ); + return Ok(false); + } + }; + + for _ in 0..height_delta { + if parent_burn_block_info.block_height == prior_burn_block_height { + return Ok(parent_burn_block_info.consensus_hash != prior_burn_block_ch); + } + + parent_burn_block_info = + match db.get_burn_block_by_hash(&parent_burn_block_info.parent_burn_block_hash) { + Ok(bi) => bi, + Err(e) => { + warn!( + "Failed to get parent burn block info for {}. Error: {e}", + parent_burn_block_info.parent_burn_block_hash + ); + return Ok(false); + } + }; + } + + Ok(false) } } diff --git a/stackslib/src/net/api/postblock_proposal.rs b/stackslib/src/net/api/postblock_proposal.rs index 9cddbbedf0..c95d9e92c4 100644 --- a/stackslib/src/net/api/postblock_proposal.rs +++ b/stackslib/src/net/api/postblock_proposal.rs @@ -68,6 +68,10 @@ pub static TEST_REPLAY_TRANSACTIONS: LazyLock< TestFlag>, > = LazyLock::new(TestFlag::default); +#[cfg(any(test, feature = "testing"))] +/// Whether to reject any transaction while we're in a replay set. +pub static TEST_REJECT_REPLAY_TXS: LazyLock> = LazyLock::new(TestFlag::default); + // This enum is used to supply a `reason_code` for validation // rejection responses. This is serialized as an enum with string // type (in jsonschema terminology). @@ -200,6 +204,24 @@ fn fault_injection_validation_delay() { #[cfg(not(any(test, feature = "testing")))] fn fault_injection_validation_delay() {} +#[cfg(any(test, feature = "testing"))] +fn fault_injection_reject_replay_txs() -> Result<(), BlockValidateRejectReason> { + let reject = TEST_REJECT_REPLAY_TXS.get(); + if reject { + Err(BlockValidateRejectReason { + reason_code: ValidateRejectCode::InvalidTransactionReplay, + reason: "Rejected by test flag".into(), + }) + } else { + Ok(()) + } +} + +#[cfg(not(any(test, feature = "testing")))] +fn fault_injection_reject_replay_txs() -> Result<(), BlockValidateRejectReason> { + Ok(()) +} + /// Represents a block proposed to the `v3/block_proposal` endpoint for validation #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct NakamotoBlockProposal { @@ -723,6 +745,7 @@ impl NakamotoBlockProposal { // Allow this to happen, tenure extend checks happen elsewhere. break; } + fault_injection_reject_replay_txs()?; let Some(replay_tx) = replay_txs.pop_front() else { // During transaction replay, we expect that the block only // contains transactions from the replay set. Thus, if we're here, diff --git a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs index 16742f4fb8..98c58ea364 100644 --- a/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs +++ b/testnet/stacks-node/src/burnchains/bitcoin_regtest_controller.rs @@ -2816,6 +2816,17 @@ impl BitcoinRPCRequest { BitcoinRPCRequest::send(config, payload) } + pub fn get_chain_tips(config: &Config) -> RPCResult { + let payload = BitcoinRPCRequest { + method: "getchaintips".to_string(), + params: vec![], + id: "stacks".to_string(), + jsonrpc: "2.0".to_string(), + }; + + BitcoinRPCRequest::send(config, payload) + } + pub fn send(config: &Config, payload: BitcoinRPCRequest) -> RPCResult { let request = BitcoinRPCRequest::build_rpc_request(config, &payload); let timeout = Duration::from_secs(u64::from(config.burnchain.timeout)); diff --git a/testnet/stacks-node/src/tests/nakamoto_integrations.rs b/testnet/stacks-node/src/tests/nakamoto_integrations.rs index 6adc8255e3..95276619ab 100644 --- a/testnet/stacks-node/src/tests/nakamoto_integrations.rs +++ b/testnet/stacks-node/src/tests/nakamoto_integrations.rs @@ -103,6 +103,7 @@ use stacks_common::util::hash::{to_hex, Hash160, Sha512Trunc256Sum}; use stacks_common::util::secp256k1::{MessageSignature, Secp256k1PrivateKey, Secp256k1PublicKey}; use stacks_common::util::{get_epoch_time_secs, sleep_ms}; use stacks_signer::chainstate::{ProposalEvalConfig, SortitionsView}; +use stacks_signer::config::DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS; use stacks_signer::signerdb::{BlockInfo, BlockState, ExtraBlockInfo, SignerDb}; use stacks_signer::v0::SpawnedSigner; @@ -6589,6 +6590,7 @@ fn signer_chainstate() { tenure_idle_timeout: Duration::from_secs(300), tenure_idle_timeout_buffer: Duration::from_secs(2), reorg_attempts_activity_timeout: Duration::from_secs(30), + reset_replay_set_after_fork_blocks: DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS, }; let mut sortitions_view = SortitionsView::fetch_view(proposal_conf, &signer_client).unwrap(); @@ -6716,6 +6718,7 @@ fn signer_chainstate() { tenure_idle_timeout: Duration::from_secs(300), tenure_idle_timeout_buffer: Duration::from_secs(2), reorg_attempts_activity_timeout: Duration::from_secs(30), + reset_replay_set_after_fork_blocks: DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS, }; let burn_block_height = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) .unwrap() @@ -6794,6 +6797,7 @@ fn signer_chainstate() { tenure_idle_timeout: Duration::from_secs(300), tenure_idle_timeout_buffer: Duration::from_secs(2), reorg_attempts_activity_timeout: Duration::from_secs(30), + reset_replay_set_after_fork_blocks: DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS, }; let mut sortitions_view = SortitionsView::fetch_view(proposal_conf, &signer_client).unwrap(); sortitions_view diff --git a/testnet/stacks-node/src/tests/signer/mod.rs b/testnet/stacks-node/src/tests/signer/mod.rs index 774fedc421..d57d28c41a 100644 --- a/testnet/stacks-node/src/tests/signer/mod.rs +++ b/testnet/stacks-node/src/tests/signer/mod.rs @@ -32,6 +32,7 @@ use libsigner::v0::messages::{ use libsigner::v0::signer_state::MinerState; use libsigner::{BlockProposal, SignerEntries, SignerEventTrait}; use serde::{Deserialize, Serialize}; +use stacks::burnchains::Txid; use stacks::chainstate::coordinator::comm::CoordinatorChannels; use stacks::chainstate::nakamoto::signer_set::NakamotoSigners; use stacks::chainstate::nakamoto::NakamotoBlock; @@ -192,6 +193,9 @@ impl + Send + 'static, T: SignerEventTrait + 'static> SignerTest + Send + 'static, T: SignerEventTrait + 'static> SignerTest( File::open(metadata_path.clone()).unwrap(), @@ -1023,6 +1031,20 @@ impl + Send + 'static, T: SignerEventTrait + 'static> SignerTest) { + self.wait_for_signer_state_check(timeout, |state| { + let Some(replay_set) = state.get_tx_replay_set() else { + return Ok(false); + }; + let txids = replay_set + .iter() + .map(|tx| tx.txid().to_hex()) + .collect::>(); + Ok(txids == expected_txids) + }) + .expect("Timed out waiting for replay set to be equal to expected txids"); + } + /// Replace the test's configured signer st pub fn replace_signers( &mut self, @@ -1509,6 +1531,31 @@ impl + Send + 'static, T: SignerEventTrait + 'static> SignerTest(accepted.into()) .expect("Failed to send accept signature"); } + + /// Get the txid of the parent block commit transaction for the given miner + pub fn get_parent_block_commit_txid(&self, miner_pk: &StacksPublicKey) -> Option { + let Some(confirmed_utxo) = self + .running_nodes + .btc_regtest_controller + .get_all_utxos(&miner_pk) + .into_iter() + .find(|utxo| utxo.confirmations == 0) + else { + return None; + }; + let unconfirmed_txid = Txid::from_bitcoin_tx_hash(&confirmed_utxo.txid); + let unconfirmed_tx = self + .running_nodes + .btc_regtest_controller + .get_raw_transaction(&unconfirmed_txid); + let parent_txid = unconfirmed_tx + .input + .get(0) + .expect("First input should exist") + .previous_output + .txid; + Some(Txid::from_bitcoin_tx_hash(&parent_txid)) + } } fn setup_stx_btc_node( diff --git a/testnet/stacks-node/src/tests/signer/v0.rs b/testnet/stacks-node/src/tests/signer/v0.rs index b04a4144e0..fdd921b72f 100644 --- a/testnet/stacks-node/src/tests/signer/v0.rs +++ b/testnet/stacks-node/src/tests/signer/v0.rs @@ -64,8 +64,8 @@ use stacks::core::{StacksEpochId, CHAIN_ID_TESTNET, HELIUM_BLOCK_LIMIT_20}; use stacks::libstackerdb::StackerDBChunkData; use stacks::net::api::getsigner::GetSignerResponse; use stacks::net::api::postblock_proposal::{ - BlockValidateResponse, ValidateRejectCode, TEST_VALIDATE_DELAY_DURATION_SECS, - TEST_VALIDATE_STALL, + BlockValidateResponse, ValidateRejectCode, TEST_REJECT_REPLAY_TXS, + TEST_VALIDATE_DELAY_DURATION_SECS, TEST_VALIDATE_STALL, }; use stacks::net::relay::fault_injection::{clear_ignore_block, set_ignore_block}; use stacks::types::chainstate::{ @@ -85,7 +85,10 @@ use stacks_common::types::chainstate::TrieHash; use stacks_common::util::sleep_ms; use stacks_signer::chainstate::{ProposalEvalConfig, SortitionsView}; use stacks_signer::client::StackerDB; -use stacks_signer::config::{build_signer_config_tomls, GlobalConfig as SignerConfig, Network}; +use stacks_signer::config::{ + build_signer_config_tomls, GlobalConfig as SignerConfig, Network, + DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS, +}; use stacks_signer::signerdb::SignerDb; use stacks_signer::v0::signer::TEST_REPEAT_PROPOSAL_RESPONSE; use stacks_signer::v0::signer_state::{ @@ -101,6 +104,7 @@ use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; use super::SignerTest; +use crate::burnchains::bitcoin_regtest_controller::BitcoinRPCRequest; use crate::event_dispatcher::{ EventObserver, MinedNakamotoBlockEvent, TEST_SKIP_BLOCK_ANNOUNCEMENT, }; @@ -291,11 +295,11 @@ impl SignerTest { Ok(self .stacks_client .get_reward_set_signers(reward_cycle) - .expect("Failed to check if reward set is calculated") - .map(|reward_set| { + .and_then(|reward_set| { debug!("Signer set: {reward_set:?}"); + Ok(reward_set.is_some()) }) - .is_some()) + .unwrap_or(false)) }) .expect("Timed out waiting for reward set calculation"); info!("Signer set calculated"); @@ -1602,6 +1606,7 @@ fn block_proposal_rejection() { tenure_idle_timeout: Duration::from_secs(300), tenure_idle_timeout_buffer: Duration::from_secs(2), reorg_attempts_activity_timeout: Duration::from_secs(30), + reset_replay_set_after_fork_blocks: DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS, }; let mut block = NakamotoBlock { header: NakamotoBlockHeader::empty(), @@ -3106,37 +3111,46 @@ fn tx_replay_forking_test() { } let num_signers = 5; - let sender_sk = Secp256k1PrivateKey::random(); + let sender_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes()); let sender_addr = tests::to_addr(&sender_sk); let send_amt = 100; let send_fee = 180; let deploy_fee = 1000000; let call_fee = 1000; - let signer_test: SignerTest = SignerTest::new_with_config_modifications( - num_signers, - vec![( - sender_addr, - (send_amt + send_fee) * 10 + deploy_fee + call_fee, - )], - |c| { - c.validate_with_replay_tx = true; - }, - |node_config| { - node_config.miner.block_commit_delay = Duration::from_secs(1); - node_config.miner.replay_transactions = true; - }, - None, - None, - ); + let signer_test: SignerTest = + SignerTest::new_with_config_modifications_and_snapshot( + num_signers, + vec![( + sender_addr, + (send_amt + send_fee) * 10 + deploy_fee + call_fee, + )], + |c| { + c.validate_with_replay_tx = true; + }, + |node_config| { + node_config.miner.block_commit_delay = Duration::from_secs(1); + node_config.miner.replay_transactions = true; + node_config.miner.activated_vrf_key_path = + Some(format!("{}/vrf_key", node_config.node.working_dir)); + }, + None, + None, + Some(function_name!()), + ); let conf = &signer_test.running_nodes.conf; let http_origin = format!("http://{}", &conf.node.rpc_bind); let stacks_miner_pk = StacksPublicKey::from_private(&conf.miner.mining_key.unwrap()); let btc_controller = &signer_test.running_nodes.btc_regtest_controller; - signer_test.boot_to_epoch_3(); - info!("------------------------- Reached Epoch 3.0 -------------------------"); - let pre_fork_tenures = 10; + if signer_test.bootstrap_snapshot() { + signer_test.shutdown_and_snapshot(); + return; + } + + info!("------------------------- Beginning test -------------------------"); + + let pre_fork_tenures = 2; for i in 0..pre_fork_tenures { info!("Mining pre-fork tenure {} of {pre_fork_tenures}", i + 1); @@ -3162,49 +3176,24 @@ fn tx_replay_forking_test() { info!("------------------------- Triggering Bitcoin Fork -------------------------"); - let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height - 2); + let tip_before = signer_test.get_peer_info(); + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); - - // note, we should still have normal signer states! - signer_test.check_signer_states_normal(); - - info!("Wait for block off of shallow fork"); - TEST_MINE_STALL.set(true); + btc_controller.build_next_block(2); - let submitted_commits = signer_test - .running_nodes - .counters - .naka_submitted_commits - .clone(); + signer_test.wait_for_replay_set_eq(30, vec![txid.clone()]); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } + btc_controller.build_next_block(1); + wait_for(30, || { + let tip = signer_test.get_peer_info(); + Ok(tip.stacks_tip_height < tip_before.stacks_tip_height) + }) + .expect("Timed out waiting for stacks tip to decrease"); let post_fork_1_nonce = get_account(&http_origin, &sender_addr).nonce; - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 1; - let txid_ok = tx_replay_set[0].txid().to_hex() == txid; - Ok(len_ok && txid_ok) - }) - .expect("Timed out waiting for tx replay set"); + signer_test.wait_for_replay_set_eq(30, vec![txid.clone()]); // We should have forked 1 tx assert_eq!(post_fork_1_nonce, pre_fork_1_nonce - 1); @@ -3264,39 +3253,23 @@ fn tx_replay_forking_test() { TEST_MINE_STALL.set(true); + info!("---- Triggering deeper fork ----"); + + let tip_before = signer_test.get_peer_info(); + let burn_header_hash_to_fork = btc_controller.get_block_hash(pre_fork_2_tip.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); + btc_controller.build_next_block(4); - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and( - &signer_test.running_nodes.btc_regtest_controller, - 60, - || Ok(submitted_commits.load(Ordering::SeqCst) > commits_count), - ) - .unwrap(); - } + wait_for(30, || { + let tip = signer_test.get_peer_info(); + Ok(tip.stacks_tip_height < tip_before.stacks_tip_height) + }) + .expect("Timed out waiting for stacks tip to decrease"); let expected_tx_replay_txids = vec![transfer_txid, contract_deploy_txid, contract_call_txid]; - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let tx_replay_set_txids = tx_replay_set - .iter() - .map(|tx| tx.txid().to_hex()) - .collect::>(); - Ok(tx_replay_set_txids == expected_tx_replay_txids) - }) - .expect("Timed out waiting for tx replay set to be updated"); + signer_test.wait_for_replay_set_eq(30, expected_tx_replay_txids.clone()); info!("---- Mining post-fork block to clear tx replay set ----"); let tip_after_fork = get_chain_info(&conf); @@ -3372,37 +3345,46 @@ fn tx_replay_reject_invalid_proposals_during_replay() { } let num_signers = 5; - let sender_sk = Secp256k1PrivateKey::random(); + let sender_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes()); let sender_addr = tests::to_addr(&sender_sk); - let sender_sk2 = Secp256k1PrivateKey::random(); + let sender_sk2 = Secp256k1PrivateKey::from_seed("sender_2".as_bytes()); let sender_addr2 = tests::to_addr(&sender_sk2); let send_amt = 100; let send_fee = 180; - let signer_test: SignerTest = SignerTest::new_with_config_modifications( - num_signers, - vec![ - (sender_addr, send_amt + send_fee), - (sender_addr2, send_amt + send_fee), - ], - |c| { - c.validate_with_replay_tx = true; - }, - |node_config| { - node_config.miner.block_commit_delay = Duration::from_secs(1); - node_config.miner.replay_transactions = true; - }, - None, - None, - ); + let signer_test: SignerTest = + SignerTest::new_with_config_modifications_and_snapshot( + num_signers, + vec![ + (sender_addr, send_amt + send_fee), + (sender_addr2, send_amt + send_fee), + ], + |c| { + c.validate_with_replay_tx = true; + }, + |node_config| { + node_config.miner.block_commit_delay = Duration::from_secs(1); + node_config.miner.replay_transactions = true; + node_config.miner.activated_vrf_key_path = + Some(format!("{}/vrf_key", node_config.node.working_dir)); + }, + None, + None, + Some(function_name!()), + ); let conf = &signer_test.running_nodes.conf; let http_origin = format!("http://{}", &conf.node.rpc_bind); let btc_controller = &signer_test.running_nodes.btc_regtest_controller; let stacks_miner_pk = StacksPublicKey::from_private(&conf.miner.mining_key.unwrap()); - signer_test.boot_to_epoch_3(); - info!("------------------------- Reached Epoch 3.0 -------------------------"); - let pre_fork_tenures = 10; + if signer_test.bootstrap_snapshot() { + signer_test.shutdown_and_snapshot(); + return; + } + + info!("------------------------- Beginning test -------------------------"); + + let pre_fork_tenures = 2; for i in 0..pre_fork_tenures { info!("Mining pre-fork tenure {} of {pre_fork_tenures}", i + 1); @@ -3426,50 +3408,15 @@ fn tx_replay_reject_invalid_proposals_during_replay() { info!("------------------------- Triggering Bitcoin Fork -------------------------"); - let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height - 2); + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); - - // note, we should still have normal signer states! - signer_test.check_signer_states_normal(); - - info!("Wait for block off of shallow fork"); - TEST_MINE_STALL.set(true); + btc_controller.build_next_block(2); - let submitted_commits = signer_test - .running_nodes - .counters - .naka_submitted_commits - .clone(); - - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } + signer_test.wait_for_replay_set_eq(30, vec![txid.clone()]); let post_fork_1_nonce = get_account(&http_origin, &sender_addr).nonce; - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 1; - let txid_ok = tx_replay_set[0].txid().to_hex() == txid; - Ok(len_ok && txid_ok) - }) - .expect("Timed out waiting for tx replay set to be updated"); - // We should have forked 1 tx assert_eq!(post_fork_1_nonce, pre_fork_1_nonce - 1); @@ -3612,6 +3559,7 @@ fn tx_replay_btc_on_stx_invalidation() { vec![(sender_addr, (send_amt + send_fee) * 10)], |c| { c.validate_with_replay_tx = true; + c.reset_replay_set_after_fork_blocks = 5; }, |node_config| { node_config.miner.block_commit_delay = Duration::from_secs(1); @@ -3675,7 +3623,7 @@ fn tx_replay_btc_on_stx_invalidation() { "Pre-stx operation should submit successfully" ); - let pre_fork_tenures = 9; + let pre_fork_tenures = 10; for i in 0..pre_fork_tenures { info!("Mining pre-fork tenure {} of {pre_fork_tenures}", i + 1); signer_test.mine_nakamoto_block(Duration::from_secs(30), true); @@ -3782,6 +3730,9 @@ fn tx_replay_btc_on_stx_invalidation() { }) .expect("Timed out waiting for block to advance by 1"); + let account = get_account(&_http_origin, &recipient_addr); + assert_eq!(account.nonce, 0, "Expected recipient nonce to be 0"); + let blocks = test_observer::get_blocks(); let block: StacksBlockEvent = serde_json::from_value(blocks.last().unwrap().clone()).expect("Failed to parse block"); @@ -3801,62 +3752,361 @@ fn tx_replay_btc_on_stx_invalidation() { signer_test.shutdown(); } -/// Test scenario where two signers disagree on the tx replay set, -/// which means there is no consensus on the tx replay set. -#[test] +/// Test scenario to ensure that the replay set is cleared +/// if there have been multiple tenures with a stalled replay set. +/// +/// This test is executed by triggering a fork, and then using +/// a test flag to reject any transaction replay blocks. +/// +/// The test mines a number of burn blocks during replay before +/// validating that the replay set is eventually cleared. #[ignore] -fn tx_replay_disagreement() { +#[test] +fn tx_replay_failsafe() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; } let num_signers = 5; - let mut miners = MultipleMinerTest::new_with_config_modifications( - num_signers, - 10, - |config| { - config.validate_with_replay_tx = true; - }, - |config| { - config.burnchain.pox_reward_length = Some(30); - config.miner.block_commit_delay = Duration::from_secs(0); - config.miner.tenure_cost_limit_per_block_percentage = None; - config.miner.replay_transactions = true; - }, - |config| { - config.burnchain.pox_reward_length = Some(30); - config.miner.block_commit_delay = Duration::from_secs(0); - config.miner.tenure_cost_limit_per_block_percentage = None; - config.miner.replay_transactions = true; - }, - ); + let sender_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes()); + let sender_addr = tests::to_addr(&sender_sk); + let send_amt = 100; + let send_fee = 180; + let signer_test: SignerTest = + SignerTest::new_with_config_modifications_and_snapshot( + num_signers, + vec![(sender_addr, (send_amt + send_fee) * 10)], + |c| { + c.validate_with_replay_tx = true; + }, + |node_config| { + node_config.miner.block_commit_delay = Duration::from_secs(1); + node_config.miner.replay_transactions = true; + node_config.miner.activated_vrf_key_path = + Some(format!("{}/vrf_key", node_config.node.working_dir)); + }, + None, + None, + Some(function_name!()), + ); - let (conf_1, _conf_2) = miners.get_node_configs(); - let _skip_commit_op_rl1 = miners - .signer_test - .running_nodes - .counters - .naka_skip_commit_op - .clone(); - let skip_commit_op_rl2 = miners.rl2_counters.naka_skip_commit_op.clone(); + let conf = &signer_test.running_nodes.conf; + let _http_origin = format!("http://{}", &conf.node.rpc_bind); + let btc_controller = &signer_test.running_nodes.btc_regtest_controller; - // Make sure that the first miner wins the first sortition. - info!("Pausing miner 2's block commit submissions"); - skip_commit_op_rl2.set(true); - miners.boot_to_epoch_3(); - let btc_controller = &miners.signer_test.running_nodes.btc_regtest_controller; + let miner_pk = btc_controller + .get_mining_pubkey() + .as_deref() + .map(Secp256k1PublicKey::from_hex) + .unwrap() + .unwrap(); - let pre_fork_tenures = 10; + if signer_test.bootstrap_snapshot() { + signer_test.shutdown_and_snapshot(); + return; + } - for i in 0..pre_fork_tenures { + info!("------------------------- Beginning test -------------------------"); + + let burnchain = conf.get_burnchain(); + + let tip = signer_test.get_peer_info(); + let pox_info = signer_test.get_pox_data(); + + info!("---- Burnchain ----"; + // "burnchain" => ?conf.burnchain, + "pox_constants" => ?burnchain.pox_constants, + "cycle" => burnchain.pox_constants.reward_cycle_index(0, tip.burn_block_height), + "pox_info" => ?pox_info, + ); + + let pre_fork_tenures = 3; + for i in 0..pre_fork_tenures { info!("Mining pre-fork tenure {} of {pre_fork_tenures}", i + 1); - miners - .signer_test - .mine_nakamoto_block(Duration::from_secs(30), false); + signer_test.mine_nakamoto_block(Duration::from_secs(30), true); } - let ignore_bitcoin_fork_keys = miners - .signer_test + info!("---- Submitting STX transfer ----"); + + let tip = get_chain_info(&conf); + // Make a transfer tx (this will get forked) + let (txid, nonce) = signer_test + .submit_transfer_tx(&sender_sk, send_fee, send_amt) + .unwrap(); + + // Ensure we got a new block with this tx + signer_test + .wait_for_nonce_increase(&sender_addr, nonce) + .expect("Timed out waiting for transfer tx to be mined"); + + wait_for(30, || { + let new_tip = get_chain_info(&conf); + Ok(new_tip.stacks_tip_height > tip.stacks_tip_height) + }) + .expect("Timed out waiting for transfer tx to be mined"); + + let tip_before = get_chain_info(&conf); + + info!("---- Triggering Bitcoin fork ----"; + "tip.stacks_tip_height" => tip_before.stacks_tip_height, + "tip.burn_block_height" => tip_before.burn_block_height, + ); + + let mut commit_txid: Option = None; + wait_for(30, || { + let Some(txid) = signer_test.get_parent_block_commit_txid(&miner_pk) else { + return Ok(false); + }; + commit_txid = Some(txid); + Ok(true) + }) + .expect("Failed to get unconfirmed tx"); + + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip_before.burn_block_height); + btc_controller.invalidate_block(&burn_header_hash_to_fork); + btc_controller.build_next_block(1); + + TEST_MINE_STALL.set(true); + + // Wait for the block commit re-broadcast to be confirmed + wait_for(10, || { + let is_confirmed = + BitcoinRPCRequest::check_transaction_confirmed(&conf, &commit_txid.unwrap()).unwrap(); + Ok(is_confirmed) + }) + .expect("Timed out waiting for transaction to be confirmed"); + + let tip_before = get_chain_info(&conf); + + info!("---- Building next block ----"; + "tip_before.stacks_tip_height" => tip_before.stacks_tip_height, + "tip_before.burn_block_height" => tip_before.burn_block_height, + ); + + btc_controller.build_next_block(1); + wait_for(30, || { + let tip = get_chain_info(&conf); + Ok(tip.stacks_tip_height < tip_before.stacks_tip_height) + }) + .expect("Timed out waiting for next block to be mined"); + + info!("---- Wait for tx replay set to be updated ----"); + + signer_test.wait_for_replay_set_eq(30, vec![txid.clone()]); + + let tip_after_fork = get_chain_info(&conf); + + info!("---- Waiting for two tenures, without replay set cleared ----"; + "tip_after_fork.stacks_tip_height" => tip_after_fork.stacks_tip_height, + "tip_after_fork.burn_block_height" => tip_after_fork.burn_block_height + ); + + TEST_REJECT_REPLAY_TXS.set(true); + TEST_MINE_STALL.set(false); + + wait_for(30, || { + let tip = get_chain_info(&conf); + Ok(tip.stacks_tip_height > tip_after_fork.stacks_tip_height) + }) + .expect("Timed out waiting for one TenureChange block to be mined"); + + signer_test + .wait_for_signer_state_check(30, |state| Ok(state.get_tx_replay_set().is_some())) + .expect("Expected replay set to still be set"); + + info!("---- Mining a second tenure ----"); + + signer_test.mine_nakamoto_block(Duration::from_secs(30), true); + + wait_for(30, || { + let tip = get_chain_info(&conf); + Ok(tip.stacks_tip_height > tip_after_fork.stacks_tip_height + 1) + }) + .expect("Timed out waiting for a TenureChange block to be mined"); + + signer_test + .wait_for_signer_state_check(30, |state| Ok(state.get_tx_replay_set().is_some())) + .expect("Expected replay set to still be set"); + + info!("---- Mining a third tenure ----"); + signer_test.mine_nakamoto_block(Duration::from_secs(30), true); + + wait_for(30, || { + let tip = get_chain_info(&conf); + Ok(tip.stacks_tip_height > tip_after_fork.stacks_tip_height + 2) + }) + .expect("Timed out waiting for a TenureChange block to be mined"); + + info!("---- Waiting for tx replay set to be cleared ----"); + + signer_test + .wait_for_signer_state_check(30, |state| Ok(state.get_tx_replay_set().is_none())) + .expect("Expected replay set to be cleared"); + + signer_test.shutdown(); +} + +/// Simple/fast test scenario for transaction replay. +/// +/// We fork one tenure, which has a STX transfer. The test +/// verifies that the replay set is updated correctly, and then +/// exits. +#[ignore] +#[test] +fn tx_replay_starts_correctly() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let num_signers = 5; + let sender_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes()); + let sender_addr = tests::to_addr(&sender_sk); + let send_amt = 100; + let send_fee = 180; + let signer_test: SignerTest = + SignerTest::new_with_config_modifications_and_snapshot( + num_signers, + vec![(sender_addr, (send_amt + send_fee) * 10)], + |c| { + c.validate_with_replay_tx = true; + }, + |node_config| { + node_config.miner.block_commit_delay = Duration::from_secs(1); + node_config.miner.replay_transactions = true; + node_config.miner.activated_vrf_key_path = + Some(format!("{}/vrf_key", node_config.node.working_dir)); + }, + None, + None, + Some(function_name!()), + ); + + let conf = &signer_test.running_nodes.conf; + let _http_origin = format!("http://{}", &conf.node.rpc_bind); + let btc_controller = &signer_test.running_nodes.btc_regtest_controller; + + if signer_test.bootstrap_snapshot() { + signer_test.shutdown_and_snapshot(); + return; + } + + info!("------------------------- Beginning test -------------------------"); + + let tip = signer_test.get_peer_info(); + + info!("---- Tip ----"; + "tip.stacks_tip_height" => tip.stacks_tip_height, + "tip.burn_block_height" => tip.burn_block_height, + ); + + let pre_fork_tenures = 1; + for i in 0..pre_fork_tenures { + info!("Mining pre-fork tenure {} of {pre_fork_tenures}", i + 1); + signer_test.mine_nakamoto_block(Duration::from_secs(30), true); + } + + info!("---- Submitting STX transfer ----"); + + // let tip = get_chain_info(&conf); + // Make a transfer tx (this will get forked) + let (txid, nonce) = signer_test + .submit_transfer_tx(&sender_sk, send_fee, send_amt) + .unwrap(); + + // Ensure we got a new block with this tx + signer_test + .wait_for_nonce_increase(&sender_addr, nonce) + .expect("Timed out waiting for transfer tx to be mined"); + + let tip_before = get_chain_info(&conf); + + info!("---- Triggering Bitcoin fork ----"; + "tip.stacks_tip_height" => tip_before.stacks_tip_height, + "tip.burn_block_height" => tip_before.burn_block_height, + "tip.consensus_hash" => %tip_before.pox_consensus, + ); + + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip_before.burn_block_height); + btc_controller.invalidate_block(&burn_header_hash_to_fork); + TEST_MINE_STALL.set(true); + btc_controller.build_next_block(2); + + wait_for(30, || { + let tip = get_chain_info(&conf); + Ok(tip.stacks_tip_height < tip_before.stacks_tip_height) + }) + .expect("Timed out waiting for next block to be mined"); + + let tip = get_chain_info(&conf); + + info!("---- Tip after fork ----"; + "tip.stacks_tip_height" => tip.stacks_tip_height, + "tip.burn_block_height" => tip.burn_block_height, + ); + + info!("---- Wait for tx replay set to be updated ----"); + + signer_test.wait_for_replay_set_eq(5, vec![txid.clone()]); + + signer_test.shutdown(); +} + +/// Test scenario where two signers disagree on the tx replay set, +/// which means there is no consensus on the tx replay set. +#[test] +#[ignore] +fn tx_replay_disagreement() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let num_signers = 5; + let sender_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes()); + let sender_addr = tests::to_addr(&sender_sk); + let send_amt = 100; + let send_fee = 180; + let signer_test: SignerTest = + SignerTest::new_with_config_modifications_and_snapshot( + num_signers, + vec![(sender_addr, (send_amt + send_fee) * 10)], + |c| { + c.validate_with_replay_tx = true; + }, + |node_config| { + node_config.miner.block_commit_delay = Duration::from_secs(1); + node_config.miner.replay_transactions = true; + }, + None, + None, + Some(function_name!()), + ); + + let conf = &signer_test.running_nodes.conf; + let _http_origin = format!("http://{}", &conf.node.rpc_bind); + let btc_controller = &signer_test.running_nodes.btc_regtest_controller; + + if signer_test.bootstrap_snapshot() { + signer_test.shutdown_and_snapshot(); + return; + } + + info!("------------------------- Beginning test -------------------------"); + + let miner_pk = btc_controller + .get_mining_pubkey() + .as_deref() + .map(Secp256k1PublicKey::from_hex) + .unwrap() + .unwrap(); + + let pre_fork_tenures = 2; + + for i in 0..pre_fork_tenures { + info!("Mining pre-fork tenure {} of {pre_fork_tenures}", i + 1); + signer_test.mine_nakamoto_block(Duration::from_secs(30), true); + } + + let ignore_bitcoin_fork_keys = signer_test .signer_stacks_private_keys .iter() .enumerate() @@ -3871,52 +4121,62 @@ fn tx_replay_disagreement() { TEST_IGNORE_BITCOIN_FORK_PUBKEYS.set(ignore_bitcoin_fork_keys); info!("------------------------- Triggering Bitcoin Fork -------------------------"); - let tip = get_chain_info(&conf_1); + let tip = get_chain_info(&conf); // Make a transfer tx (this will get forked) - let (txid, _) = miners.send_transfer_tx(); + let (txid, _) = signer_test + .submit_transfer_tx(&sender_sk, send_fee, send_amt) + .unwrap(); wait_for(30, || { - let new_tip = get_chain_info(&conf_1); + let new_tip = get_chain_info(&conf); Ok(new_tip.stacks_tip_height > tip.stacks_tip_height) }) .expect("Timed out waiting for transfer tx to be mined"); - let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height - 2); + let mut commit_txid: Option = None; + wait_for(30, || { + let Some(txid) = signer_test.get_parent_block_commit_txid(&miner_pk) else { + return Ok(false); + }; + commit_txid = Some(txid); + Ok(true) + }) + .expect("Failed to get unconfirmed tx"); + + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); + btc_controller.build_next_block(1); - // note, we should still have normal signer states! - miners.signer_test.check_signer_states_normal(); + // Wait for the block commit re-broadcast to be confirmed + wait_for(10, || { + let is_confirmed = + BitcoinRPCRequest::check_transaction_confirmed(&conf, &commit_txid.unwrap()).unwrap(); + Ok(is_confirmed) + }) + .expect("Timed out waiting for transaction to be confirmed"); - info!("Wait for block off of shallow fork"); + let tip_before = get_chain_info(&conf); - TEST_MINE_STALL.set(true); + info!("---- Building next block ----"; + "tip_before.stacks_tip_height" => tip_before.stacks_tip_height, + "tip_before.burn_block_height" => tip_before.burn_block_height, + ); - let submitted_commits = miners - .signer_test - .running_nodes - .counters - .naka_submitted_commits - .clone(); + btc_controller.build_next_block(1); + wait_for(30, || { + let tip = get_chain_info(&conf); + Ok(tip.stacks_tip_height < tip_before.stacks_tip_height) + }) + .expect("Timed out waiting for next block to be mined"); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf_1).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } + TEST_MINE_STALL.set(true); + + btc_controller.build_next_block(1); // Wait for the signer states to be updated. Odd indexed signers // should not have a replay set. wait_for(30, || { - let (signer_states, _) = miners.signer_test.get_burn_updated_states(); + let (signer_states, _) = signer_test.get_burn_updated_states(); let all_pass = signer_states.iter().enumerate().all(|(i, state)| { if i % 2 == 0 { let Some(tx_replay_set) = state.get_tx_replay_set() else { @@ -3931,27 +4191,26 @@ fn tx_replay_disagreement() { }) .expect("Timed out waiting for signer states to be updated"); - let tip = get_chain_info(&conf_1); + let tip = get_chain_info(&conf); TEST_MINE_STALL.set(false); // Now, wait for the tx replay set to be cleared wait_for(30, || { - let new_tip = get_chain_info(&conf_1); + let new_tip = get_chain_info(&conf); Ok(new_tip.stacks_tip_height >= tip.stacks_tip_height + 2) }) .expect("Timed out waiting for transfer tx to be mined"); - miners - .signer_test + signer_test .wait_for_signer_state_check(30, |state| { let tx_replay_set = state.get_tx_replay_set(); Ok(tx_replay_set.is_none()) }) .expect("Timed out waiting for tx replay set to be cleared"); - miners.shutdown(); + signer_test.shutdown(); } #[test] @@ -3963,7 +4222,6 @@ fn tx_replay_disagreement() { /// The test flow is: /// /// - Boot to Epoch 3 -/// - Mine 3 tenures /// - Submit 2 STX Transfer txs (Tx1, Tx2) in the last tenure /// - Trigger a Bitcoin fork (3 blocks) /// - Verify that signers move into tx replay state [Tx1, Tx2] @@ -3974,33 +4232,39 @@ fn tx_replay_solved_by_mempool_txs() { } let num_signers = 5; - let sender1_sk = Secp256k1PrivateKey::random(); + let sender1_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes()); let sender1_addr = tests::to_addr(&sender1_sk); let send_amt = 100; let send_fee = 180; let num_txs = 2; - let signer_test: SignerTest = SignerTest::new_with_config_modifications( - num_signers, - vec![(sender1_addr, (send_amt + send_fee) * num_txs)], - |c| { - c.validate_with_replay_tx = true; - }, - |node_config| { - node_config.miner.block_commit_delay = Duration::from_secs(1); - node_config.miner.replay_transactions = true; - }, - None, - None, - ); + let signer_test: SignerTest = + SignerTest::new_with_config_modifications_and_snapshot( + num_signers, + vec![(sender1_addr, (send_amt + send_fee) * num_txs)], + |c| { + c.validate_with_replay_tx = true; + }, + |node_config| { + node_config.miner.block_commit_delay = Duration::from_secs(1); + node_config.miner.replay_transactions = true; + node_config.miner.activated_vrf_key_path = + Some(format!("{}/vrf_key", node_config.node.working_dir)); + }, + None, + None, + Some(function_name!()), + ); let conf = &signer_test.running_nodes.conf; let btc_controller = &signer_test.running_nodes.btc_regtest_controller; - let counters = &signer_test.running_nodes.counters; let http_origin = format!("http://{}", &conf.node.rpc_bind); - signer_test.boot_to_epoch_3(); - info!("------------------------- Reached Epoch 3.0 -------------------------"); + if signer_test.bootstrap_snapshot() { + signer_test.shutdown_and_snapshot(); + return; + } + info!("------------------------- Beginning test -------------------------"); - let pre_fork_tenures = 3; + let pre_fork_tenures = 2; for i in 0..pre_fork_tenures { info!("Mining pre-fork tenure {} of {pre_fork_tenures}", i + 1); signer_test.mine_nakamoto_block(Duration::from_secs(30), true); @@ -4027,41 +4291,14 @@ fn tx_replay_solved_by_mempool_txs() { info!("------------------------- Triggering Bitcoin Fork -------------------------"); let tip = get_chain_info(&conf); - let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height - 2); + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); - // note, we should still have normal signer states! - signer_test.check_signer_states_normal(); - - info!("Wait for block off of shallow fork"); TEST_MINE_STALL.set(true); + btc_controller.build_next_block(2); - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } + info!("Wait for block off of shallow fork"); - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 2; - let txid_ok = tx_replay_set[0].txid().to_hex() == sender1_tx1 - && tx_replay_set[1].txid().to_hex() == sender1_tx2; - Ok(len_ok && txid_ok) - }) - .expect("Timed out waiting for tx replay set to be updated"); + signer_test.wait_for_replay_set_eq(30, vec![sender1_tx1.clone(), sender1_tx2.clone()]); // We should have forked 2 txs let sender1_nonce_post_fork = get_account(&http_origin, &sender1_addr).nonce; @@ -4234,31 +4471,37 @@ fn tx_replay_with_fork_occured_before_starting_replaying_txs() { } let num_signers = 5; - let sender1_sk = Secp256k1PrivateKey::random(); + let sender1_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes()); let sender1_addr = tests::to_addr(&sender1_sk); let send_amt = 100; let send_fee = 180; let num_txs = 1; - let signer_test: SignerTest = SignerTest::new_with_config_modifications( - num_signers, - vec![(sender1_addr, (send_amt + send_fee) * num_txs)], - |c| { - c.validate_with_replay_tx = true; - }, - |node_config| { - node_config.miner.block_commit_delay = Duration::from_secs(1); - node_config.miner.replay_transactions = true; - }, - None, - None, - ); + let signer_test: SignerTest = + SignerTest::new_with_config_modifications_and_snapshot( + num_signers, + vec![(sender1_addr, (send_amt + send_fee) * num_txs)], + |c| { + c.validate_with_replay_tx = true; + }, + |node_config| { + node_config.miner.block_commit_delay = Duration::from_secs(1); + node_config.miner.replay_transactions = true; + node_config.miner.activated_vrf_key_path = + Some(format!("{}/vrf_key", node_config.node.working_dir)); + }, + None, + None, + Some(function_name!()), + ); let conf = &signer_test.running_nodes.conf; let btc_controller = &signer_test.running_nodes.btc_regtest_controller; - let counters = &signer_test.running_nodes.counters; let http_origin = format!("http://{}", &conf.node.rpc_bind); - signer_test.boot_to_epoch_3(); - info!("------------------------- Reached Epoch 3.0 -------------------------"); + if signer_test.bootstrap_snapshot() { + signer_test.shutdown_and_snapshot(); + return; + } + info!("------------------------- Beginning test -------------------------"); let pre_fork_tenures = 12; //go to 2nd tenure of 12th cycle for i in 0..pre_fork_tenures { @@ -4271,49 +4514,23 @@ fn tx_replay_with_fork_occured_before_starting_replaying_txs() { .submit_transfer_tx(&sender1_sk, send_fee, send_amt) .unwrap(); signer_test - .wait_for_nonce_increase(&sender1_addr, sender1_nonce) - .expect("Expect sender1 nonce increased"); - - let sender1_nonce = get_account(&http_origin, &sender1_addr).nonce; - assert_eq!(1, sender1_nonce); - - info!("------------------------- Triggering Bitcoin Fork #1 -------------------------"); - let tip = get_chain_info(&conf); - let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height - 2); - btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); - // note, we should still have normal signer states! - signer_test.check_signer_states_normal(); - - info!("Wait for block off of shallow fork"); - TEST_MINE_STALL.set(true); - - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } + .wait_for_nonce_increase(&sender1_addr, sender1_nonce) + .expect("Expect sender1 nonce increased"); + + let sender1_nonce = get_account(&http_origin, &sender1_addr).nonce; + assert_eq!(1, sender1_nonce); + + info!("------------------------- Triggering Bitcoin Fork #1 -------------------------"); + let tip = get_chain_info(&conf); + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height); + btc_controller.invalidate_block(&burn_header_hash_to_fork); + TEST_MINE_STALL.set(true); + btc_controller.build_next_block(2); + + info!("Wait for block off of shallow fork"); // Signers move in Tx Replay mode - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 1; - let txid_ok = tx_replay_set[0].txid().to_hex() == sender1_tx1; - Ok(len_ok && txid_ok) - }) - .expect("Timed out waiting for tx replay set to be updated"); + signer_test.wait_for_replay_set_eq(30, vec![sender1_tx1.clone()]); // We should have forked 1 tx let sender1_nonce_post_fork = get_account(&http_origin, &sender1_addr).nonce; @@ -4323,37 +4540,13 @@ fn tx_replay_with_fork_occured_before_starting_replaying_txs() { let tip = get_chain_info(&conf); let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); - - info!("Wait for block off of shallow fork"); TEST_MINE_STALL.set(true); + btc_controller.build_next_block(2); - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } + info!("Wait for block off of shallow fork"); //Signers still are in the initial state of Tx Replay mode - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 1; - let txid_ok = tx_replay_set[0].txid().to_hex() == sender1_tx1; - Ok(len_ok && txid_ok) - }) - .expect("Timed out waiting for tx replay set to be updated"); + signer_test.wait_for_replay_set_eq(30, vec![sender1_tx1.clone()]); info!("----------- Solve TX Replay ------------"); TEST_MINE_STALL.set(false); @@ -4391,31 +4584,36 @@ fn tx_replay_with_fork_after_empty_tenures_before_starting_replaying_txs() { } let num_signers = 5; - let sender1_sk = Secp256k1PrivateKey::random(); + let sender1_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes()); let sender1_addr = tests::to_addr(&sender1_sk); let send_amt = 100; let send_fee = 180; let num_txs = 1; - let signer_test: SignerTest = SignerTest::new_with_config_modifications( - num_signers, - vec![(sender1_addr, (send_amt + send_fee) * num_txs)], - |c| { - c.validate_with_replay_tx = true; - }, - |node_config| { - node_config.miner.block_commit_delay = Duration::from_secs(1); - node_config.miner.replay_transactions = true; - }, - None, - None, - ); + let signer_test: SignerTest = + SignerTest::new_with_config_modifications_and_snapshot( + num_signers, + vec![(sender1_addr, (send_amt + send_fee) * num_txs)], + |c| { + c.validate_with_replay_tx = true; + c.reset_replay_set_after_fork_blocks = 5; + }, + |node_config| { + node_config.miner.block_commit_delay = Duration::from_secs(1); + node_config.miner.replay_transactions = true; + }, + None, + None, + Some(function_name!()), + ); let conf = &signer_test.running_nodes.conf; let btc_controller = &signer_test.running_nodes.btc_regtest_controller; - let counters = &signer_test.running_nodes.counters; let http_origin = format!("http://{}", &conf.node.rpc_bind); - signer_test.boot_to_epoch_3(); - info!("------------------------- Reached Epoch 3.0 -------------------------"); + if signer_test.bootstrap_snapshot() { + signer_test.shutdown_and_snapshot(); + return; + } + info!("------------------------- Beginning test -------------------------"); let pre_fork_tenures = 10; //go to Tenure #4 in Cycle #12 for i in 0..pre_fork_tenures { @@ -4437,73 +4635,27 @@ fn tx_replay_with_fork_after_empty_tenures_before_starting_replaying_txs() { info!("------------------------- Triggering Bitcoin Fork #1 -------------------------"); let tip = get_chain_info(&conf); - let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height - 2); + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); - // note, we should still have normal signer states! - signer_test.check_signer_states_normal(); - - info!("Wait for block off of shallow fork"); TEST_MINE_STALL.set(true); + btc_controller.build_next_block(2); - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } + info!("Wait for block off of shallow fork"); // Signers moved in Tx Replay mode - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 1; - let txid_ok = tx_replay_set[0].txid().to_hex() == sender1_tx1; - Ok(len_ok && txid_ok) - }) - .expect("Timed out waiting for tx replay set to be updated"); + signer_test.wait_for_replay_set_eq(30, vec![sender1_tx1.clone()]); // We should have forked tx1 let sender1_nonce_post_fork = get_account(&http_origin, &sender1_addr).nonce; assert_eq!(0, sender1_nonce_post_fork); info!("------------------- Produce Empty Tenuree -------------------------"); - TEST_MINE_STALL.set(false); let tip = get_chain_info(&conf); - _ = wait_for_tenure_change_tx(30, TenureChangeCause::BlockFound, tip.stacks_tip_height + 1); - TEST_MINE_STALL.set(true); - - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - TEST_MINE_STALL.set(false); - let tip = get_chain_info(&conf); _ = wait_for_tenure_change_tx(30, TenureChangeCause::BlockFound, tip.stacks_tip_height + 1); TEST_MINE_STALL.set(true); - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 1; - let txid_ok = tx_replay_set[0].txid().to_hex() == sender1_tx1; - Ok(len_ok && txid_ok) - }) - .expect("Timed out waiting for tx replay set to be updated"); + signer_test.wait_for_replay_set_eq(30, vec![sender1_tx1.clone()]); info!("------------------------- Triggering Bitcoin Fork #2 -------------------------"); test_observer::clear(); @@ -4511,36 +4663,13 @@ fn tx_replay_with_fork_after_empty_tenures_before_starting_replaying_txs() { let tip = get_chain_info(&conf); let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); + TEST_MINE_STALL.set(true); + btc_controller.build_next_block(2); info!("Wait for block off of shallow fork"); - TEST_MINE_STALL.set(true); - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } // Signers still are in Tx Replay mode (as the initial replay state) - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 1; - let txid_ok = tx_replay_set[0].txid().to_hex() == sender1_tx1; - Ok(len_ok && txid_ok) - }) - .expect("Timed out waiting for tx replay set to be updated"); + signer_test.wait_for_replay_set_eq(30, vec![sender1_tx1.clone()]); info!("------------------------- Mine Tx Replay Set -------------------------"); TEST_MINE_STALL.set(false); @@ -4574,37 +4703,40 @@ fn tx_replay_with_fork_causing_replay_set_to_be_updated() { } let num_signers = 5; - let sender1_sk = Secp256k1PrivateKey::random(); + let sender1_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes()); let sender1_addr = tests::to_addr(&sender1_sk); let send_amt = 100; let send_fee = 180; let num_txs = 2; - let signer_test: SignerTest = SignerTest::new_with_config_modifications( - num_signers, - vec![(sender1_addr, (send_amt + send_fee) * num_txs)], - |c| { - c.validate_with_replay_tx = true; - }, - |node_config| { - node_config.miner.block_commit_delay = Duration::from_secs(1); - node_config.miner.replay_transactions = true; - }, - None, - None, - ); + let signer_test: SignerTest = + SignerTest::new_with_config_modifications_and_snapshot( + num_signers, + vec![(sender1_addr, (send_amt + send_fee) * num_txs)], + |c| { + c.validate_with_replay_tx = true; + }, + |node_config| { + node_config.miner.block_commit_delay = Duration::from_secs(1); + node_config.miner.replay_transactions = true; + }, + None, + None, + Some(function_name!()), + ); let conf = &signer_test.running_nodes.conf; let btc_controller = &signer_test.running_nodes.btc_regtest_controller; - let counters = &signer_test.running_nodes.counters; let http_origin = format!("http://{}", &conf.node.rpc_bind); - signer_test.boot_to_epoch_3(); - info!("------------------------- Reached Epoch 3.0 -------------------------"); + if signer_test.bootstrap_snapshot() { + signer_test.shutdown_and_snapshot(); + return; + } + info!("------------------------- Beginning test -------------------------"); let pre_fork_tenures = 10; for i in 0..pre_fork_tenures { info!("Mining pre-fork tenure {} of {pre_fork_tenures}", i + 1); signer_test.mine_nakamoto_block(Duration::from_secs(30), true); - signer_test.check_signer_states_normal(); } // Make 2 transfer txs, each in its own tenure so that can be forked in different forks @@ -4618,11 +4750,9 @@ fn tx_replay_with_fork_causing_replay_set_to_be_updated() { .expect("Expect sender1 nonce increased"); signer_test.mine_nakamoto_block(Duration::from_secs(30), true); - signer_test.mine_nakamoto_block(Duration::from_secs(30), true); - signer_test.mine_nakamoto_block(Duration::from_secs(30), true); let tip_at_tx2 = get_chain_info(&conf); - assert_eq!(244, tip_at_tx2.burn_block_height); + assert_eq!(242, tip_at_tx2.burn_block_height); let (sender1_tx2, sender1_nonce) = signer_test .submit_transfer_tx(&sender1_sk, send_fee, send_amt) .unwrap(); @@ -4634,82 +4764,45 @@ fn tx_replay_with_fork_causing_replay_set_to_be_updated() { assert_eq!(2, sender1_nonce); info!("------------------------- Triggering Bitcoin Fork #1 -------------------------"); - let burn_header_hash_to_fork = btc_controller.get_block_hash(tip_at_tx2.burn_block_height - 2); + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip_at_tx2.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); - // note, we should still have normal signer states! - signer_test.check_signer_states_normal(); + btc_controller.build_next_block(1); info!("Wait for block off of shallow fork"); TEST_MINE_STALL.set(true); - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } - assert_eq!(247, get_chain_info(&conf).burn_block_height); + btc_controller.build_next_block(1); + + wait_for(10, || { + let tip = get_chain_info(&conf); + Ok(tip.burn_block_height == 243) + }) + .expect("Timed out waiting for burn block height to be 243"); // Signers move in Tx Replay mode - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 1; - let txid_ok = tx_replay_set[0].txid().to_hex() == sender1_tx2; - Ok(len_ok && txid_ok) - }) - .expect("Timed out waiting for tx replay set to be updated"); + signer_test.wait_for_replay_set_eq(30, vec![sender1_tx2.clone()]); // We should have forked one tx (Tx2) let sender1_nonce_post_fork = get_account(&http_origin, &sender1_addr).nonce; assert_eq!(1, sender1_nonce_post_fork); - info!("------------------------- Triggering Bitcoin Fork #2 -------------------------"); + info!( + "------------------------- Triggering Bitcoin Fork #2 from {} -------------------------", + tip_at_tx1.burn_block_height + ); let burn_header_hash_to_fork = btc_controller.get_block_hash(tip_at_tx1.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(7); + btc_controller.build_next_block(4); + wait_for(10, || { + let tip = get_chain_info(&conf); + info!("Burn block height: {}", tip.burn_block_height); + Ok(tip.burn_block_height == 244) + }) + .expect("Timed out waiting for burn block height to be 244"); info!("Wait for block off of shallow fork"); - TEST_MINE_STALL.set(true); - - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } - assert_eq!(250, get_chain_info(&conf).burn_block_height); //Signers should update the Tx Replay Set - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 2; - let txid_ok = tx_replay_set[0].txid().to_hex() == sender1_tx1 - && tx_replay_set[1].txid().to_hex() == sender1_tx2; - Ok(len_ok && txid_ok) - }) - .expect("Timed out waiting for tx replay set to be updated"); + signer_test.wait_for_replay_set_eq(30, vec![sender1_tx1.clone(), sender1_tx2.clone()]); info!("----------- Solve TX Replay ------------"); TEST_MINE_STALL.set(false); @@ -4746,31 +4839,35 @@ fn tx_replay_with_fork_causing_replay_to_be_cleared_due_to_cycle() { } let num_signers = 5; - let sender1_sk = Secp256k1PrivateKey::random(); + let sender1_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes()); let sender1_addr = tests::to_addr(&sender1_sk); let send_amt = 100; let send_fee = 180; let num_txs = 2; - let signer_test: SignerTest = SignerTest::new_with_config_modifications( - num_signers, - vec![(sender1_addr, (send_amt + send_fee) * num_txs)], - |c| { - c.validate_with_replay_tx = true; - }, - |node_config| { - node_config.miner.block_commit_delay = Duration::from_secs(1); - node_config.miner.replay_transactions = true; - }, - None, - None, - ); + let signer_test: SignerTest = + SignerTest::new_with_config_modifications_and_snapshot( + num_signers, + vec![(sender1_addr, (send_amt + send_fee) * num_txs)], + |c| { + c.validate_with_replay_tx = true; + }, + |node_config| { + node_config.miner.block_commit_delay = Duration::from_secs(1); + node_config.miner.replay_transactions = true; + }, + None, + None, + Some(function_name!()), + ); let conf = &signer_test.running_nodes.conf; let btc_controller = &signer_test.running_nodes.btc_regtest_controller; - let counters = &signer_test.running_nodes.counters; let http_origin = format!("http://{}", &conf.node.rpc_bind); - signer_test.boot_to_epoch_3(); - info!("------------------------- Reached Epoch 3.0 -------------------------"); + if signer_test.bootstrap_snapshot() { + signer_test.shutdown_and_snapshot(); + return; + } + info!("------------------------- Beginning test -------------------------"); let pre_fork_tenures = 8; for i in 0..pre_fork_tenures { @@ -4803,40 +4900,15 @@ fn tx_replay_with_fork_causing_replay_to_be_cleared_due_to_cycle() { assert_eq!(1, sender1_nonce); info!("------------------------- Triggering Bitcoin Fork #1 -------------------------"); - let burn_header_hash_to_fork = btc_controller.get_block_hash(tip_at_rc12.burn_block_height - 2); + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip_at_rc12.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); - // note, we should still have normal signer states! - signer_test.check_signer_states_normal(); + TEST_MINE_STALL.set(true); + btc_controller.build_next_block(2); info!("Wait for block off of shallow fork"); - TEST_MINE_STALL.set(true); - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } // Signers move in Tx Replay mode - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 1; - let txid_ok = tx_replay_set[0].txid().to_hex() == sender1_tx1; - Ok(len_ok && txid_ok) - }) - .expect("Timed out waiting for tx replay set to be updated"); + signer_test.wait_for_replay_set_eq(30, vec![sender1_tx1.clone()]); // We should have forked one tx (Tx2) let sender1_nonce_post_fork = get_account(&http_origin, &sender1_addr).nonce; @@ -4845,25 +4917,9 @@ fn tx_replay_with_fork_causing_replay_to_be_cleared_due_to_cycle() { info!("------------------------- Triggering Bitcoin Fork #2 -------------------------"); let burn_header_hash_to_fork = btc_controller.get_block_hash(tip_at_rc11.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(7); + btc_controller.build_next_block(6); info!("Wait for block off of shallow fork"); - TEST_MINE_STALL.set(true); - - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } //Signers should clear the Tx Replay Set signer_test @@ -4883,7 +4939,6 @@ fn tx_replay_with_fork_causing_replay_to_be_cleared_due_to_cycle() { /// The test flow is: /// /// - Boot to Epoch 3 -/// - Mine 10 tenures (to handle multiple fork in Cycle 12) /// - Deploy 1 Big Contract and mine 2 tenures (to escape fork) /// - Submit 2 Contract Call txs (Tx1, Tx2) in the last tenure, /// requiring Tenure Extend due to Tenure Budget exceeded @@ -4899,35 +4954,40 @@ fn tx_replay_with_fork_middle_replay_while_tenure_extending() { } let num_signers = 5; - let sender_sk = Secp256k1PrivateKey::random(); + let sender_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes()); let sender_addr = tests::to_addr(&sender_sk); let deploy_fee = 1000000; let call_fee = 1000; let call_num = 2; - let signer_test: SignerTest = SignerTest::new_with_config_modifications( - num_signers, - vec![(sender_addr, deploy_fee + call_fee * call_num)], - |c| { - c.validate_with_replay_tx = true; - c.tenure_idle_timeout = Duration::from_secs(10); - }, - |node_config| { - node_config.miner.block_commit_delay = Duration::from_secs(1); - node_config.miner.replay_transactions = true; - }, - None, - None, - ); + let signer_test: SignerTest = + SignerTest::new_with_config_modifications_and_snapshot( + num_signers, + vec![(sender_addr, deploy_fee + call_fee * call_num)], + |c| { + c.validate_with_replay_tx = true; + c.tenure_idle_timeout = Duration::from_secs(10); + c.reset_replay_set_after_fork_blocks = 5; + }, + |node_config| { + node_config.miner.block_commit_delay = Duration::from_secs(1); + node_config.miner.replay_transactions = true; + }, + None, + None, + Some(function_name!()), + ); let conf = &signer_test.running_nodes.conf; let http_origin = format!("http://{}", &conf.node.rpc_bind); let btc_controller = &signer_test.running_nodes.btc_regtest_controller; - let counters = &signer_test.running_nodes.counters; let stacks_miner_pk = StacksPublicKey::from_private(&conf.miner.mining_key.unwrap()); - signer_test.boot_to_epoch_3(); - info!("------------------------- Reached Epoch 3.0 -------------------------"); + if signer_test.bootstrap_snapshot() { + signer_test.shutdown_and_snapshot(); + return; + } + info!("------------------------- Beginning test -------------------------"); - let pre_fork_tenures = 10; + let pre_fork_tenures = 2; for i in 0..pre_fork_tenures { info!("Mining pre-fork tenure {} of {pre_fork_tenures}", i + 1); signer_test.mine_nakamoto_block(Duration::from_secs(30), true); @@ -4950,7 +5010,6 @@ fn tx_replay_with_fork_middle_replay_while_tenure_extending() { .expect("Timed out waiting for nonce to increase"); signer_test.mine_nakamoto_block(Duration::from_secs(30), true); - signer_test.mine_nakamoto_block(Duration::from_secs(30), true); // Then, sumbmit 2 Contract Calls that require Tenure Extension to be addressed. info!("---- Submit big tx1 to be mined ----"); @@ -4962,12 +5021,13 @@ fn tx_replay_with_fork_middle_replay_while_tenure_extending() { .expect("Timed out waiting for nonce to increase"); info!("---- Submit big tx2 to be mined ----"); + let tip = get_chain_info(conf); + let (txid2, txid2_nonce) = signer_test .submit_contract_call(&sender_sk, call_fee, "big-contract", "big-tx", &vec![]) .unwrap(); // Tenure Extend happen because of tenure budget exceeded - let tip = get_chain_info(conf); _ = wait_for_tenure_change_tx(30, TenureChangeCause::Extended, tip.stacks_tip_height + 1); signer_test @@ -4979,39 +5039,14 @@ fn tx_replay_with_fork_middle_replay_while_tenure_extending() { info!("------------------------- Triggering Bitcoin Fork -------------------------"); let tip = get_chain_info(conf); - let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height - 1); + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); - - info!("Wait for block off of shallow fork"); TEST_MINE_STALL.set(true); + btc_controller.build_next_block(2); - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } + info!("Wait for block off of shallow fork"); - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 2; - let txid1_ok = tx_replay_set[0].txid().to_hex() == txid1; - let txid2_ok = tx_replay_set[1].txid().to_hex() == txid2; - Ok(len_ok && txid1_ok && txid2_ok) - }) - .expect("Timed out waiting for tx replay set"); + signer_test.wait_for_replay_set_eq(30, vec![txid1.clone(), txid2.clone()]); let post_fork_nonce = get_account(&http_origin, &sender_addr).nonce; assert_eq!(1, post_fork_nonce); //due to contract deploy tx @@ -5025,54 +5060,18 @@ fn tx_replay_with_fork_middle_replay_while_tenure_extending() { TEST_MINE_STALL.set(true); // Signers still waiting for the Tx Replay set to be completed - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 2; - let txid1_ok = tx_replay_set[0].txid().to_hex() == txid1; - let txid2_ok = tx_replay_set[1].txid().to_hex() == txid2; - Ok(len_ok && txid1_ok && txid2_ok) - }) - .expect("Timed out waiting for tx replay set"); + signer_test.wait_for_replay_set_eq(30, vec![txid1.clone(), txid2.clone()]); info!("------------------------- Triggering Bitcoin Fork #2 -------------------------"); //Fork in the middle of Tx Replay let tip = get_chain_info(&conf); let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height - 1); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); + btc_controller.build_next_block(2); info!("Wait for block off of shallow fork"); - TEST_MINE_STALL.set(true); - - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 2; - let txid1_ok = tx_replay_set[0].txid().to_hex() == txid1; - let txid2_ok = tx_replay_set[1].txid().to_hex() == txid2; - Ok(len_ok && txid1_ok && txid2_ok) - }) - .expect("Timed out waiting for tx replay set"); + signer_test.wait_for_replay_set_eq(30, vec![txid1.clone(), txid2.clone()]); let post_fork_nonce = get_account(&http_origin, &sender_addr).nonce; assert_eq!(1, post_fork_nonce); //due to contract deploy tx @@ -5104,7 +5103,6 @@ fn tx_replay_with_fork_middle_replay_while_tenure_extending() { /// The test flow is: /// /// - Boot to Epoch 3 -/// - Mine 10 tenures (to handle multiple fork in Cycle 12) /// - Deploy 1 Big Contract and mine 2 tenures (to escape fork) /// - Submit 2 Contract Call txs (Tx1, Tx2) in the last tenure, /// requiring Tenure Extend due to Tenure Budget exceeded @@ -5123,45 +5121,49 @@ fn tx_replay_with_fork_middle_replay_while_tenure_extending_and_new_tx_submitted } let num_signers = 5; - let sender1_sk = Secp256k1PrivateKey::random(); + let sender1_sk = Secp256k1PrivateKey::from_seed("sender_1".as_bytes()); let sender1_addr = tests::to_addr(&sender1_sk); let send1_deploy_fee = 1000000; let send1_call_fee = 1000; let send1_call_num = 2; - let sender2_sk = Secp256k1PrivateKey::random(); + let sender2_sk = Secp256k1PrivateKey::from_seed("sender_2".as_bytes()); let sender2_addr = tests::to_addr(&sender2_sk); let send2_amt = 100; let send2_fee = 180; let send2_txs = 1; - let signer_test: SignerTest = SignerTest::new_with_config_modifications( - num_signers, - vec![ - ( - sender1_addr, - send1_deploy_fee + send1_call_fee * send1_call_num, - ), - (sender2_addr, (send2_amt + send2_fee) * send2_txs), - ], - |c| { - c.validate_with_replay_tx = true; - c.tenure_idle_timeout = Duration::from_secs(10); - }, - |node_config| { - node_config.miner.block_commit_delay = Duration::from_secs(1); - node_config.miner.replay_transactions = true; - }, - None, - None, - ); + let signer_test: SignerTest = + SignerTest::new_with_config_modifications_and_snapshot( + num_signers, + vec![ + ( + sender1_addr, + send1_deploy_fee + send1_call_fee * send1_call_num, + ), + (sender2_addr, (send2_amt + send2_fee) * send2_txs), + ], + |c| { + c.validate_with_replay_tx = true; + c.tenure_idle_timeout = Duration::from_secs(10); + }, + |node_config| { + node_config.miner.block_commit_delay = Duration::from_secs(1); + node_config.miner.replay_transactions = true; + }, + None, + None, + Some(function_name!()), + ); let conf = &signer_test.running_nodes.conf; let http_origin = format!("http://{}", &conf.node.rpc_bind); let btc_controller = &signer_test.running_nodes.btc_regtest_controller; - let counters = &signer_test.running_nodes.counters; let stacks_miner_pk = StacksPublicKey::from_private(&conf.miner.mining_key.unwrap()); - signer_test.boot_to_epoch_3(); - info!("------------------------- Reached Epoch 3.0 -------------------------"); - let pre_fork_tenures = 10; + if signer_test.bootstrap_snapshot() { + signer_test.shutdown_and_snapshot(); + return; + } + info!("------------------------- Beginning test -------------------------"); + let pre_fork_tenures = 2; for i in 0..pre_fork_tenures { info!("Mining pre-fork tenure {} of {pre_fork_tenures}", i + 1); signer_test.mine_nakamoto_block(Duration::from_secs(30), true); @@ -5184,7 +5186,6 @@ fn tx_replay_with_fork_middle_replay_while_tenure_extending_and_new_tx_submitted .expect("Timed out waiting for nonce to increase"); signer_test.mine_nakamoto_block(Duration::from_secs(30), true); - signer_test.mine_nakamoto_block(Duration::from_secs(30), true); // Then, sumbmit 2 Contract Calls that require Tenure Extension to be addressed. info!("---- Waiting for first big tx to be mined ----"); @@ -5226,39 +5227,14 @@ fn tx_replay_with_fork_middle_replay_while_tenure_extending_and_new_tx_submitted info!("------------------------- Triggering Bitcoin Fork -------------------------"); let tip = get_chain_info(conf); - let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height - 1); + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); - - info!("Wait for block off of shallow fork"); TEST_MINE_STALL.set(true); + btc_controller.build_next_block(2); - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } + info!("Wait for block off of shallow fork"); - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 2; - let txid1_ok = tx_replay_set[0].txid().to_hex() == txid1; - let txid2_ok = tx_replay_set[1].txid().to_hex() == txid2; - Ok(len_ok && txid1_ok && txid2_ok) - }) - .expect("Timed out waiting for tx replay set"); + signer_test.wait_for_replay_set_eq(30, vec![txid1.clone(), txid2.clone()]); let post_fork_nonce = get_account(&http_origin, &sender1_addr).nonce; assert_eq!(1, post_fork_nonce); //due to contract deploy tx @@ -5272,17 +5248,7 @@ fn tx_replay_with_fork_middle_replay_while_tenure_extending_and_new_tx_submitted TEST_MINE_STALL.set(true); // Signers still waiting for the Tx Replay set to be completed - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 2; - let txid1_ok = tx_replay_set[0].txid().to_hex() == txid1; - let txid2_ok = tx_replay_set[1].txid().to_hex() == txid2; - Ok(len_ok && txid1_ok && txid2_ok) - }) - .expect("Timed out waiting for tx replay set"); + signer_test.wait_for_replay_set_eq(30, vec![txid1.clone(), txid2.clone()]); info!("---- New Transaction is Submitted ----"); // Tx3 reach the mempool, meanwhile mining is stalled @@ -5293,39 +5259,13 @@ fn tx_replay_with_fork_middle_replay_while_tenure_extending_and_new_tx_submitted info!("------------------------- Triggering Bitcoin Fork #2 -------------------------"); //Fork in the middle of Tx Replay let tip = get_chain_info(&conf); - let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height - 1); + let burn_header_hash_to_fork = btc_controller.get_block_hash(tip.burn_block_height); btc_controller.invalidate_block(&burn_header_hash_to_fork); - btc_controller.build_next_block(3); + btc_controller.build_next_block(2); info!("Wait for block off of shallow fork"); - TEST_MINE_STALL.set(true); - - let submitted_commits = counters.naka_submitted_commits.clone(); - // we need to mine some blocks to get back to being considered a frequent miner - for i in 0..3 { - let current_burn_height = get_chain_info(&conf).burn_block_height; - info!( - "Mining block #{i} to be considered a frequent miner"; - "current_burn_height" => current_burn_height, - ); - let commits_count = submitted_commits.load(Ordering::SeqCst); - next_block_and(btc_controller, 60, || { - Ok(submitted_commits.load(Ordering::SeqCst) > commits_count) - }) - .unwrap(); - } - signer_test - .wait_for_signer_state_check(30, |state| { - let Some(tx_replay_set) = state.get_tx_replay_set() else { - return Ok(false); - }; - let len_ok = tx_replay_set.len() == 2; - let txid1_ok = tx_replay_set[0].txid().to_hex() == txid1; - let txid2_ok = tx_replay_set[1].txid().to_hex() == txid2; - Ok(len_ok && txid1_ok && txid2_ok) - }) - .expect("Timed out waiting for tx replay set"); + signer_test.wait_for_replay_set_eq(30, vec![txid1.clone(), txid2.clone()]); let sender1_nonce_post_fork = get_account(&http_origin, &sender1_addr).nonce; assert_eq!(1, sender1_nonce_post_fork); //due to contract deploy tx @@ -10121,6 +10061,7 @@ fn block_validation_response_timeout() { tenure_idle_timeout: Duration::from_secs(300), tenure_idle_timeout_buffer: Duration::from_secs(2), reorg_attempts_activity_timeout: Duration::from_secs(30), + reset_replay_set_after_fork_blocks: DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS, }; let mut block = NakamotoBlock { header: NakamotoBlockHeader::empty(), @@ -10410,6 +10351,7 @@ fn block_validation_pending_table() { tenure_idle_timeout: Duration::from_secs(300), tenure_idle_timeout_buffer: Duration::from_secs(2), reorg_attempts_activity_timeout: Duration::from_secs(30), + reset_replay_set_after_fork_blocks: DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS, }; let mut block = NakamotoBlock { header: NakamotoBlockHeader::empty(), @@ -11693,6 +11635,7 @@ fn incoming_signers_ignore_block_proposals() { tenure_idle_timeout: Duration::from_secs(300), tenure_idle_timeout_buffer: Duration::from_secs(2), reorg_attempts_activity_timeout: Duration::from_secs(30), + reset_replay_set_after_fork_blocks: DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS, }; let mut block = NakamotoBlock { header: NakamotoBlockHeader::empty(), @@ -11868,6 +11811,7 @@ fn outgoing_signers_ignore_block_proposals() { tenure_idle_timeout: Duration::from_secs(300), tenure_idle_timeout_buffer: Duration::from_secs(2), reorg_attempts_activity_timeout: Duration::from_secs(30), + reset_replay_set_after_fork_blocks: DEFAULT_RESET_REPLAY_SET_AFTER_FORK_BLOCKS, }; let mut block = NakamotoBlock { header: NakamotoBlockHeader::empty(),