diff --git a/.github/workflows/bitcoin-tests.yml b/.github/workflows/bitcoin-tests.yml index f7b96b96c7..a6aa956391 100644 --- a/.github/workflows/bitcoin-tests.yml +++ b/.github/workflows/bitcoin-tests.yml @@ -87,6 +87,8 @@ jobs: - tests::signer::v0::miner_gather_signatures - tests::signer::v0::mine_2_nakamoto_reward_cycles - tests::signer::v0::end_of_tenure + - tests::signer::v0::forked_tenure_okay + - tests::signer::v0::forked_tenure_invalid - tests::nakamoto_integrations::stack_stx_burn_op_integration_test - tests::nakamoto_integrations::check_block_heights - tests::nakamoto_integrations::clarity_burn_state diff --git a/stacks-signer/src/chainstate.rs b/stacks-signer/src/chainstate.rs index b6872b8079..95c60d3a3c 100644 --- a/stacks-signer/src/chainstate.rs +++ b/stacks-signer/src/chainstate.rs @@ -34,22 +34,10 @@ use crate::signerdb::SignerDb; pub enum SignerChainstateError { /// Error resulting from database interactions #[error("Database error: {0}")] - DBError(DBError), + DBError(#[from] DBError), /// Error resulting from crate::client interactions #[error("Client error: {0}")] - ClientError(ClientError), -} - -impl From for SignerChainstateError { - fn from(value: ClientError) -> Self { - Self::ClientError(value) - } -} - -impl From for SignerChainstateError { - fn from(value: DBError) -> Self { - Self::DBError(value) - } + ClientError(#[from] ClientError), } /// Captures this signer's current view of a sortition's miner. @@ -93,8 +81,8 @@ pub struct SortitionState { /// Captures the configuration settings used by the signer when evaluating block proposals. #[derive(Debug, Clone)] pub struct ProposalEvalConfig { - /// How much time between the first block proposal in a tenure and the next bitcoin block - /// must pass before a subsequent miner isn't allowed to reorg the tenure + /// How much time must pass between the first block proposal in a tenure and the next bitcoin block + /// before a subsequent miner isn't allowed to reorg the tenure pub first_proposal_burn_block_timing: Duration, } @@ -323,6 +311,9 @@ impl SortitionsView { "Most recent miner's tenure does not build off the prior sortition, checking if this is valid behavior"; "proposed_block_consensus_hash" => %block.header.consensus_hash, "proposed_block_signer_sighash" => %block.header.signer_signature_hash(), + "sortition_state.consensus_hash" => %sortition_state.consensus_hash, + "sortition_state.prior_sortition" => %sortition_state.prior_sortition, + "sortition_state.parent_tenure_id" => %sortition_state.parent_tenure_id, ); let tenures_reorged = client.get_tenure_forking_info( @@ -367,8 +358,14 @@ impl SortitionsView { sortition_state_received_time { // how long was there between when the proposal was received and the next sortition started? - let proposal_to_sortition = sortition_state_received_time - .saturating_sub(local_block_info.proposed_time); + let proposal_to_sortition = if let Some(signed_at) = + local_block_info.signed_self + { + sortition_state_received_time.saturating_sub(signed_at) + } else { + info!("We did not sign over the reorged tenure's first block, considering it as a late-arriving proposal"); + 0 + }; if Duration::from_secs(proposal_to_sortition) <= *first_proposal_burn_block_timing { diff --git a/stacks-signer/src/config.rs b/stacks-signer/src/config.rs index 4b9970e38f..4c7bc565d3 100644 --- a/stacks-signer/src/config.rs +++ b/stacks-signer/src/config.rs @@ -151,8 +151,8 @@ pub struct SignerConfig { pub max_tx_fee_ustx: Option, /// The path to the signer's database file pub db_path: PathBuf, - /// How much time between the first block proposal in a tenure and the next bitcoin block - /// must pass before a subsequent miner isn't allowed to reorg the tenure + /// How much time must pass between the first block proposal in a tenure and the next bitcoin block + /// before a subsequent miner isn't allowed to reorg the tenure pub first_proposal_burn_block_timing: Duration, } @@ -233,8 +233,8 @@ struct RawConfigFile { pub db_path: String, /// Metrics endpoint pub metrics_endpoint: Option, - /// How much time between the first block proposal in a tenure and the next bitcoin block - /// must pass before a subsequent miner isn't allowed to reorg the tenure + /// How much time must pass between the first block proposal in a tenure and the next bitcoin block + /// before a subsequent miner isn't allowed to reorg the tenure pub first_proposal_burn_block_timing_secs: Option, } diff --git a/stacks-signer/src/signerdb.rs b/stacks-signer/src/signerdb.rs index 582e2027d2..f6405b23ad 100644 --- a/stacks-signer/src/signerdb.rs +++ b/stacks-signer/src/signerdb.rs @@ -83,6 +83,16 @@ impl BlockInfo { block_info } + /// Mark this block as valid, signed over, and record a timestamp in the block info if it wasn't + /// already set. + pub fn mark_signed_and_valid(&mut self) { + self.valid = Some(true); + self.signed_over = true; + if self.signed_self.is_none() { + self.signed_self = Some(get_epoch_time_secs()); + } + } + /// Return the block's signer signature hash pub fn signer_signature_hash(&self) -> Sha512Trunc256Sum { self.block.header.signer_signature_hash() diff --git a/stacks-signer/src/v0/signer.rs b/stacks-signer/src/v0/signer.rs index 661e34d5ea..51a6bf91ad 100644 --- a/stacks-signer/src/v0/signer.rs +++ b/stacks-signer/src/v0/signer.rs @@ -409,7 +409,7 @@ impl Signer { return; } }; - block_info.valid = Some(true); + block_info.mark_signed_and_valid(); let signature = self .private_key .sign(&signer_signature_hash.0) diff --git a/stackslib/src/chainstate/nakamoto/miner.rs b/stackslib/src/chainstate/nakamoto/miner.rs index 3c4990de4d..136e7b84dd 100644 --- a/stackslib/src/chainstate/nakamoto/miner.rs +++ b/stackslib/src/chainstate/nakamoto/miner.rs @@ -216,6 +216,11 @@ impl NakamotoBlockBuilder { tenure_id_consensus_hash.clone(), parent_stacks_header.index_block_hash(), bitvec_len, + parent_stacks_header + .anchored_header + .as_stacks_nakamoto() + .map(|b| b.timestamp) + .unwrap_or(0), ), }) } diff --git a/stackslib/src/chainstate/nakamoto/mod.rs b/stackslib/src/chainstate/nakamoto/mod.rs index 53e4f58747..cd1be59349 100644 --- a/stackslib/src/chainstate/nakamoto/mod.rs +++ b/stackslib/src/chainstate/nakamoto/mod.rs @@ -635,6 +635,7 @@ impl NakamotoBlockHeader { consensus_hash: ConsensusHash, parent_block_id: StacksBlockId, bitvec_len: u16, + parent_timestamp: u64, ) -> NakamotoBlockHeader { NakamotoBlockHeader { version: NAKAMOTO_BLOCK_VERSION, @@ -644,7 +645,7 @@ impl NakamotoBlockHeader { parent_block_id, tx_merkle_root: Sha512Trunc256Sum([0u8; 32]), state_index_root: TrieHash([0u8; 32]), - timestamp: get_epoch_time_secs(), + timestamp: std::cmp::max(parent_timestamp, get_epoch_time_secs()), miner_signature: MessageSignature::empty(), signer_signature: vec![], pox_treatment: BitVec::ones(bitvec_len) diff --git a/stackslib/src/net/api/getsortition.rs b/stackslib/src/net/api/getsortition.rs index ce17b5427b..5e0557ca26 100644 --- a/stackslib/src/net/api/getsortition.rs +++ b/stackslib/src/net/api/getsortition.rs @@ -247,7 +247,7 @@ impl RPCRequestHandler for GetSortitionHandler { stacks_parent_sn.consensus_hash.clone() } else { // we actually need to perform the marf lookup - let last_sortition = handle.get_last_snapshot_with_sortition(stacks_parent_sn.block_height)?; + let last_sortition = handle.get_last_snapshot_with_sortition(sortition_sn.block_height.saturating_sub(1))?; last_sortition.consensus_hash }; diff --git a/testnet/stacks-node/src/nakamoto_node/miner.rs b/testnet/stacks-node/src/nakamoto_node/miner.rs index 8cab796a65..436951c237 100644 --- a/testnet/stacks-node/src/nakamoto_node/miner.rs +++ b/testnet/stacks-node/src/nakamoto_node/miner.rs @@ -243,6 +243,27 @@ impl BlockMinerThread { }; if let Some(mut new_block) = new_block { + #[cfg(test)] + { + if *TEST_BROADCAST_STALL.lock().unwrap() == Some(true) { + // Do an extra check just so we don't log EVERY time. + warn!("Broadcasting is stalled due to testing directive."; + "stacks_block_id" => %new_block.block_id(), + "stacks_block_hash" => %new_block.header.block_hash(), + "height" => new_block.header.chain_length, + "consensus_hash" => %new_block.header.consensus_hash + ); + while *TEST_BROADCAST_STALL.lock().unwrap() == Some(true) { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + info!("Broadcasting is no longer stalled due to testing directive."; + "block_id" => %new_block.block_id(), + "height" => new_block.header.chain_length, + "consensus_hash" => %new_block.header.consensus_hash + ); + } + } + let (reward_set, signer_signature) = match self.gather_signatures( &mut new_block, self.burn_block.block_height, @@ -656,26 +677,6 @@ impl BlockMinerThread { reward_set: RewardSet, stackerdbs: &StackerDBs, ) -> Result<(), NakamotoNodeError> { - #[cfg(test)] - { - if *TEST_BROADCAST_STALL.lock().unwrap() == Some(true) { - // Do an extra check just so we don't log EVERY time. - warn!("Broadcasting is stalled due to testing directive."; - "stacks_block_id" => %block.block_id(), - "stacks_block_hash" => %block.header.block_hash(), - "height" => block.header.chain_length, - "consensus_hash" => %block.header.consensus_hash - ); - while *TEST_BROADCAST_STALL.lock().unwrap() == Some(true) { - std::thread::sleep(std::time::Duration::from_millis(10)); - } - info!("Broadcasting is no longer stalled due to testing directive."; - "block_id" => %block.block_id(), - "height" => block.header.chain_length, - "consensus_hash" => %block.header.consensus_hash - ); - } - } let mut chain_state = neon_node::open_chainstate_with_faults(&self.config) .expect("FATAL: could not open chainstate DB"); let sort_db = SortitionDB::open( @@ -1014,7 +1015,6 @@ impl BlockMinerThread { ChainstateError::NoTransactionsToMine, )); } - let mining_key = self.keychain.get_nakamoto_sk(); let miner_signature = mining_key .sign(block.header.miner_signature_hash().as_bytes()) @@ -1028,6 +1028,7 @@ impl BlockMinerThread { block.txs.len(); "signer_sighash" => %block.header.signer_signature_hash(), "consensus_hash" => %block.header.consensus_hash, + "timestamp" => block.header.timestamp, ); self.event_dispatcher.process_mined_nakamoto_block_event( diff --git a/testnet/stacks-node/src/tests/signer/mod.rs b/testnet/stacks-node/src/tests/signer/mod.rs index 18a4ea40f3..12584ab89a 100644 --- a/testnet/stacks-node/src/tests/signer/mod.rs +++ b/testnet/stacks-node/src/tests/signer/mod.rs @@ -104,6 +104,15 @@ impl + Send + 'static, T: SignerEventTrait + 'static> SignerTest, wait_on_signers: Option, + ) -> Self { + Self::new_with_config_modifications(num_signers, initial_balances, wait_on_signers, |_| {}) + } + + fn new_with_config_modifications ()>( + num_signers: usize, + initial_balances: Vec<(StacksAddress, u64)>, + wait_on_signers: Option, + modifier: F, ) -> Self { // Generate Signer Data let signer_stacks_private_keys = (0..num_signers) @@ -148,8 +157,9 @@ impl + Send + 'static, T: SignerEventTrait + 'static> SignerTest + Send + 'static, T: SignerEventTrait + 'static> SignerTest. -use std::env; use std::sync::atomic::Ordering; use std::time::{Duration, Instant}; +use std::{env, thread}; use clarity::vm::types::PrincipalData; use libsigner::v0::messages::{ BlockRejection, BlockResponse, MessageSlotID, RejectCode, SignerMessage, }; use libsigner::{BlockProposal, SignerSession, StackerDBSession}; -use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader}; +use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader, NakamotoChainState}; use stacks::chainstate::stacks::boot::MINERS_NAME; +use stacks::chainstate::stacks::db::{StacksChainState, StacksHeaderInfo}; use stacks::codec::StacksMessageCodec; use stacks::libstackerdb::StackerDBChunkData; use stacks::net::api::postblock_proposal::TEST_VALIDATE_STALL; @@ -40,6 +41,9 @@ use tracing_subscriber::prelude::*; use tracing_subscriber::{fmt, EnvFilter}; use super::SignerTest; +use crate::event_dispatcher::MinedNakamotoBlockEvent; +use crate::nakamoto_node::miner::TEST_BROADCAST_STALL; +use crate::nakamoto_node::relayer::TEST_SKIP_COMMIT_OP; use crate::tests::nakamoto_integrations::{boot_to_epoch_3_reward_set, next_block_and}; use crate::tests::neon_integrations::{ get_chain_info, next_block_and_wait, submit_tx, test_observer, @@ -446,6 +450,343 @@ fn mine_2_nakamoto_reward_cycles() { signer_test.shutdown(); } +#[test] +#[ignore] +fn forked_tenure_invalid() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + let result = forked_tenure_testing(Duration::from_secs(5), Duration::from_secs(7), false); + + assert_ne!(result.tip_b, result.tip_a); + assert_eq!(result.tip_b, result.tip_c); + assert_ne!(result.tip_c, result.tip_a); + + // Block B was built atop block A + assert_eq!( + result.tip_b.stacks_block_height, + result.tip_a.stacks_block_height + 1 + ); + assert_eq!( + result.mined_b.parent_block_id, + result.tip_a.index_block_hash().to_string() + ); + + // Block C was built AFTER Block B was built, but BEFORE it was broadcasted, so it should be built off of Block A + assert_eq!( + result.mined_c.parent_block_id, + result.tip_a.index_block_hash().to_string() + ); + assert_ne!( + result + .tip_c + .anchored_header + .as_stacks_nakamoto() + .unwrap() + .signer_signature_hash(), + result.mined_c.signer_signature_hash, + "Mined block during tenure C should not have become the chain tip" + ); + + assert!(result.tip_c_2.is_none()); + assert!(result.mined_c_2.is_none()); + + // Tenure D should continue progress + assert_ne!(result.tip_c, result.tip_d); + assert_ne!(result.tip_b, result.tip_d); + assert_ne!(result.tip_a, result.tip_d); + + // Tenure D builds off of Tenure B + assert_eq!( + result.tip_d.stacks_block_height, + result.tip_b.stacks_block_height + 1, + ); + assert_eq!( + result.mined_d.parent_block_id, + result.tip_b.index_block_hash().to_string() + ); +} + +#[test] +#[ignore] +fn forked_tenure_okay() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let result = forked_tenure_testing(Duration::from_secs(360), Duration::from_secs(0), true); + + assert_ne!(result.tip_b, result.tip_a); + assert_ne!(result.tip_b, result.tip_c); + assert_ne!(result.tip_c, result.tip_a); + + // Block B was built atop block A + assert_eq!( + result.tip_b.stacks_block_height, + result.tip_a.stacks_block_height + 1 + ); + assert_eq!( + result.mined_b.parent_block_id, + result.tip_a.index_block_hash().to_string() + ); + + // Block C was built AFTER Block B was built, but BEFORE it was broadcasted, so it should be built off of Block A + assert_eq!( + result.tip_c.stacks_block_height, + result.tip_a.stacks_block_height + 1 + ); + assert_eq!( + result.mined_c.parent_block_id, + result.tip_a.index_block_hash().to_string() + ); + + let tenure_c_2 = result.tip_c_2.unwrap(); + assert_ne!(result.tip_c, tenure_c_2); + assert_ne!(tenure_c_2, result.tip_d); + assert_ne!(result.tip_c, result.tip_d); + + // Second block of tenure C builds off of block C + assert_eq!( + tenure_c_2.stacks_block_height, + result.tip_c.stacks_block_height + 1, + ); + assert_eq!( + result.mined_c_2.unwrap().parent_block_id, + result.tip_c.index_block_hash().to_string() + ); + + // Tenure D builds off of the second block of tenure C + assert_eq!( + result.tip_d.stacks_block_height, + tenure_c_2.stacks_block_height + 1, + ); + assert_eq!( + result.mined_d.parent_block_id, + tenure_c_2.index_block_hash().to_string() + ); +} + +struct TenureForkingResult { + tip_a: StacksHeaderInfo, + tip_b: StacksHeaderInfo, + tip_c: StacksHeaderInfo, + tip_c_2: Option, + tip_d: StacksHeaderInfo, + mined_b: MinedNakamotoBlockEvent, + mined_c: MinedNakamotoBlockEvent, + mined_c_2: Option, + mined_d: MinedNakamotoBlockEvent, +} + +/// This test spins up a nakamoto-neon node. +/// It starts in Epoch 2.0, mines with `neon_node` to Epoch 3.0, and then switches +/// to Nakamoto operation (activating pox-4 by submitting a stack-stx tx). The BootLoop +/// struct handles the epoch-2/3 tear-down and spin-up. +/// Miner A mines a regular tenure, its last block being block a_x. +/// Miner B starts its tenure, Miner B produces a Stacks block b_0, but miner C submits its block commit before b_0 is broadcasted. +/// Bitcoin block C, containing Miner C's block commit, is mined BEFORE miner C has a chance to update their block commit with b_0's information. +/// This test asserts: +/// * tenure C ignores b_0, and correctly builds off of block a_x. +fn forked_tenure_testing( + proposal_limit: Duration, + post_btc_block_pause: Duration, + expect_tenure_c: bool, +) -> TenureForkingResult { + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); + + let num_signers = 5; + let sender_sk = Secp256k1PrivateKey::new(); + let sender_addr = tests::to_addr(&sender_sk); + let send_amt = 100; + let send_fee = 180; + let recipient = PrincipalData::from(StacksAddress::burn_address(false)); + let mut signer_test: SignerTest = SignerTest::new_with_config_modifications( + num_signers, + vec![(sender_addr.clone(), send_amt + send_fee)], + Some(Duration::from_secs(15)), + |config| { + // make the duration long enough that the reorg attempt will definitely be accepted + config.first_proposal_burn_block_timing = proposal_limit; + }, + ); + let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind); + + signer_test.boot_to_epoch_3(); + info!("------------------------- Reached Epoch 3.0 -------------------------"); + + let naka_conf = signer_test.running_nodes.conf.clone(); + let burnchain = naka_conf.get_burnchain(); + let sortdb = burnchain.open_sortition_db(true).unwrap(); + let (chainstate, _) = StacksChainState::open( + naka_conf.is_mainnet(), + naka_conf.burnchain.chain_id, + &naka_conf.get_chainstate_path_str(), + None, + ) + .unwrap(); + + let commits_submitted = signer_test.running_nodes.commits_submitted.clone(); + let mined_blocks = signer_test.running_nodes.nakamoto_blocks_mined.clone(); + let proposed_blocks = signer_test.running_nodes.nakamoto_blocks_proposed.clone(); + + info!("Starting tenure A."); + // In the next block, the miner should win the tenure and submit a stacks block + let commits_before = commits_submitted.load(Ordering::SeqCst); + let blocks_before = mined_blocks.load(Ordering::SeqCst); + next_block_and( + &mut signer_test.running_nodes.btc_regtest_controller, + 60, + || { + let commits_count = commits_submitted.load(Ordering::SeqCst); + let blocks_count = mined_blocks.load(Ordering::SeqCst); + Ok(commits_count > commits_before && blocks_count > blocks_before) + }, + ) + .unwrap(); + + let tip_a = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) + .unwrap() + .unwrap(); + + // For the next tenure, submit the commit op but do not allow any stacks blocks to be broadcasted + TEST_BROADCAST_STALL.lock().unwrap().replace(true); + let blocks_before = mined_blocks.load(Ordering::SeqCst); + let commits_before = commits_submitted.load(Ordering::SeqCst); + info!("Starting tenure B."); + next_block_and( + &mut signer_test.running_nodes.btc_regtest_controller, + 60, + || { + let commits_count = commits_submitted.load(Ordering::SeqCst); + Ok(commits_count > commits_before) + }, + ) + .unwrap(); + + info!("Commit op is submitted; unpause tenure B's block"); + + // Unpause the broadcast of Tenure B's block, do not submit commits. + TEST_SKIP_COMMIT_OP.lock().unwrap().replace(true); + TEST_BROADCAST_STALL.lock().unwrap().replace(false); + + // Wait for a stacks block to be broadcasted + let start_time = Instant::now(); + while mined_blocks.load(Ordering::SeqCst) <= blocks_before { + assert!( + start_time.elapsed() < Duration::from_secs(30), + "FAIL: Test timed out while waiting for block production", + ); + thread::sleep(Duration::from_secs(1)); + } + + info!("Tenure B broadcasted a block. Wait {post_btc_block_pause:?}, issue the next bitcon block, and un-stall block commits."); + thread::sleep(post_btc_block_pause); + let tip_b = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) + .unwrap() + .unwrap(); + let blocks = test_observer::get_mined_nakamoto_blocks(); + let mined_b = blocks.last().unwrap().clone(); + + info!("Starting tenure C."); + // Submit a block commit op for tenure C + let commits_before = commits_submitted.load(Ordering::SeqCst); + let blocks_before = if expect_tenure_c { + mined_blocks.load(Ordering::SeqCst) + } else { + proposed_blocks.load(Ordering::SeqCst) + }; + next_block_and( + &mut signer_test.running_nodes.btc_regtest_controller, + 60, + || { + TEST_SKIP_COMMIT_OP.lock().unwrap().replace(false); + let commits_count = commits_submitted.load(Ordering::SeqCst); + let blocks_count = if expect_tenure_c { + mined_blocks.load(Ordering::SeqCst) + } else { + proposed_blocks.load(Ordering::SeqCst) + }; + Ok(commits_count > commits_before && blocks_count > blocks_before) + }, + ) + .unwrap(); + + info!("Tenure C produced (or proposed) a block!"); + let tip_c = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) + .unwrap() + .unwrap(); + + let blocks = test_observer::get_mined_nakamoto_blocks(); + let mined_c = blocks.last().unwrap().clone(); + + let (tip_c_2, mined_c_2) = if !expect_tenure_c { + (None, None) + } else { + // Now let's produce a second block for tenure C and ensure it builds off of block C. + let blocks_before = mined_blocks.load(Ordering::SeqCst); + let start_time = Instant::now(); + // submit a tx so that the miner will mine an extra block + let sender_nonce = 0; + let transfer_tx = + make_stacks_transfer(&sender_sk, sender_nonce, send_fee, &recipient, send_amt); + let tx = submit_tx(&http_origin, &transfer_tx); + info!("Submitted tx {tx} in Tenure C to mine a second block"); + while mined_blocks.load(Ordering::SeqCst) <= blocks_before { + assert!( + start_time.elapsed() < Duration::from_secs(30), + "FAIL: Test timed out while waiting for block production", + ); + thread::sleep(Duration::from_secs(1)); + } + + info!("Tenure C produced a second block!"); + + let block_2_tenure_c = + NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) + .unwrap() + .unwrap(); + let blocks = test_observer::get_mined_nakamoto_blocks(); + let block_2_c = blocks.last().cloned().unwrap(); + (Some(block_2_tenure_c), Some(block_2_c)) + }; + + info!("Starting tenure D."); + // Submit a block commit op for tenure D and mine a stacks block + let commits_before = commits_submitted.load(Ordering::SeqCst); + let blocks_before = mined_blocks.load(Ordering::SeqCst); + next_block_and( + &mut signer_test.running_nodes.btc_regtest_controller, + 60, + || { + let commits_count = commits_submitted.load(Ordering::SeqCst); + let blocks_count = mined_blocks.load(Ordering::SeqCst); + Ok(commits_count > commits_before && blocks_count > blocks_before) + }, + ) + .unwrap(); + + let tip_d = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb) + .unwrap() + .unwrap(); + let blocks = test_observer::get_mined_nakamoto_blocks(); + let mined_d = blocks.last().unwrap().clone(); + signer_test.shutdown(); + TenureForkingResult { + tip_a, + tip_b, + tip_c, + tip_c_2, + tip_d, + mined_b, + mined_c, + mined_c_2, + mined_d, + } +} + #[test] #[ignore] /// This test checks the behavior at the end of a tenure. Specifically: