diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index 11bcd3e4f3..475054b0e4 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -128,9 +128,9 @@ use crate::operations::BurnchainOpSigner; use crate::run_loop::boot_nakamoto; use crate::tests::neon_integrations::{ call_read_only, get_account, get_account_result, get_chain_info_opt, get_chain_info_result, - get_constant, get_neighbors, get_node_health, get_pox_info, get_sortition_info, - next_block_and_wait, run_until_burnchain_height, submit_tx, submit_tx_fallible, test_observer, - wait_for_runloop, + get_chain_tip_height, get_constant, get_neighbors, get_node_health, get_pox_info, + get_sortition_info, next_block_and_wait, run_until_burnchain_height, submit_tx, + submit_tx_fallible, test_observer, wait_for_runloop, wait_for_tenure_change_tx, }; use crate::tests::signer::SignerTest; use crate::tests::{gen_random_port, get_chain_info, make_contract_publish, to_addr}; @@ -18297,3 +18297,123 @@ fn smaller_tenure_size_for_miner_with_tenure_extend() { run_loop_thread.join().unwrap(); } + +#[test] +#[ignore] +/// The goal of this test is to ensure that a nakamoto miner is able to extend +/// its tenure when a new Bitcoin block arrives with no block commits (and thus +/// no new miner election). This should be true whether or not the miner has +/// submitted a valid block commit. We test: +/// 1. An empty Bitcoin block with no commits, even though the miner had +/// submitted a valid commit +/// 2. A Bitcoin block with an old commit from the previous tenure +/// 3. An empty Bitcoin block with no commits, and the miner never submitted +/// one. + +fn tenure_extend_no_commits() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let mut signers = TestSigners::default(); + let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); + naka_conf.connection_options.block_proposal_max_age_secs = u64::MAX; + naka_conf.miner.block_commit_delay = Duration::from_secs(600); + let http_origin = naka_conf.node.data_url.clone(); + let sender_signer_sk = Secp256k1PrivateKey::random(); + let sender_signer_addr = tests::to_addr(&sender_signer_sk); + naka_conf.add_initial_balance( + PrincipalData::from(sender_signer_addr.clone()).to_string(), + 100000, + ); + let stacker_sk = setup_stacker(&mut naka_conf); + + test_observer::spawn(); + test_observer::register_any(&mut naka_conf); + + let mut btcd_controller = BitcoinCoreController::from_stx_config(&naka_conf); + btcd_controller + .start_bitcoind() + .expect("Failed starting bitcoind"); + let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None); + btc_regtest_controller.bootstrap_chain(201); + + let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap(); + let run_loop_stopper = run_loop.get_termination_switch(); + let Counters { + blocks_processed, + naka_submitted_commits: commits_submitted, + .. + } = run_loop.counters(); + let counters = run_loop.counters(); + + let coord_channel = run_loop.coordinator_channels(); + + let run_loop_thread = thread::spawn(move || run_loop.start(None, 0)); + wait_for_runloop(&blocks_processed); + boot_to_epoch_3( + &naka_conf, + &blocks_processed, + &[stacker_sk.clone()], + &[sender_signer_sk], + &mut Some(&mut signers), + &mut btc_regtest_controller, + ); + + info!("Nakamoto miner started..."); + blind_signer(&naka_conf, &signers, &counters); + + wait_for_first_naka_block_commit(60, &commits_submitted); + + // Mine a regular nakamoto tenure + next_block_and_mine_commit(&mut btc_regtest_controller, 60, &naka_conf, &counters).unwrap(); + + let expected_height = get_chain_tip_height(&http_origin) + 1; + test_observer::clear(); + + // Skip block commits so that for the next block, there is no new commit + counters.naka_skip_commit_op.set(true); + + // Mine an empty Bitcoin block (no commits) + info!("1. Mining an empty Bitcoin block, even though the miner had submitted a valid commit"); + + btc_regtest_controller.build_empty_block(); + + // Wait for a Stacks block with a tenure extend + wait_for_tenure_change_tx(30, TenureChangeCause::Extended, expected_height) + .expect("Timed out waiting for tenure extend"); + + // assert that this produced a sortition without a winner + let sortition = get_sortition_info(&naka_conf); + assert!(!sortition.was_sortition); + + info!("2. Mining another Bitcoin block, which will contain the old block commit"); + let expected_height = get_chain_tip_height(&http_origin) + 1; + btc_regtest_controller.build_next_block(1); + + wait_for_tenure_change_tx(30, TenureChangeCause::Extended, expected_height) + .expect("Timed out waiting for tenure extend"); + + // assert that this produced a sortition without a winner + let sortition = get_sortition_info(&naka_conf); + assert!(!sortition.was_sortition); + + info!("3. Mining another Bitcoin block, which will contain no block commits"); + let expected_height = get_chain_tip_height(&http_origin) + 1; + btc_regtest_controller.build_next_block(1); + + wait_for_tenure_change_tx(30, TenureChangeCause::Extended, expected_height) + .expect("Timed out waiting for tenure extend"); + + // assert that this produced a sortition without a winner + let sortition = get_sortition_info(&naka_conf); + assert!(!sortition.was_sortition); + + coord_channel + .lock() + .expect("Mutex poisoned") + .stop_chains_coordinator(); + run_loop_stopper.store(false, Ordering::SeqCst); + + run_loop_thread.join().unwrap(); +} diff --git a/stacks-node/src/tests/neon_integrations.rs b/stacks-node/src/tests/neon_integrations.rs index cf349fdc30..fb6a860fb4 100644 --- a/stacks-node/src/tests/neon_integrations.rs +++ b/stacks-node/src/tests/neon_integrations.rs @@ -37,7 +37,7 @@ use stacks::chainstate::stacks::miner::{ }; use stacks::chainstate::stacks::{ StacksBlock, StacksBlockHeader, StacksMicroblock, StacksPrivateKey, StacksPublicKey, - StacksTransaction, TransactionContractCall, TransactionPayload, + StacksTransaction, TenureChangeCause, TransactionContractCall, TransactionPayload, }; use stacks::codec::StacksMessageCodec; use stacks::config::{EventKeyType, EventObserverConfig, FeeEstimatorName, InitialBalance}; @@ -1562,7 +1562,7 @@ fn get_chain_tip(http_origin: &str) -> (ConsensusHash, BlockHeaderHash) { ) } -fn get_chain_tip_height(http_origin: &str) -> u64 { +pub fn get_chain_tip_height(http_origin: &str) -> u64 { let client = reqwest::blocking::Client::new(); let path = format!("{http_origin}/v2/info"); let res = client @@ -9618,3 +9618,36 @@ fn mock_miner_replay() { miner_channel.stop_chains_coordinator(); follower_channel.stop_chains_coordinator(); } + +/// Waits for a tenure change transaction to be observed in the test_observer at the expected height +pub fn wait_for_tenure_change_tx( + timeout_secs: u64, + cause: TenureChangeCause, + expected_height: u64, +) -> Result { + let mut result = None; + wait_for(timeout_secs, || { + let blocks = test_observer::get_blocks(); + for block in blocks { + let height = block["block_height"].as_u64().unwrap(); + if height == expected_height { + let transactions = block["transactions"].as_array().unwrap(); + for tx in transactions { + let raw_tx = tx["raw_tx"].as_str().unwrap(); + let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); + let parsed = + StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); + if let TransactionPayload::TenureChange(payload) = &parsed.payload { + if payload.cause.is_eq(&cause) { + info!("Found tenure change transaction: {parsed:?}"); + result = Some(block); + return Ok(true); + } + } + } + } + } + Ok(false) + })?; + Ok(result.unwrap()) +} diff --git a/stacks-node/src/tests/signer/v0.rs b/stacks-node/src/tests/signer/v0.rs index c3aeaa886a..34c2b8cc21 100644 --- a/stacks-node/src/tests/signer/v0.rs +++ b/stacks-node/src/tests/signer/v0.rs @@ -133,7 +133,7 @@ use crate::tests::nakamoto_integrations::{ use crate::tests::neon_integrations::{ get_account, get_chain_info, get_chain_info_opt, get_sortition_info, get_sortition_info_ch, next_block_and_wait, run_until_burnchain_height, submit_tx, submit_tx_fallible, test_observer, - TestProxy, + wait_for_tenure_change_tx, TestProxy, }; use crate::tests::signer::commands::*; use crate::tests::signer::SpawnedSignerTrait; @@ -1228,39 +1228,6 @@ pub fn verify_sortition_winner(sortdb: &SortitionDB, miner_pkh: &Hash160) { assert_eq!(&tip.miner_pk_hash.unwrap(), miner_pkh); } -/// Waits for a tenure change transaction to be observed in the test_observer at the expected height -fn wait_for_tenure_change_tx( - timeout_secs: u64, - cause: TenureChangeCause, - expected_height: u64, -) -> Result { - let mut result = None; - wait_for(timeout_secs, || { - let blocks = test_observer::get_blocks(); - for block in blocks { - let height = block["block_height"].as_u64().unwrap(); - if height == expected_height { - let transactions = block["transactions"].as_array().unwrap(); - for tx in transactions { - let raw_tx = tx["raw_tx"].as_str().unwrap(); - let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap(); - let parsed = - StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - if let TransactionPayload::TenureChange(payload) = &parsed.payload { - if payload.cause.is_eq(&cause) { - info!("Found tenure change transaction: {parsed:?}"); - result = Some(block); - return Ok(true); - } - } - } - } - } - Ok(false) - })?; - Ok(result.unwrap()) -} - /// Waits for a block proposal to be observed in the test_observer stackerdb chunks at the expected height /// and signed by the expected miner pub fn wait_for_block_proposal(