Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 123 additions & 3 deletions stacks-node/src/tests/nakamoto_integrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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();
}
37 changes: 35 additions & 2 deletions stacks-node/src/tests/neon_integrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<serde_json::Value, String> {
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())
}
35 changes: 1 addition & 34 deletions stacks-node/src/tests/signer/v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<serde_json::Value, String> {
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(
Expand Down
Loading