• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacks-network / stacks-core / 26250451051-1

21 May 2026 08:11PM UTC coverage: 85.585% (-0.1%) from 85.712%
26250451051-1

Pull #7215

github

ec9d4c
web-flow
Merge 9487bf852 into af1280aac
Pull Request #7215: Chore: fix flake in non_blocking_minority_configured_to_favour_...

188844 of 220651 relevant lines covered (85.58%)

18975267.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.09
/stacks-node/src/run_loop/nakamoto.rs
1
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2
// Copyright (C) 2020-2026 Stacks Open Internet Foundation
3
//
4
// This program is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// This program is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
use std::sync::atomic::AtomicBool;
17
use std::sync::mpsc::sync_channel;
18
use std::sync::{Arc, Mutex};
19
use std::thread::JoinHandle;
20
use std::{cmp, thread};
21

22
use stacks::burnchains::bitcoin::address::{BitcoinAddress, LegacyBitcoinAddressType};
23
use stacks::burnchains::{Burnchain, Error as burnchain_error};
24
use stacks::chainstate::burn::db::sortdb::SortitionDB;
25
use stacks::chainstate::burn::BlockSnapshot;
26
use stacks::chainstate::coordinator::comm::{CoordinatorChannels, CoordinatorReceivers};
27
use stacks::chainstate::coordinator::{
28
    ChainsCoordinator, ChainsCoordinatorConfig, CoordinatorCommunication,
29
};
30
use stacks::chainstate::stacks::db::{ChainStateBootData, StacksChainState};
31
use stacks::chainstate::stacks::miner::{signal_mining_blocked, signal_mining_ready, MinerStatus};
32
use stacks::core::StacksEpochId;
33
use stacks::net::atlas::{AtlasConfig, AtlasDB, Attachment};
34
use stacks_common::types::PublicKey;
35
use stacks_common::util::hash::Hash160;
36
use stacks_common::util::{get_epoch_time_secs, sleep_ms};
37
use stx_genesis::GenesisData;
38

39
use crate::burnchains::make_bitcoin_indexer;
40
use crate::globals::Globals as GenericGlobals;
41
use crate::monitoring::{start_serving_monitoring_metrics, MonitoringError};
42
use crate::nakamoto_node::{self, StacksNode, BLOCK_PROCESSOR_STACK_SIZE, RELAYER_MAX_BUFFER};
43
use crate::neon_node::LeaderKeyRegistrationState;
44
use crate::node::{
45
    get_account_balances, get_account_lockups, get_names, get_namespaces,
46
    use_test_genesis_chainstate,
47
};
48
use crate::run_loop::boot_nakamoto::Neon2NakaData;
49
use crate::run_loop::neon;
50
use crate::run_loop::neon::Counters;
51
use crate::syncctl::{PoxSyncWatchdog, PoxSyncWatchdogComms};
52
use crate::{
53
    run_loop, BitcoinRegtestController, BurnchainController, Config, EventDispatcher, Keychain,
54
};
55

56
pub const STDERR: i32 = 2;
57
pub type Globals = GenericGlobals<nakamoto_node::relayer::RelayerDirective>;
58

59
/// Coordinating a node running in nakamoto mode. This runloop operates very similarly to the neon runloop.
60
pub struct RunLoop {
61
    config: Config,
62
    globals: Option<Globals>,
63
    counters: Counters,
64
    coordinator_channels: Option<(CoordinatorReceivers, CoordinatorChannels)>,
65
    should_keep_running: Arc<AtomicBool>,
66
    event_dispatcher: EventDispatcher,
67
    #[allow(dead_code)]
68
    pox_watchdog: Option<PoxSyncWatchdog>, // can't be instantiated until .start() is called
69
    is_miner: Option<bool>,       // not known until .start() is called
70
    burnchain: Option<Burnchain>, // not known until .start() is called
71
    pox_watchdog_comms: PoxSyncWatchdogComms,
72
    /// NOTE: this is duplicated in self.globals, but it needs to be accessible before globals is
73
    /// instantiated (namely, so the test framework can access it).
74
    miner_status: Arc<Mutex<MinerStatus>>,
75
    monitoring_thread: Option<JoinHandle<Result<(), MonitoringError>>>,
76
}
77

78
impl RunLoop {
79
    /// Sets up a runloop and node, given a config.
80
    ///
81
    /// If no event_dispatcher is passed, a new one is created. Allowing one to be passed in
82
    /// allows the nakamoto runloop to continue using the same event dispatcher as the
83
    /// neon runloop at the epoch 2->3 transition.
84
    pub fn new(
242✔
85
        config: Config,
242✔
86
        should_keep_running: Option<Arc<AtomicBool>>,
242✔
87
        counters: Option<Counters>,
242✔
88
        monitoring_thread: Option<JoinHandle<Result<(), MonitoringError>>>,
242✔
89
        event_dispatcher: Option<EventDispatcher>,
242✔
90
    ) -> Self {
242✔
91
        let channels = CoordinatorCommunication::instantiate();
242✔
92
        let should_keep_running =
242✔
93
            should_keep_running.unwrap_or_else(|| Arc::new(AtomicBool::new(true)));
242✔
94
        let pox_watchdog_comms = PoxSyncWatchdogComms::new(should_keep_running.clone());
242✔
95
        let miner_status = Arc::new(Mutex::new(MinerStatus::make_ready(
242✔
96
            config.burnchain.burn_fee_cap,
242✔
97
        )));
98

99
        let event_dispatcher = event_dispatcher.unwrap_or_else(|| {
242✔
100
            let mut event_dispatcher = EventDispatcher::new_with_custom_queue_size(
2✔
101
                config.get_working_dir(),
2✔
102
                config.node.effective_event_dispatcher_queue_size(),
2✔
103
            );
104
            for observer in config.events_observers.iter() {
2✔
105
                event_dispatcher.register_observer(observer);
×
106
            }
×
107
            event_dispatcher
2✔
108
        });
2✔
109

110
        Self {
242✔
111
            config,
242✔
112
            globals: None,
242✔
113
            coordinator_channels: Some(channels),
242✔
114
            counters: counters.unwrap_or_default(),
242✔
115
            should_keep_running,
242✔
116
            event_dispatcher,
242✔
117
            pox_watchdog: None,
242✔
118
            is_miner: None,
242✔
119
            burnchain: None,
242✔
120
            pox_watchdog_comms,
242✔
121
            miner_status,
242✔
122
            monitoring_thread,
242✔
123
        }
242✔
124
    }
242✔
125

126
    pub(crate) fn get_globals(&self) -> Globals {
484✔
127
        self.globals
484✔
128
            .clone()
484✔
129
            .expect("FATAL: globals not instantiated")
484✔
130
    }
484✔
131

132
    fn set_globals(&mut self, globals: Globals) {
242✔
133
        self.globals = Some(globals);
242✔
134
    }
242✔
135

136
    pub(crate) fn get_coordinator_channel(&self) -> Option<CoordinatorChannels> {
242✔
137
        self.coordinator_channels.as_ref().map(|x| x.1.clone())
242✔
138
    }
242✔
139

140
    pub(crate) fn get_counters(&self) -> Counters {
2✔
141
        self.counters.clone()
2✔
142
    }
2✔
143

144
    pub(crate) fn config(&self) -> &Config {
25,496✔
145
        &self.config
25,496✔
146
    }
25,496✔
147

148
    pub(crate) fn get_event_dispatcher(&self) -> EventDispatcher {
484✔
149
        self.event_dispatcher.clone()
484✔
150
    }
484✔
151

152
    pub(crate) fn is_miner(&self) -> bool {
484✔
153
        self.is_miner.unwrap_or(false)
484✔
154
    }
484✔
155

156
    pub(crate) fn get_termination_switch(&self) -> Arc<AtomicBool> {
2✔
157
        self.should_keep_running.clone()
2✔
158
    }
2✔
159

160
    pub(crate) fn get_burnchain(&self) -> Burnchain {
968✔
161
        self.burnchain
968✔
162
            .clone()
968✔
163
            .expect("FATAL: tried to get runloop burnchain before calling .start()")
968✔
164
    }
968✔
165

166
    pub(crate) fn get_miner_status(&self) -> Arc<Mutex<MinerStatus>> {
242✔
167
        self.miner_status.clone()
242✔
168
    }
242✔
169

170
    /// Seconds to wait before retrying UTXO check during startup
171
    const UTXO_RETRY_INTERVAL: u64 = 10;
172
    /// Number of times to retry UTXO check during startup
173
    const UTXO_RETRY_COUNT: u64 = 6;
174

175
    /// Determine if we're the miner.
176
    /// If there's a network error, then assume that we're not a miner.
177
    fn check_is_miner(&mut self, burnchain: &mut BitcoinRegtestController) -> bool {
242✔
178
        if self.config.node.miner {
242✔
179
            // If we are mock mining, then we don't need to check for UTXOs and
180
            // we can just return true.
181
            if self.config.get_node_config(false).mock_mining {
238✔
182
                return true;
5✔
183
            }
233✔
184
            let keychain = Keychain::default(self.config.node.seed.clone());
233✔
185
            let mut op_signer = keychain.generate_op_signer();
233✔
186
            if let Err(e) = burnchain.create_wallet_if_dne() {
233✔
187
                warn!("Error when creating wallet: {e:?}");
×
188
            }
233✔
189
            let mut btc_addrs = vec![(
233✔
190
                StacksEpochId::Epoch2_05,
233✔
191
                // legacy
233✔
192
                BitcoinAddress::from_bytes_legacy(
233✔
193
                    self.config.burnchain.get_bitcoin_network().1,
233✔
194
                    LegacyBitcoinAddressType::PublicKeyHash,
233✔
195
                    &Hash160::from_data(&op_signer.get_public_key().to_bytes()).0,
233✔
196
                )
233✔
197
                .expect("FATAL: failed to construct legacy bitcoin address"),
233✔
198
            )];
233✔
199
            if self.config.miner.segwit {
233✔
200
                btc_addrs.push((
×
201
                    StacksEpochId::Epoch21,
×
202
                    // segwit p2wpkh
×
203
                    BitcoinAddress::from_bytes_segwit_p2wpkh(
×
204
                        self.config.burnchain.get_bitcoin_network().1,
×
205
                        &Hash160::from_data(&op_signer.get_public_key().to_bytes_compressed()).0,
×
206
                    )
×
207
                    .expect("FATAL: failed to construct segwit p2wpkh address"),
×
208
                ));
×
209
            }
233✔
210

211
            // retry UTXO check a few times, in case bitcoind is still starting up
212
            for _ in 0..Self::UTXO_RETRY_COUNT {
233✔
213
                for (epoch_id, btc_addr) in &btc_addrs {
233✔
214
                    info!("Miner node: checking UTXOs at address: {btc_addr}");
233✔
215
                    let utxos =
233✔
216
                        burnchain.get_utxos(*epoch_id, &op_signer.get_public_key(), 1, None, 0);
233✔
217
                    if utxos.is_none() {
233✔
218
                        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)");
×
219
                    } else {
220
                        info!("UTXOs found - will run as a Miner node");
233✔
221
                        return true;
233✔
222
                    }
223
                }
224
                thread::sleep(std::time::Duration::from_secs(Self::UTXO_RETRY_INTERVAL));
×
225
            }
226
            panic!("No UTXOs found, exiting");
×
227
        } else {
228
            info!("Will run as a Follower node");
4✔
229
            false
4✔
230
        }
231
    }
242✔
232

233
    /// Boot up the stacks chainstate.
234
    /// Instantiate the chainstate and push out the boot receipts to observers
235
    /// This is only public so we can test it.
236
    fn boot_chainstate(&mut self, burnchain_config: &Burnchain) -> StacksChainState {
242✔
237
        let use_test_genesis_data = use_test_genesis_chainstate(&self.config);
242✔
238

239
        // load up genesis balances
240
        let initial_balances = self
242✔
241
            .config
242✔
242
            .initial_balances
242✔
243
            .iter()
242✔
244
            .map(|e| (e.address.clone(), e.amount))
2,021✔
245
            .collect();
242✔
246

247
        // instantiate chainstate
248
        let mut boot_data = ChainStateBootData {
242✔
249
            initial_balances,
242✔
250
            post_flight_callback: None,
242✔
251
            first_burnchain_block_hash: burnchain_config.first_block_hash.clone(),
242✔
252
            first_burnchain_block_height: burnchain_config.first_block_height as u32,
242✔
253
            first_burnchain_block_timestamp: burnchain_config.first_block_timestamp,
242✔
254
            pox_constants: burnchain_config.pox_constants.clone(),
242✔
255
            get_bulk_initial_lockups: Some(Box::new(move || {
242✔
256
                get_account_lockups(use_test_genesis_data)
×
257
            })),
×
258
            get_bulk_initial_balances: Some(Box::new(move || {
242✔
259
                get_account_balances(use_test_genesis_data)
×
260
            })),
×
261
            get_bulk_initial_namespaces: Some(Box::new(move || {
242✔
262
                get_namespaces(use_test_genesis_data)
×
263
            })),
×
264
            get_bulk_initial_names: Some(Box::new(move || get_names(use_test_genesis_data))),
242✔
265
        };
266

267
        let (chain_state_db, receipts) = StacksChainState::open_and_exec(
242✔
268
            self.config.is_mainnet(),
242✔
269
            self.config.burnchain.chain_id,
242✔
270
            &self.config.get_chainstate_path_str(),
242✔
271
            Some(&mut boot_data),
242✔
272
            Some(self.config.node.get_marf_opts()),
242✔
273
        )
242✔
274
        .unwrap();
242✔
275
        run_loop::announce_boot_receipts(
242✔
276
            &mut self.event_dispatcher,
242✔
277
            &chain_state_db,
242✔
278
            &burnchain_config.pox_constants,
242✔
279
            &receipts,
242✔
280
        );
281
        chain_state_db
242✔
282
    }
242✔
283

284
    /// Instantiate the Stacks chain state and start the chains coordinator thread.
285
    /// Returns the coordinator thread handle, and the receiving end of the coordinator's atlas
286
    /// attachment channel.
287
    fn spawn_chains_coordinator(
242✔
288
        &mut self,
242✔
289
        burnchain_config: &Burnchain,
242✔
290
        coordinator_receivers: CoordinatorReceivers,
242✔
291
        miner_status: Arc<Mutex<MinerStatus>>,
242✔
292
    ) -> JoinHandle<()> {
242✔
293
        let use_test_genesis_data = use_test_genesis_chainstate(&self.config);
242✔
294

295
        // load up genesis Atlas attachments
296
        let mut atlas_config = AtlasConfig::new(self.config.is_mainnet());
242✔
297
        let genesis_attachments = GenesisData::new(use_test_genesis_data)
242✔
298
            .read_name_zonefiles()
242✔
299
            .map(|z| Attachment::new(z.zonefile_content.as_bytes().to_vec()))
2,904✔
300
            .collect();
242✔
301
        atlas_config.genesis_attachments = Some(genesis_attachments);
242✔
302

303
        let chain_state_db = self.boot_chainstate(burnchain_config);
242✔
304

305
        // NOTE: re-instantiate AtlasConfig so we don't have to keep the genesis attachments around
306
        let moved_atlas_config = self.config.atlas.clone();
242✔
307
        let moved_config = self.config.clone();
242✔
308
        let moved_burnchain_config = burnchain_config.clone();
242✔
309
        let coordinator_dispatcher = self.event_dispatcher.clone();
242✔
310
        let atlas_db = AtlasDB::connect(
242✔
311
            moved_atlas_config.clone(),
242✔
312
            &self.config.get_atlas_db_file_path(),
242✔
313
            true,
314
        )
315
        .expect("Failed to connect Atlas DB during startup");
242✔
316
        let coordinator_indexer =
242✔
317
            make_bitcoin_indexer(&self.config, Some(self.should_keep_running.clone()));
242✔
318

319
        let rpc_port = moved_config
242✔
320
            .node
242✔
321
            .rpc_bind_addr()
242✔
322
            .unwrap_or_else(|| panic!("Failed to parse socket: {}", &moved_config.node.rpc_bind))
242✔
323
            .port();
242✔
324
        let coordinator_thread_handle = thread::Builder::new()
242✔
325
            .name(format!("chains-coordinator:{rpc_port}"))
242✔
326
            .stack_size(BLOCK_PROCESSOR_STACK_SIZE)
242✔
327
            .spawn(move || {
242✔
328
                debug!(
242✔
329
                    "chains-coordinator thread ID is {:?}",
330
                    thread::current().id()
×
331
                );
332
                let mut cost_estimator = moved_config.make_cost_estimator();
242✔
333
                let mut fee_estimator = moved_config.make_fee_estimator();
242✔
334

335
                let coord_config = ChainsCoordinatorConfig {
242✔
336
                    txindex: moved_config.node.txindex,
242✔
337
                };
242✔
338
                ChainsCoordinator::run(
242✔
339
                    coord_config,
242✔
340
                    chain_state_db,
242✔
341
                    moved_burnchain_config,
242✔
342
                    &coordinator_dispatcher,
242✔
343
                    coordinator_receivers,
242✔
344
                    moved_atlas_config,
242✔
345
                    cost_estimator.as_deref_mut(),
242✔
346
                    fee_estimator.as_deref_mut(),
242✔
347
                    miner_status,
242✔
348
                    coordinator_indexer,
242✔
349
                    atlas_db,
242✔
350
                );
351
            })
242✔
352
            .expect("FATAL: failed to start chains coordinator thread");
242✔
353

354
        coordinator_thread_handle
242✔
355
    }
242✔
356

357
    /// Start Prometheus logging
358
    fn start_prometheus(&mut self) {
242✔
359
        if self.monitoring_thread.is_some() {
242✔
360
            info!("Monitoring thread already running, nakamoto run-loop will not restart it");
7✔
361
            return;
7✔
362
        }
235✔
363
        let Some(prometheus_bind) = self.config.node.prometheus_bind.clone() else {
235✔
364
            return;
234✔
365
        };
366
        let monitoring_thread = thread::Builder::new()
1✔
367
            .name("prometheus".to_string())
1✔
368
            .spawn(move || {
1✔
369
                debug!("prometheus thread ID is {:?}", thread::current().id());
1✔
370
                start_serving_monitoring_metrics(prometheus_bind)
1✔
371
            })
1✔
372
            .expect("FATAL: failed to start monitoring thread");
1✔
373

374
        self.monitoring_thread.replace(monitoring_thread);
1✔
375
    }
242✔
376

377
    /// Get the sortition DB's highest block height, aligned to a reward cycle boundary, and the
378
    /// highest sortition.
379
    /// Returns (height at rc start, sortition)
380
    fn get_reward_cycle_sortition_db_height(
242✔
381
        sortdb: &SortitionDB,
242✔
382
        burnchain_config: &Burnchain,
242✔
383
    ) -> (u64, BlockSnapshot) {
242✔
384
        let (stacks_ch, _) = SortitionDB::get_canonical_stacks_chain_tip_hash(sortdb.conn())
242✔
385
            .expect("BUG: failed to load canonical stacks chain tip hash");
242✔
386

387
        let sn = match SortitionDB::get_block_snapshot_consensus(sortdb.conn(), &stacks_ch)
242✔
388
            .expect("BUG: failed to query sortition DB")
242✔
389
        {
390
            Some(sn) => sn,
242✔
391
            None => {
392
                debug!("No canonical stacks chain tip hash present");
×
393
                let sn = SortitionDB::get_first_block_snapshot(sortdb.conn())
×
394
                    .expect("BUG: failed to get first-ever block snapshot");
×
395
                sn
×
396
            }
397
        };
398

399
        (
242✔
400
            burnchain_config.reward_cycle_to_block_height(
242✔
401
                burnchain_config
242✔
402
                    .block_height_to_reward_cycle(sn.block_height)
242✔
403
                    .expect("BUG: snapshot preceeds first reward cycle"),
242✔
404
            ),
242✔
405
            sn,
242✔
406
        )
242✔
407
    }
242✔
408

409
    /// Starts the node runloop.
410
    ///
411
    /// This function will block by looping infinitely.
412
    /// It will start the burnchain (separate thread), set-up a channel in
413
    /// charge of coordinating the new blocks coming from the burnchain and
414
    /// the nodes, taking turns on tenures.
415
    pub fn start(
242✔
416
        &mut self,
242✔
417
        burnchain_opt: Option<Burnchain>,
242✔
418
        mut mine_start: u64,
242✔
419
        data_from_neon: Option<Neon2NakaData>,
242✔
420
    ) {
242✔
421
        let (coordinator_receivers, coordinator_senders) = self
242✔
422
            .coordinator_channels
242✔
423
            .take()
242✔
424
            .expect("Run loop already started, can only start once after initialization.");
242✔
425

426
        // setup the termination handler, allow it to error if a prior runloop already set it
427
        neon::RunLoop::setup_termination_handler(self.should_keep_running.clone(), true);
242✔
428

429
        let burnchain_result = neon::RunLoop::instantiate_burnchain_state(
242✔
430
            &self.config,
242✔
431
            self.should_keep_running.clone(),
242✔
432
            burnchain_opt,
242✔
433
            coordinator_senders.clone(),
242✔
434
        );
435

436
        let mut burnchain = match burnchain_result {
242✔
437
            Ok(burnchain_controller) => burnchain_controller,
242✔
438
            Err(burnchain_error::ShutdownInitiated) => {
439
                info!("Exiting stacks-node");
×
440
                return;
×
441
            }
442
            Err(e) => {
×
443
                error!("Error initializing burnchain: {e}");
×
444
                info!("Exiting stacks-node");
×
445
                return;
×
446
            }
447
        };
448

449
        let burnchain_config = burnchain.get_burnchain();
242✔
450
        self.burnchain = Some(burnchain_config.clone());
242✔
451

452
        // can we mine?
453
        let is_miner = self.check_is_miner(&mut burnchain);
242✔
454
        self.is_miner = Some(is_miner);
242✔
455

456
        // relayer linkup
457
        let (relay_send, relay_recv) = sync_channel(RELAYER_MAX_BUFFER);
242✔
458

459
        // set up globals so other subsystems can instantiate off of the runloop state.
460
        let globals = Globals::new(
242✔
461
            coordinator_senders,
242✔
462
            self.get_miner_status(),
242✔
463
            relay_send,
242✔
464
            self.counters.clone(),
242✔
465
            self.pox_watchdog_comms.clone(),
242✔
466
            self.should_keep_running.clone(),
242✔
467
            mine_start,
242✔
468
            LeaderKeyRegistrationState::default(),
242✔
469
        );
470
        self.set_globals(globals.clone());
242✔
471

472
        // have headers; boot up the chains coordinator and instantiate the chain state
473
        let coordinator_thread_handle = self.spawn_chains_coordinator(
242✔
474
            &burnchain_config,
242✔
475
            coordinator_receivers,
242✔
476
            globals.get_miner_status(),
242✔
477
        );
478
        self.start_prometheus();
242✔
479

480
        // We announce a new burn block so that the chains coordinator
481
        // can resume prior work and handle eventual unprocessed sortitions
482
        // stored during a previous session.
483
        globals.coord().announce_new_burn_block();
242✔
484

485
        // Make sure at least one sortition has happened, and make sure it's globally available
486
        let sortdb = burnchain.sortdb_mut();
242✔
487
        let (rc_aligned_height, sn) =
242✔
488
            RunLoop::get_reward_cycle_sortition_db_height(sortdb, &burnchain_config);
242✔
489

490
        let burnchain_tip_snapshot = if sn.block_height == burnchain_config.first_block_height {
242✔
491
            // need at least one sortition to happen.
492
            burnchain
×
493
                .wait_for_sortitions(globals.coord().clone(), sn.block_height + 1)
×
494
                .expect("Unable to get burnchain tip")
×
495
                .block_snapshot
×
496
        } else {
497
            sn
242✔
498
        };
499

500
        globals.set_last_sortition(burnchain_tip_snapshot);
242✔
501

502
        // Boot up the p2p network and relayer, and figure out how many sortitions we have so far
503
        // (it could be non-zero if the node is resuming from chainstate)
504
        let mut node = StacksNode::spawn(self, globals.clone(), relay_recv, data_from_neon);
242✔
505

506
        // Wait for all pending sortitions to process
507
        let burnchain_db = burnchain_config
242✔
508
            .open_burnchain_db(false)
242✔
509
            .expect("FATAL: failed to open burnchain DB");
242✔
510
        let burnchain_db_tip = burnchain_db
242✔
511
            .get_canonical_chain_tip()
242✔
512
            .expect("FATAL: failed to query burnchain DB");
242✔
513
        let mut burnchain_tip = burnchain
242✔
514
            .wait_for_sortitions(globals.coord().clone(), burnchain_db_tip.block_height)
242✔
515
            .expect("Unable to get burnchain tip");
242✔
516

517
        // Start the runloop
518
        debug!("Runloop: Begin run loop");
242✔
519
        self.counters.bump_blocks_processed();
242✔
520

521
        let mut sortition_db_height = rc_aligned_height;
242✔
522
        let mut burnchain_height = sortition_db_height;
242✔
523
        let mut num_sortitions_in_last_cycle;
524

525
        // prepare to fetch the first reward cycle!
526
        let mut target_burnchain_block_height = cmp::min(
242✔
527
            burnchain_config.reward_cycle_to_block_height(
242✔
528
                burnchain_config
242✔
529
                    .block_height_to_reward_cycle(burnchain_height)
242✔
530
                    .expect("BUG: block height is not in a reward cycle")
242✔
531
                    + 1,
242✔
532
            ),
533
            burnchain.get_headers_height() - 1,
242✔
534
        );
535

536
        debug!("Runloop: Begin main runloop starting a burnchain block {sortition_db_height}");
242✔
537

538
        let mut last_tenure_sortition_height = 0;
242✔
539
        let mut poll_deadline = 0;
242✔
540

541
        loop {
542
            if !globals.keep_running() {
20,778✔
543
                // The p2p thread relies on the same atomic_bool, it will
544
                // discontinue its execution after completing its ongoing runloop epoch.
545
                info!("Terminating p2p process");
220✔
546
                info!("Terminating relayer");
220✔
547
                info!("Terminating chains-coordinator");
220✔
548

549
                globals.coord().stop_chains_coordinator();
220✔
550
                coordinator_thread_handle.join().unwrap();
220✔
551
                node.join();
220✔
552

553
                info!("Exiting stacks-node");
220✔
554
                break;
220✔
555
            }
20,558✔
556

557
            let remote_chain_height = burnchain.get_headers_height() - 1;
20,558✔
558

559
            // wait for the p2p state-machine to do at least one pass
560
            debug!("Runloop: Wait until Stacks block downloads reach a quiescent state before processing more burnchain blocks"; "remote_chain_height" => remote_chain_height, "local_chain_height" => burnchain_height);
20,558✔
561

562
            // TODO: for now, we just set initial block download false.
563
            //   I think that the sync watchdog probably needs to change a fair bit
564
            //   for nakamoto. There may be some opportunity to refactor this runloop
565
            //   as well (e.g., the `mine_start` should be integrated with the
566
            //   watchdog so that there's just one source of truth about ibd),
567
            //   but I think all of this can be saved for post-neon work.
568
            let ibd = false;
20,558✔
569
            self.pox_watchdog_comms.set_ibd(ibd);
20,558✔
570

571
            // calculate burnchain sync percentage
572
            let percent: f64 = if remote_chain_height > 0 {
20,558✔
573
                burnchain_tip.block_snapshot.block_height as f64 / remote_chain_height as f64
20,558✔
574
            } else {
575
                0.0
×
576
            };
577

578
            // Download each burnchain block and process their sortitions.  This, in turn, will
579
            // cause the node's p2p and relayer threads to go fetch and download Stacks blocks and
580
            // process them.  This loop runs for one reward cycle, so that the next pass of the
581
            // runloop will cause the PoX sync watchdog to wait until it believes that the node has
582
            // obtained all the Stacks blocks it can.
583
            debug!(
20,558✔
584
                "Runloop: Download burnchain blocks up to reward cycle #{} (height {target_burnchain_block_height})",
585
                burnchain_config
×
586
                    .block_height_to_reward_cycle(target_burnchain_block_height)
×
587
                    .expect("FATAL: target burnchain block height does not have a reward cycle");
×
588
                "total_burn_sync_percent" => %percent,
589
                "local_burn_height" => burnchain_tip.block_snapshot.block_height,
×
590
                "remote_tip_height" => remote_chain_height
×
591
            );
592

593
            loop {
594
                if !globals.keep_running() {
40,215✔
595
                    break;
218✔
596
                }
39,997✔
597

598
                if poll_deadline > get_epoch_time_secs() {
39,997✔
599
                    sleep_ms(1_000);
19,674✔
600
                    continue;
19,674✔
601
                }
20,323✔
602
                poll_deadline = get_epoch_time_secs() + self.config().burnchain.poll_time_secs;
20,323✔
603

604
                let (next_burnchain_tip, tip_burnchain_height) =
20,319✔
605
                    match burnchain.sync(Some(target_burnchain_block_height)) {
20,323✔
606
                        Ok(x) => x,
20,319✔
607
                        Err(e) => {
4✔
608
                            warn!("Runloop: Burnchain controller stopped: {e}");
4✔
609
                            continue;
4✔
610
                        }
611
                    };
612

613
                // *now* we know the burnchain height
614
                burnchain_tip = next_burnchain_tip;
20,319✔
615
                burnchain_height = tip_burnchain_height;
20,319✔
616

617
                let sortition_tip = &burnchain_tip.block_snapshot.sortition_id;
20,319✔
618
                let next_sortition_height = burnchain_tip.block_snapshot.block_height;
20,319✔
619

620
                if next_sortition_height != last_tenure_sortition_height {
20,319✔
621
                    info!(
1,757✔
622
                        "Runloop: Downloaded burnchain blocks up to height {burnchain_height}; target height is {target_burnchain_block_height}; remote_chain_height = {remote_chain_height} next_sortition_height = {next_sortition_height}, sortition_db_height = {sortition_db_height}"
623
                    );
624
                }
18,562✔
625

626
                if next_sortition_height > sortition_db_height {
20,319✔
627
                    debug!(
1,757✔
628
                        "Runloop: New burnchain block height {next_sortition_height} > {sortition_db_height}"
629
                    );
630

631
                    let mut sort_count = 0;
1,757✔
632

633
                    debug!("Runloop: block mining until we process all sortitions");
1,757✔
634
                    signal_mining_blocked(globals.get_miner_status());
1,757✔
635

636
                    // first, let's process all blocks in (sortition_db_height, next_sortition_height]
637
                    for block_to_process in (sortition_db_height + 1)..(next_sortition_height + 1) {
4,469✔
638
                        // stop mining so we can advance the sortition DB and so our
639
                        // ProcessTenure() directive (sent by relayer_sortition_notify() below)
640
                        // will be unblocked.
641

642
                        let block = {
4,469✔
643
                            let ic = burnchain.sortdb_ref().index_conn();
4,469✔
644
                            SortitionDB::get_ancestor_snapshot(&ic, block_to_process, sortition_tip)
4,469✔
645
                                .unwrap()
4,469✔
646
                                .expect(
4,469✔
647
                                    "Failed to find block in fork processed by burnchain indexer",
4,469✔
648
                                )
649
                        };
650
                        if block.sortition {
4,469✔
651
                            sort_count += 1;
4,292✔
652
                        }
4,292✔
653

654
                        let sortition_id = &block.sortition_id;
4,469✔
655

656
                        // Have the node process the new block, that can include, or not, a sortition.
657
                        if let Err(e) = node.process_burnchain_state(
4,469✔
658
                            self.config(),
4,469✔
659
                            burnchain.sortdb_mut(),
4,469✔
660
                            sortition_id,
4,469✔
661
                            ibd,
4,469✔
662
                        ) {
4,469✔
663
                            // relayer errored, exit.
664
                            error!("Runloop: Block relayer and miner errored, exiting."; "err" => ?e);
22✔
665
                            return;
22✔
666
                        }
4,447✔
667
                    }
668

669
                    debug!("Runloop: enable miner after processing sortitions");
1,735✔
670
                    signal_mining_ready(globals.get_miner_status());
1,735✔
671

672
                    num_sortitions_in_last_cycle = sort_count;
1,735✔
673
                    debug!(
1,735✔
674
                        "Runloop: Synchronized sortitions up to block height {next_sortition_height} from {sortition_db_height} (chain tip height is {burnchain_height}); {num_sortitions_in_last_cycle} sortitions"
675
                    );
676

677
                    sortition_db_height = next_sortition_height;
1,735✔
678
                } else if ibd {
18,562✔
679
                    // drive block processing after we reach the burnchain tip.
×
680
                    // we may have downloaded all the blocks already,
×
681
                    // so we can't rely on the relayer alone to
×
682
                    // drive it.
×
683
                    globals.coord().announce_new_stacks_block();
×
684
                }
18,562✔
685

686
                if burnchain_height >= target_burnchain_block_height
20,297✔
687
                    || burnchain_height >= remote_chain_height
2✔
688
                {
689
                    break;
20,318✔
690
                }
2✔
691
            }
692

693
            // advance one reward cycle at a time.
694
            // If we're still downloading, then this is simply target_burnchain_block_height + reward_cycle_len.
695
            // Otherwise, this is burnchain_tip + reward_cycle_len
696
            let next_target_burnchain_block_height = cmp::min(
20,536✔
697
                burnchain_config.reward_cycle_to_block_height(
20,536✔
698
                    burnchain_config
20,536✔
699
                        .block_height_to_reward_cycle(target_burnchain_block_height)
20,536✔
700
                        .expect("FATAL: burnchain height before system start")
20,536✔
701
                        + 1,
20,536✔
702
                ),
703
                remote_chain_height,
20,536✔
704
            );
705

706
            debug!("Runloop: Advance target burnchain block height from {target_burnchain_block_height} to {next_target_burnchain_block_height} (sortition height {sortition_db_height})");
20,536✔
707
            target_burnchain_block_height = next_target_burnchain_block_height;
20,536✔
708

709
            if sortition_db_height >= burnchain_height && !ibd {
20,536✔
710
                let canonical_stacks_tip_height =
18,984✔
711
                    SortitionDB::get_canonical_burn_chain_tip(burnchain.sortdb_ref().conn())
18,984✔
712
                        .map(|snapshot| snapshot.canonical_stacks_tip_height)
18,984✔
713
                        .unwrap_or(0);
18,984✔
714
                if canonical_stacks_tip_height < mine_start {
18,984✔
715
                    info!(
×
716
                        "Runloop: Synchronized full burnchain, but stacks tip height is {canonical_stacks_tip_height}, and we are trying to boot to {mine_start}, not mining until reaching chain tip"
717
                    );
718
                } else {
719
                    // once we've synced to the chain tip once, don't apply this check again.
720
                    //  this prevents a possible corner case in the event of a PoX fork.
721
                    mine_start = 0;
18,984✔
722

723
                    // at tip, and not downloading. proceed to mine.
724
                    if last_tenure_sortition_height != sortition_db_height {
18,984✔
725
                        if is_miner {
1,753✔
726
                            info!(
1,741✔
727
                                "Runloop: Synchronized full burnchain up to height {sortition_db_height}. Proceeding to mine blocks"
728
                            );
729
                        } else {
730
                            info!(
12✔
731
                                "Runloop: Synchronized full burnchain up to height {sortition_db_height}."
732
                            );
733
                        }
734
                        last_tenure_sortition_height = sortition_db_height;
1,753✔
735
                        globals.raise_initiative("runloop-synced".to_string());
1,753✔
736
                    }
17,231✔
737
                }
738
            }
1,552✔
739
        }
740
    }
242✔
741
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc