Skip to content

Commit e6b8ca9

Browse files
committed
fix: #5750 better win detection on restart
1 parent a5a6ce6 commit e6b8ca9

File tree

3 files changed

+237
-2
lines changed

3 files changed

+237
-2
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to the versioning scheme outlined in the [README.md](README.md).
77

8+
## [Unreleased]
9+
10+
### Fixed
11+
12+
- Miners who restart their nodes immediately before a winning tenure now correctly detect that
13+
they won the tenure after their nodes restart ([#5750](https://github.com/stacks-network/stacks-core/issues/5750)).
14+
815
## [3.1.0.0.4]
916

1017
### Added

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -705,8 +705,16 @@ impl RelayerThread {
705705
.expect("FATAL: unknown consensus hash");
706706

707707
// 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;
708+
let _cleared = self.last_commits.remove(&sn.winning_block_txid);
709+
let was_winning_pkh = if let (Some(ref winning_pkh), Some(ref my_pkh)) =
710+
(sn.miner_pk_hash, self.get_mining_key_pkh())
711+
{
712+
winning_pkh == my_pkh
713+
} else {
714+
false
715+
};
716+
717+
let won_sortition = sn.sortition && was_winning_pkh;
710718
if won_sortition {
711719
increment_stx_blocks_mined_counter();
712720
}

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

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1740,6 +1740,226 @@ fn simple_neon_integration() {
17401740
run_loop_thread.join().unwrap();
17411741
}
17421742

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

0 commit comments

Comments
 (0)