Skip to content

Commit 59c7e7c

Browse files
authored
Merge pull request #5752 from stacks-network/feat/better-win-detection
Fix: better win detection on restart
2 parents 034a166 + 3e54697 commit 59c7e7c

File tree

4 files changed

+239
-2
lines changed

4 files changed

+239
-2
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ jobs:
170170
- tests::nakamoto_integrations::v3_blockbyheight_api_endpoint
171171
- tests::nakamoto_integrations::mine_invalid_principal_from_consensus_buff
172172
- tests::nakamoto_integrations::test_tenure_extend_from_flashblocks
173+
- tests::nakamoto_integrations::restarting_miner
173174
# TODO: enable these once v1 signer is supported by a new nakamoto epoch
174175
# - tests::signer::v1::dkg
175176
# - tests::signer::v1::sign_request_rejected

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1818

1919
- Miner will include other transactions in blocks with tenure extend transactions (#5760)
2020

21+
### Fixed
22+
23+
- Miners who restart their nodes immediately before a winning tenure now correctly detect that
24+
they won the tenure after their nodes restart ([#5750](https://github.com/stacks-network/stacks-core/issues/5750)).
25+
2126
## [3.1.0.0.4]
2227

2328
### Added

testnet/stacks-node/src/nakamoto_node/relayer.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,9 @@ impl RelayerThread {
694694
/// this sortition matches the sortition tip and we have a parent to build atop.
695695
///
696696
/// Otherwise, returns None, meaning no action will be taken.
697+
// This method is covered by the e2e bitcoind tests, which do not show up
698+
// in mutant coverage.
699+
#[cfg_attr(test, mutants::skip)]
697700
fn process_sortition(
698701
&mut self,
699702
consensus_hash: ConsensusHash,
@@ -705,8 +708,16 @@ impl RelayerThread {
705708
.expect("FATAL: unknown consensus hash");
706709

707710
// always clear this even if this isn't the latest sortition
708-
let cleared = self.last_commits.remove(&sn.winning_block_txid);
709-
let won_sortition = sn.sortition && cleared;
711+
let _cleared = self.last_commits.remove(&sn.winning_block_txid);
712+
let was_winning_pkh = if let (Some(ref winning_pkh), Some(ref my_pkh)) =
713+
(sn.miner_pk_hash, self.get_mining_key_pkh())
714+
{
715+
winning_pkh == my_pkh
716+
} else {
717+
false
718+
};
719+
720+
let won_sortition = sn.sortition && was_winning_pkh;
710721
if won_sortition {
711722
increment_stx_blocks_mined_counter();
712723
}

testnet/stacks-node/src/tests/nakamoto_integrations.rs

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,6 +1746,226 @@ fn simple_neon_integration() {
17461746
run_loop_thread.join().unwrap();
17471747
}
17481748

1749+
#[test]
1750+
#[ignore]
1751+
/// Test a scenario in which a miner is restarted right before a tenure
1752+
/// which they won. The miner, on restart, should begin mining the new tenure.
1753+
fn restarting_miner() {
1754+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
1755+
return;
1756+
}
1757+
1758+
let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None);
1759+
let prom_bind = "127.0.0.1:6000".to_string();
1760+
naka_conf.node.prometheus_bind = Some(prom_bind.clone());
1761+
naka_conf.miner.activated_vrf_key_path =
1762+
Some(format!("{}/vrf_key", naka_conf.node.working_dir));
1763+
naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(5);
1764+
let sender_sk = Secp256k1PrivateKey::from_seed(&[1, 2, 1, 2, 1, 2]);
1765+
// setup sender + recipient for a test stx transfer
1766+
let sender_addr = tests::to_addr(&sender_sk);
1767+
let send_amt = 1000;
1768+
let send_fee = 100;
1769+
naka_conf.add_initial_balance(
1770+
PrincipalData::from(sender_addr).to_string(),
1771+
send_amt * 2 + send_fee,
1772+
);
1773+
let sender_signer_sk = Secp256k1PrivateKey::from_seed(&[3, 2, 3, 2, 3, 2]);
1774+
let sender_signer_addr = tests::to_addr(&sender_signer_sk);
1775+
let mut signers = TestSigners::new(vec![sender_signer_sk]);
1776+
naka_conf.add_initial_balance(PrincipalData::from(sender_signer_addr).to_string(), 100000);
1777+
let stacker_sk = setup_stacker(&mut naka_conf);
1778+
1779+
test_observer::spawn();
1780+
test_observer::register_any(&mut naka_conf);
1781+
1782+
let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
1783+
btcd_controller
1784+
.start_bitcoind()
1785+
.expect("Failed starting bitcoind");
1786+
let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None);
1787+
btc_regtest_controller.bootstrap_chain(201);
1788+
1789+
let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap();
1790+
let run_loop_stopper = run_loop.get_termination_switch();
1791+
let Counters {
1792+
blocks_processed,
1793+
naka_submitted_commits: commits_submitted,
1794+
naka_proposed_blocks: proposals_submitted,
1795+
..
1796+
} = run_loop.counters();
1797+
let coord_channel = run_loop.coordinator_channels();
1798+
1799+
let mut run_loop_2 = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap();
1800+
let _run_loop_2_stopper = run_loop.get_termination_switch();
1801+
let Counters {
1802+
blocks_processed: blocks_processed_2,
1803+
naka_submitted_commits: commits_submitted_2,
1804+
naka_proposed_blocks: proposals_submitted_2,
1805+
..
1806+
} = run_loop_2.counters();
1807+
let coord_channel_2 = run_loop_2.coordinator_channels();
1808+
1809+
let run_loop_thread = thread::spawn(move || run_loop.start(None, 0));
1810+
wait_for_runloop(&blocks_processed);
1811+
boot_to_epoch_3(
1812+
&naka_conf,
1813+
&blocks_processed,
1814+
&[stacker_sk],
1815+
&[sender_signer_sk],
1816+
&mut Some(&mut signers),
1817+
&mut btc_regtest_controller,
1818+
);
1819+
1820+
info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner");
1821+
1822+
let burnchain = naka_conf.get_burnchain();
1823+
let sortdb = burnchain.open_sortition_db(true).unwrap();
1824+
let (chainstate, _) = StacksChainState::open(
1825+
naka_conf.is_mainnet(),
1826+
naka_conf.burnchain.chain_id,
1827+
&naka_conf.get_chainstate_path_str(),
1828+
None,
1829+
)
1830+
.unwrap();
1831+
1832+
let block_height_pre_3_0 =
1833+
NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
1834+
.unwrap()
1835+
.unwrap()
1836+
.stacks_block_height;
1837+
1838+
info!("Nakamoto miner started...");
1839+
blind_signer_multinode(
1840+
&signers,
1841+
&[&naka_conf, &naka_conf],
1842+
vec![proposals_submitted, proposals_submitted_2],
1843+
);
1844+
1845+
wait_for_first_naka_block_commit(60, &commits_submitted);
1846+
1847+
// Mine 2 nakamoto tenures
1848+
for _i in 0..2 {
1849+
next_block_and_mine_commit(
1850+
&mut btc_regtest_controller,
1851+
60,
1852+
&coord_channel,
1853+
&commits_submitted,
1854+
)
1855+
.unwrap();
1856+
}
1857+
1858+
let last_tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
1859+
.unwrap()
1860+
.unwrap();
1861+
info!(
1862+
"Latest tip";
1863+
"height" => last_tip.stacks_block_height,
1864+
"is_nakamoto" => last_tip.anchored_header.as_stacks_nakamoto().is_some(),
1865+
);
1866+
1867+
// close the current miner
1868+
coord_channel
1869+
.lock()
1870+
.expect("Mutex poisoned")
1871+
.stop_chains_coordinator();
1872+
run_loop_stopper.store(false, Ordering::SeqCst);
1873+
run_loop_thread.join().unwrap();
1874+
1875+
// mine a bitcoin block -- this should include a winning commit from
1876+
// the miner
1877+
btc_regtest_controller.build_next_block(1);
1878+
1879+
// start it back up
1880+
1881+
let _run_loop_thread = thread::spawn(move || run_loop_2.start(None, 0));
1882+
wait_for_runloop(&blocks_processed_2);
1883+
1884+
info!(" ================= RESTARTED THE MINER =================");
1885+
1886+
let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
1887+
.unwrap()
1888+
.unwrap();
1889+
info!(
1890+
"Latest tip";
1891+
"height" => tip.stacks_block_height,
1892+
"is_nakamoto" => tip.anchored_header.as_stacks_nakamoto().is_some(),
1893+
);
1894+
1895+
wait_for(60, || {
1896+
let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
1897+
.unwrap()
1898+
.unwrap();
1899+
Ok(tip.stacks_block_height > last_tip.stacks_block_height)
1900+
})
1901+
.unwrap_or_else(|e| {
1902+
let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
1903+
.unwrap()
1904+
.unwrap();
1905+
1906+
error!(
1907+
"Failed to get a new block after restart";
1908+
"last_tip_height" => last_tip.stacks_block_height,
1909+
"latest_tip" => tip.stacks_block_height,
1910+
"error" => &e,
1911+
);
1912+
1913+
panic!("{e}")
1914+
});
1915+
1916+
// Mine 2 more nakamoto tenures
1917+
for _i in 0..2 {
1918+
next_block_and_mine_commit(
1919+
&mut btc_regtest_controller,
1920+
60,
1921+
&coord_channel_2,
1922+
&commits_submitted_2,
1923+
)
1924+
.unwrap();
1925+
}
1926+
1927+
// load the chain tip, and assert that it is a nakamoto block and at least 30 blocks have advanced in epoch 3
1928+
let tip = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
1929+
.unwrap()
1930+
.unwrap();
1931+
info!(
1932+
"=== Last tip ===";
1933+
"height" => tip.stacks_block_height,
1934+
"is_nakamoto" => tip.anchored_header.as_stacks_nakamoto().is_some(),
1935+
);
1936+
1937+
assert!(tip.anchored_header.as_stacks_nakamoto().is_some());
1938+
1939+
// Check that we aren't missing burn blocks
1940+
let bhh = u64::from(tip.burn_header_height);
1941+
// make sure every burn block after the nakamoto transition has a mined
1942+
// nakamoto block in it.
1943+
let missing = test_observer::get_missing_burn_blocks(220..=bhh).unwrap();
1944+
1945+
// This test was flakey because it was sometimes missing burn block 230, which is right at the Nakamoto transition
1946+
// So it was possible to miss a burn block during the transition
1947+
// But I don't it matters at this point since the Nakamoto transition has already happened on mainnet
1948+
// So just print a warning instead, don't count it as an error
1949+
let missing_is_error: Vec<_> = missing
1950+
.into_iter()
1951+
.filter(|i| match i {
1952+
230 => {
1953+
warn!("Missing burn block {i}");
1954+
false
1955+
}
1956+
_ => true,
1957+
})
1958+
.collect();
1959+
1960+
if !missing_is_error.is_empty() {
1961+
panic!("Missing the following burn blocks: {missing_is_error:?}");
1962+
}
1963+
1964+
check_nakamoto_empty_block_heuristics();
1965+
1966+
assert!(tip.stacks_block_height >= block_height_pre_3_0 + 4);
1967+
}
1968+
17491969
#[test]
17501970
#[ignore]
17511971
/// This test spins up a nakamoto-neon node.

0 commit comments

Comments
 (0)