Skip to content

Commit

Permalink
WIP: add a test for continue tenure
Browse files Browse the repository at this point in the history
Signed-off-by: Jacinta Ferrant <jacinta@trustmachines.co>
  • Loading branch information
jferrant committed May 30, 2024
1 parent aa71172 commit 0f67c5b
Show file tree
Hide file tree
Showing 2 changed files with 269 additions and 15 deletions.
35 changes: 20 additions & 15 deletions testnet/stacks-node/src/nakamoto_node/relayer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,15 @@ impl RelayerThread {
error!("Relayer: failed to get last sortition snapshot: {e:?}");
NakamotoNodeError::SnapshotNotFoundForChainTip
})?;

if Some(block_snapshot.winning_block_txid) != self.current_mining_commit_tx {
debug!("Relayer: the miner did not win the last sortition. No tenure to continue.";
"current_mining_commit_tx" => %self.current_mining_commit_tx.unwrap_or(Txid([0u8; 32])),
"block_snapshot_winning_block_txid" => %block_snapshot.winning_block_txid
);
return Ok(());
};

let Some(block_header) = NakamotoChainState::get_block_header_by_consensus_hash(
self.chainstate.db(),
&block_snapshot.consensus_hash,
Expand All @@ -670,17 +679,12 @@ impl RelayerThread {
error!("Relayer: failed to get block header for the last sortition snapshsot: {e:?}");
NakamotoNodeError::MissingTenureStartBlockHeader
})?
else {
error!("Relayer: failed to get block header for the last sortition snapshsot");
return Err(NakamotoNodeError::MissingTenureStartBlockHeader);
};

if Some(block_snapshot.winning_block_txid) != self.current_mining_commit_tx {
debug!("Relayer: the miner did not win the last sortition. No tenure to continue.");
return Ok(());
};
.ok_or_else(|| {
error!("Relayer: failed to find block header for the last sortition snapshsot");
NakamotoNodeError::MissingTenureStartBlockHeader
})?;

let Some(last_parent_tenure_header) =
let last_parent_tenure_header =
NakamotoChainState::get_nakamoto_tenure_finish_block_header(
self.chainstate.db(),
&block_header.consensus_hash,
Expand All @@ -689,10 +693,11 @@ impl RelayerThread {
error!("Relayer: failed to get last block of parent tenure: {e:?}");
NakamotoNodeError::ParentNotFound
})?
else {
warn!("Failed loading last block of parent tenure"; "consensus_hash" => %block_header.consensus_hash);
return Err(NakamotoNodeError::ParentNotFound);
};
.ok_or_else(|| {
error!("Relayer: failed to find block header for parent tenure");
NakamotoNodeError::ParentNotFound
})?;

let parent_tenure_info = ParentTenureInfo {
parent_tenure_blocks: 1 + last_parent_tenure_header.stacks_block_height
- block_header.stacks_block_height,
Expand Down Expand Up @@ -744,7 +749,7 @@ impl RelayerThread {
MinerDirective::ContinueTenure { new_burn_view } => {
match self.continue_tenure(new_burn_view) {
Ok(()) => {
debug!("Relayer: handled continue tenure.");
debug!("Relayer: successfully handled continue tenure.");
}
Err(e) => {
error!("Relayer: Failed to continue tenure: {:?}", e);
Expand Down
249 changes: 249 additions & 0 deletions testnet/stacks-node/src/tests/nakamoto_integrations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3895,3 +3895,252 @@ fn check_block_heights() {

run_loop_thread.join().unwrap();
}

#[test]
#[ignore]
/// 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.
/// This test makes three assertions:
/// * 30 blocks are mined after 3.0 starts. This is enough to mine across 2 reward cycles
/// * A transaction submitted to the mempool in 3.0 will be mined in 3.0
/// * The final chain tip is a nakamoto block
fn continue_tenure_extend() {
if env::var("BITCOIND_TEST") != Ok("1".into()) {
return;
}

let signers = TestSigners::default();
let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None);
let prom_bind = format!("{}:{}", "127.0.0.1", 6000);
naka_conf.node.prometheus_bind = Some(prom_bind.clone());
naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1000);
let sender_sk = Secp256k1PrivateKey::new();
// setup sender + recipient for a test stx transfer
let sender_addr = tests::to_addr(&sender_sk);
let send_amt = 1000;
let send_fee = 100;
naka_conf.add_initial_balance(
PrincipalData::from(sender_addr.clone()).to_string(),
send_amt * 2 + send_fee,
);
let sender_signer_sk = Secp256k1PrivateKey::new();
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 recipient = PrincipalData::from(StacksAddress::burn_address(false));
let stacker_sk = setup_stacker(&mut naka_conf);

test_observer::spawn();
let observer_port = test_observer::EVENT_OBSERVER_PORT;
naka_conf.events_observers.insert(EventObserverConfig {
endpoint: format!("localhost:{observer_port}"),
events_keys: vec![EventKeyType::AnyEvent],
});

let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
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_vrfs: vrfs_submitted,
naka_submitted_commits: commits_submitted,
naka_proposed_blocks: proposals_submitted,
..
} = 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],
&[sender_signer_sk],
Some(&signers),
&mut btc_regtest_controller,
);

info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner");

let burnchain = naka_conf.get_burnchain();
let sortdb = burnchain.open_sortition_db(true).unwrap();
let (mut chainstate, _) = StacksChainState::open(
naka_conf.is_mainnet(),
naka_conf.burnchain.chain_id,
&naka_conf.get_chainstate_path_str(),
None,
)
.unwrap();

let block_height_pre_3_0 =
NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
.unwrap()
.unwrap()
.stacks_block_height;

// query for prometheus metrics
#[cfg(feature = "monitoring_prom")]
{
let prom_http_origin = format!("http://{}", prom_bind);
let client = reqwest::blocking::Client::new();
let res = client
.get(&prom_http_origin)
.send()
.unwrap()
.text()
.unwrap();
let expected_result = format!("stacks_node_stacks_tip_height {block_height_pre_3_0}");
assert!(res.contains(&expected_result));
}

info!("Nakamoto miner started...");
blind_signer(&naka_conf, &signers, proposals_submitted);

// first block wakes up the run loop, wait until a key registration has been submitted.
next_block_and(&mut btc_regtest_controller, 60, || {
let vrf_count = vrfs_submitted.load(Ordering::SeqCst);
Ok(vrf_count >= 1)
})
.unwrap();

// second block should confirm the VRF register, wait until a block commit is submitted
next_block_and(&mut btc_regtest_controller, 60, || {
let commits_count = commits_submitted.load(Ordering::SeqCst);
Ok(commits_count >= 1)
})
.unwrap();

// Mine a regular nakamoto tenures
next_block_and_mine_commit(
&mut btc_regtest_controller,
60,
&coord_channel,
&commits_submitted,
)
.unwrap();

signer_vote_if_needed(
&btc_regtest_controller,
&naka_conf,
&[sender_signer_sk],
&signers,
);

TEST_SKIP_COMMIT_OP.lock().unwrap().replace(true);

next_block_and(&mut btc_regtest_controller, 60, || Ok(true)).unwrap();

signer_vote_if_needed(
&btc_regtest_controller,
&naka_conf,
&[sender_signer_sk],
&signers,
);

TEST_SKIP_COMMIT_OP.lock().unwrap().replace(false);
// Submit a TX
let transfer_tx = make_stacks_transfer(&sender_sk, 0, send_fee, &recipient, send_amt);
let transfer_tx_hex = format!("0x{}", to_hex(&transfer_tx));

let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
.unwrap()
.unwrap();

let mut mempool = naka_conf
.connect_mempool_db()
.expect("Database failure opening mempool");

mempool
.submit_raw(
&mut chainstate,
&sortdb,
&tip.consensus_hash,
&tip.anchored_header.block_hash(),
transfer_tx.clone(),
&ExecutionCost::max_value(),
&StacksEpochId::Epoch30,
)
.unwrap();
// Mine 15 more nakamoto tenures
for _i in 0..15 {
next_block_and_mine_commit(
&mut btc_regtest_controller,
60,
&coord_channel,
&commits_submitted,
)
.unwrap();

signer_vote_if_needed(
&btc_regtest_controller,
&naka_conf,
&[sender_signer_sk],
&signers,
);
}

// load the chain tip, and assert that it is a nakamoto block and at least 30 blocks have advanced in epoch 3
let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
.unwrap()
.unwrap();
info!(
"Latest tip";
"height" => tip.stacks_block_height,
"is_nakamoto" => tip.anchored_header.as_stacks_nakamoto().is_some(),
);

// assert that the transfer tx was observed
let transfer_tx_included = test_observer::get_blocks()
.into_iter()
.find(|block_json| {
block_json["transactions"]
.as_array()
.unwrap()
.iter()
.find(|tx_json| tx_json["raw_tx"].as_str() == Some(&transfer_tx_hex))
.is_some()
})
.is_some();

assert!(
transfer_tx_included,
"Nakamoto node failed to include the transfer tx"
);

assert!(tip.anchored_header.as_stacks_nakamoto().is_some());
assert!(tip.stacks_block_height >= block_height_pre_3_0 + 30);

// make sure prometheus returns an updated height
#[cfg(feature = "monitoring_prom")]
{
let prom_http_origin = format!("http://{}", prom_bind);
let client = reqwest::blocking::Client::new();
let res = client
.get(&prom_http_origin)
.send()
.unwrap()
.text()
.unwrap();
let expected_result = format!("stacks_node_stacks_tip_height {}", tip.stacks_block_height);
assert!(res.contains(&expected_result));
}

coord_channel
.lock()
.expect("Mutex poisoned")
.stop_chains_coordinator();
run_loop_stopper.store(false, Ordering::SeqCst);

run_loop_thread.join().unwrap();
}

0 comments on commit 0f67c5b

Please sign in to comment.