Skip to content

Commit dd2404a

Browse files
authored
Merge pull request #5134 from stacks-network/feat/utxo-check-loop
feat: retry check for UTXOs on startup
2 parents b723ac1 + 7ca1ce1 commit dd2404a

File tree

4 files changed

+190
-26
lines changed

4 files changed

+190
-26
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ jobs:
108108
- tests::nakamoto_integrations::continue_tenure_extend
109109
- tests::nakamoto_integrations::mock_mining
110110
- tests::nakamoto_integrations::multiple_miners
111+
- tests::nakamoto_integrations::utxo_check_on_startup_panic
112+
- tests::nakamoto_integrations::utxo_check_on_startup_recover
111113
# Do not run this one until we figure out why it fails in CI
112114
# - tests::neon_integrations::bitcoin_reorg_flap
113115
# - tests::neon_integrations::bitcoin_reorg_flap_with_follower

testnet/stacks-node/src/run_loop/nakamoto.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,11 @@ impl RunLoop {
155155
self.miner_status.clone()
156156
}
157157

158+
/// Seconds to wait before retrying UTXO check during startup
159+
const UTXO_RETRY_INTERVAL: u64 = 10;
160+
/// Number of times to retry UTXO check during startup
161+
const UTXO_RETRY_COUNT: u64 = 6;
162+
158163
/// Determine if we're the miner.
159164
/// If there's a network error, then assume that we're not a miner.
160165
fn check_is_miner(&mut self, burnchain: &mut BitcoinRegtestController) -> bool {
@@ -187,22 +192,26 @@ impl RunLoop {
187192
));
188193
}
189194

190-
for (epoch_id, btc_addr) in btc_addrs.into_iter() {
191-
info!("Miner node: checking UTXOs at address: {}", &btc_addr);
192-
let utxos = burnchain.get_utxos(epoch_id, &op_signer.get_public_key(), 1, None, 0);
193-
if utxos.is_none() {
194-
warn!("UTXOs not found for {}. If this is unexpected, please ensure that your bitcoind instance is indexing transactions for the address {} (importaddress)", btc_addr, btc_addr);
195-
} else {
196-
info!("UTXOs found - will run as a Miner node");
195+
// retry UTXO check a few times, in case bitcoind is still starting up
196+
for _ in 0..Self::UTXO_RETRY_COUNT {
197+
for (epoch_id, btc_addr) in &btc_addrs {
198+
info!("Miner node: checking UTXOs at address: {btc_addr}");
199+
let utxos =
200+
burnchain.get_utxos(*epoch_id, &op_signer.get_public_key(), 1, None, 0);
201+
if utxos.is_none() {
202+
warn!("UTXOs not found for {btc_addr}. If this is unexpected, please ensure that your bitcoind instance is indexing transactions for the address {btc_addr} (importaddress)");
203+
} else {
204+
info!("UTXOs found - will run as a Miner node");
205+
return true;
206+
}
207+
}
208+
if self.config.get_node_config(false).mock_mining {
209+
info!("No UTXOs found, but configured to mock mine");
197210
return true;
198211
}
212+
thread::sleep(std::time::Duration::from_secs(Self::UTXO_RETRY_INTERVAL));
199213
}
200-
if self.config.get_node_config(false).mock_mining {
201-
info!("No UTXOs found, but configured to mock mine");
202-
return true;
203-
} else {
204-
return false;
205-
}
214+
panic!("No UTXOs found, exiting");
206215
} else {
207216
info!("Will run as a Follower node");
208217
false

testnet/stacks-node/src/run_loop/neon.rs

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,11 @@ impl RunLoop {
358358
}
359359
}
360360

361+
/// Seconds to wait before retrying UTXO check during startup
362+
const UTXO_RETRY_INTERVAL: u64 = 10;
363+
/// Number of times to retry UTXO check during startup
364+
const UTXO_RETRY_COUNT: u64 = 6;
365+
361366
/// Determine if we're the miner.
362367
/// If there's a network error, then assume that we're not a miner.
363368
fn check_is_miner(&mut self, burnchain: &mut BitcoinRegtestController) -> bool {
@@ -390,22 +395,26 @@ impl RunLoop {
390395
));
391396
}
392397

393-
for (epoch_id, btc_addr) in btc_addrs.into_iter() {
394-
info!("Miner node: checking UTXOs at address: {}", &btc_addr);
395-
let utxos = burnchain.get_utxos(epoch_id, &op_signer.get_public_key(), 1, None, 0);
396-
if utxos.is_none() {
397-
warn!("UTXOs not found for {}. If this is unexpected, please ensure that your bitcoind instance is indexing transactions for the address {} (importaddress)", btc_addr, btc_addr);
398-
} else {
399-
info!("UTXOs found - will run as a Miner node");
398+
// retry UTXO check a few times, in case bitcoind is still starting up
399+
for _ in 0..Self::UTXO_RETRY_COUNT {
400+
for (epoch_id, btc_addr) in &btc_addrs {
401+
info!("Miner node: checking UTXOs at address: {btc_addr}");
402+
let utxos =
403+
burnchain.get_utxos(*epoch_id, &op_signer.get_public_key(), 1, None, 0);
404+
if utxos.is_none() {
405+
warn!("UTXOs not found for {btc_addr}. If this is unexpected, please ensure that your bitcoind instance is indexing transactions for the address {btc_addr} (importaddress)");
406+
} else {
407+
info!("UTXOs found - will run as a Miner node");
408+
return true;
409+
}
410+
}
411+
if self.config.get_node_config(false).mock_mining {
412+
info!("No UTXOs found, but configured to mock mine");
400413
return true;
401414
}
415+
thread::sleep(std::time::Duration::from_secs(Self::UTXO_RETRY_INTERVAL));
402416
}
403-
if self.config.get_node_config(false).mock_mining {
404-
info!("No UTXOs found, but configured to mock mine");
405-
return true;
406-
} else {
407-
return false;
408-
}
417+
panic!("No UTXOs found, exiting");
409418
} else {
410419
info!("Will run as a Follower node");
411420
false

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

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7552,3 +7552,147 @@ fn mock_mining() {
75527552
run_loop_thread.join().unwrap();
75537553
follower_thread.join().unwrap();
75547554
}
7555+
7556+
#[test]
7557+
#[ignore]
7558+
/// This test checks for the proper handling of the case where UTXOs are not
7559+
/// available on startup. After 1 minute, the miner thread should panic.
7560+
fn utxo_check_on_startup_panic() {
7561+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
7562+
return;
7563+
}
7564+
7565+
let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None);
7566+
println!("Nakamoto node started with config: {:?}", naka_conf);
7567+
let prom_bind = format!("{}:{}", "127.0.0.1", 6000);
7568+
naka_conf.node.prometheus_bind = Some(prom_bind.clone());
7569+
naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1000);
7570+
7571+
test_observer::spawn();
7572+
let observer_port = test_observer::EVENT_OBSERVER_PORT;
7573+
naka_conf.events_observers.insert(EventObserverConfig {
7574+
endpoint: format!("localhost:{observer_port}"),
7575+
events_keys: vec![EventKeyType::AnyEvent],
7576+
});
7577+
7578+
let mut epochs = NAKAMOTO_INTEGRATION_EPOCHS.to_vec();
7579+
let (last, rest) = epochs.split_last_mut().unwrap();
7580+
for (index, epoch) in rest.iter_mut().enumerate() {
7581+
epoch.start_height = index as u64;
7582+
epoch.end_height = (index + 1) as u64;
7583+
}
7584+
last.start_height = 131;
7585+
7586+
let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
7587+
btcd_controller
7588+
.start_bitcoind()
7589+
.expect("Failed starting bitcoind");
7590+
let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None);
7591+
// Do not fully bootstrap the chain, so that the UTXOs are not yet available
7592+
btc_regtest_controller.bootstrap_chain(99);
7593+
7594+
let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap();
7595+
let run_loop_stopper = run_loop.get_termination_switch();
7596+
let coord_channel = run_loop.coordinator_channels();
7597+
7598+
let run_loop_thread = thread::spawn(move || run_loop.start(None, 0));
7599+
7600+
let timeout = Duration::from_secs(70);
7601+
let start_time = Instant::now();
7602+
7603+
loop {
7604+
// Check if the thread has panicked
7605+
if run_loop_thread.is_finished() {
7606+
match run_loop_thread.join() {
7607+
Ok(_) => {
7608+
// Thread completed without panicking
7609+
panic!("Miner should have panicked but it exited cleanly.");
7610+
}
7611+
Err(_) => {
7612+
// Thread panicked
7613+
info!("Thread has panicked!");
7614+
break;
7615+
}
7616+
}
7617+
}
7618+
7619+
// Check if 70 seconds have passed
7620+
assert!(
7621+
start_time.elapsed() < timeout,
7622+
"Miner should have panicked."
7623+
);
7624+
7625+
thread::sleep(Duration::from_millis(1000));
7626+
}
7627+
7628+
coord_channel
7629+
.lock()
7630+
.expect("Mutex poisoned")
7631+
.stop_chains_coordinator();
7632+
run_loop_stopper.store(false, Ordering::SeqCst);
7633+
}
7634+
7635+
#[test]
7636+
#[ignore]
7637+
/// This test checks for the proper handling of the case where UTXOs are not
7638+
/// available on startup, but become available later, before the 1 minute
7639+
/// timeout. The miner thread should recover and continue mining.
7640+
fn utxo_check_on_startup_recover() {
7641+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
7642+
return;
7643+
}
7644+
7645+
let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None);
7646+
println!("Nakamoto node started with config: {:?}", naka_conf);
7647+
let prom_bind = format!("{}:{}", "127.0.0.1", 6000);
7648+
naka_conf.node.prometheus_bind = Some(prom_bind.clone());
7649+
naka_conf.miner.wait_on_interim_blocks = Duration::from_secs(1000);
7650+
7651+
test_observer::spawn();
7652+
let observer_port = test_observer::EVENT_OBSERVER_PORT;
7653+
naka_conf.events_observers.insert(EventObserverConfig {
7654+
endpoint: format!("localhost:{observer_port}"),
7655+
events_keys: vec![EventKeyType::AnyEvent],
7656+
});
7657+
7658+
let mut epochs = NAKAMOTO_INTEGRATION_EPOCHS.to_vec();
7659+
let (last, rest) = epochs.split_last_mut().unwrap();
7660+
for (index, epoch) in rest.iter_mut().enumerate() {
7661+
epoch.start_height = index as u64;
7662+
epoch.end_height = (index + 1) as u64;
7663+
}
7664+
last.start_height = 131;
7665+
7666+
let mut btcd_controller = BitcoinCoreController::new(naka_conf.clone());
7667+
btcd_controller
7668+
.start_bitcoind()
7669+
.expect("Failed starting bitcoind");
7670+
let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None);
7671+
// Do not fully bootstrap the chain, so that the UTXOs are not yet available
7672+
btc_regtest_controller.bootstrap_chain(99);
7673+
// btc_regtest_controller.bootstrap_chain(108);
7674+
7675+
let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap();
7676+
let run_loop_stopper = run_loop.get_termination_switch();
7677+
let Counters {
7678+
blocks_processed, ..
7679+
} = run_loop.counters();
7680+
7681+
let coord_channel = run_loop.coordinator_channels();
7682+
7683+
let run_loop_thread = thread::spawn(move || run_loop.start(None, 0));
7684+
7685+
// Sleep for 30s to allow the miner to start and reach the UTXO check loop
7686+
thread::sleep(Duration::from_secs(30));
7687+
7688+
btc_regtest_controller.bootstrap_chain(3);
7689+
7690+
wait_for_runloop(&blocks_processed);
7691+
7692+
coord_channel
7693+
.lock()
7694+
.expect("Mutex poisoned")
7695+
.stop_chains_coordinator();
7696+
run_loop_stopper.store(false, Ordering::SeqCst);
7697+
run_loop_thread.join().unwrap();
7698+
}

0 commit comments

Comments
 (0)