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

stacks-network / stacks-core / 25801484257-1

13 May 2026 01:15PM UTC coverage: 85.648% (-0.06%) from 85.712%
25801484257-1

Pull #7183

github

2d7e6d
web-flow
Merge 420cb597a into 31276d071
Pull Request #7183: Fix problematic transaction handling

110 of 130 new or added lines in 7 files covered. (84.62%)

5464 existing lines in 98 files now uncovered.

188263 of 219809 relevant lines covered (85.65%)

18940648.33 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(
245✔
85
        config: Config,
245✔
86
        should_keep_running: Option<Arc<AtomicBool>>,
245✔
87
        counters: Option<Counters>,
245✔
88
        monitoring_thread: Option<JoinHandle<Result<(), MonitoringError>>>,
245✔
89
        event_dispatcher: Option<EventDispatcher>,
245✔
90
    ) -> Self {
245✔
91
        let channels = CoordinatorCommunication::instantiate();
245✔
92
        let should_keep_running =
245✔
93
            should_keep_running.unwrap_or_else(|| Arc::new(AtomicBool::new(true)));
245✔
94
        let pox_watchdog_comms = PoxSyncWatchdogComms::new(should_keep_running.clone());
245✔
95
        let miner_status = Arc::new(Mutex::new(MinerStatus::make_ready(
245✔
96
            config.burnchain.burn_fee_cap,
245✔
97
        )));
98

99
        let event_dispatcher = event_dispatcher.unwrap_or_else(|| {
245✔
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✔
UNCOV
105
                event_dispatcher.register_observer(observer);
×
UNCOV
106
            }
×
107
            event_dispatcher
2✔
108
        });
2✔
109

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

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

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

136
    pub(crate) fn get_coordinator_channel(&self) -> Option<CoordinatorChannels> {
245✔
137
        self.coordinator_channels.as_ref().map(|x| x.1.clone())
245✔
138
    }
245✔
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,573✔
145
        &self.config
25,573✔
146
    }
25,573✔
147

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

152
    pub(crate) fn is_miner(&self) -> bool {
490✔
153
        self.is_miner.unwrap_or(false)
490✔
154
    }
490✔
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 {
980✔
161
        self.burnchain
980✔
162
            .clone()
980✔
163
            .expect("FATAL: tried to get runloop burnchain before calling .start()")
980✔
164
    }
980✔
165

166
    pub(crate) fn get_miner_status(&self) -> Arc<Mutex<MinerStatus>> {
245✔
167
        self.miner_status.clone()
245✔
168
    }
245✔
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 {
245✔
178
        if self.config.node.miner {
245✔
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 {
241✔
182
                return true;
5✔
183
            }
236✔
184
            let keychain = Keychain::default(self.config.node.seed.clone());
236✔
185
            let mut op_signer = keychain.generate_op_signer();
236✔
186
            if let Err(e) = burnchain.create_wallet_if_dne() {
236✔
UNCOV
187
                warn!("Error when creating wallet: {e:?}");
×
188
            }
236✔
189
            let mut btc_addrs = vec![(
236✔
190
                StacksEpochId::Epoch2_05,
236✔
191
                // legacy
236✔
192
                BitcoinAddress::from_bytes_legacy(
236✔
193
                    self.config.burnchain.get_bitcoin_network().1,
236✔
194
                    LegacyBitcoinAddressType::PublicKeyHash,
236✔
195
                    &Hash160::from_data(&op_signer.get_public_key().to_bytes()).0,
236✔
196
                )
236✔
197
                .expect("FATAL: failed to construct legacy bitcoin address"),
236✔
198
            )];
236✔
199
            if self.config.miner.segwit {
236✔
UNCOV
200
                btc_addrs.push((
×
UNCOV
201
                    StacksEpochId::Epoch21,
×
UNCOV
202
                    // segwit p2wpkh
×
UNCOV
203
                    BitcoinAddress::from_bytes_segwit_p2wpkh(
×
UNCOV
204
                        self.config.burnchain.get_bitcoin_network().1,
×
UNCOV
205
                        &Hash160::from_data(&op_signer.get_public_key().to_bytes_compressed()).0,
×
UNCOV
206
                    )
×
UNCOV
207
                    .expect("FATAL: failed to construct segwit p2wpkh address"),
×
208
                ));
×
209
            }
236✔
210

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

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

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

267
        let (chain_state_db, receipts) = StacksChainState::open_and_exec(
245✔
268
            self.config.is_mainnet(),
245✔
269
            self.config.burnchain.chain_id,
245✔
270
            &self.config.get_chainstate_path_str(),
245✔
271
            Some(&mut boot_data),
245✔
272
            Some(self.config.node.get_marf_opts()),
245✔
273
        )
245✔
274
        .unwrap();
245✔
275
        run_loop::announce_boot_receipts(
245✔
276
            &mut self.event_dispatcher,
245✔
277
            &chain_state_db,
245✔
278
            &burnchain_config.pox_constants,
245✔
279
            &receipts,
245✔
280
        );
281
        chain_state_db
245✔
282
    }
245✔
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(
245✔
288
        &mut self,
245✔
289
        burnchain_config: &Burnchain,
245✔
290
        coordinator_receivers: CoordinatorReceivers,
245✔
291
        miner_status: Arc<Mutex<MinerStatus>>,
245✔
292
    ) -> JoinHandle<()> {
245✔
293
        let use_test_genesis_data = use_test_genesis_chainstate(&self.config);
245✔
294

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

303
        let chain_state_db = self.boot_chainstate(burnchain_config);
245✔
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();
245✔
307
        let moved_config = self.config.clone();
245✔
308
        let moved_burnchain_config = burnchain_config.clone();
245✔
309
        let coordinator_dispatcher = self.event_dispatcher.clone();
245✔
310
        let atlas_db = AtlasDB::connect(
245✔
311
            moved_atlas_config.clone(),
245✔
312
            &self.config.get_atlas_db_file_path(),
245✔
313
            true,
314
        )
315
        .expect("Failed to connect Atlas DB during startup");
245✔
316
        let coordinator_indexer =
245✔
317
            make_bitcoin_indexer(&self.config, Some(self.should_keep_running.clone()));
245✔
318

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

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

354
        coordinator_thread_handle
245✔
355
    }
245✔
356

357
    /// Start Prometheus logging
358
    fn start_prometheus(&mut self) {
245✔
359
        if self.monitoring_thread.is_some() {
245✔
360
            info!("Monitoring thread already running, nakamoto run-loop will not restart it");
7✔
361
            return;
7✔
362
        }
238✔
363
        let Some(prometheus_bind) = self.config.node.prometheus_bind.clone() else {
238✔
364
            return;
237✔
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
    }
245✔
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(
245✔
381
        sortdb: &SortitionDB,
245✔
382
        burnchain_config: &Burnchain,
245✔
383
    ) -> (u64, BlockSnapshot) {
245✔
384
        let (stacks_ch, _) = SortitionDB::get_canonical_stacks_chain_tip_hash(sortdb.conn())
245✔
385
            .expect("BUG: failed to load canonical stacks chain tip hash");
245✔
386

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

399
        (
245✔
400
            burnchain_config.reward_cycle_to_block_height(
245✔
401
                burnchain_config
245✔
402
                    .block_height_to_reward_cycle(sn.block_height)
245✔
403
                    .expect("BUG: snapshot preceeds first reward cycle"),
245✔
404
            ),
245✔
405
            sn,
245✔
406
        )
245✔
407
    }
245✔
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(
245✔
416
        &mut self,
245✔
417
        burnchain_opt: Option<Burnchain>,
245✔
418
        mut mine_start: u64,
245✔
419
        data_from_neon: Option<Neon2NakaData>,
245✔
420
    ) {
245✔
421
        let (coordinator_receivers, coordinator_senders) = self
245✔
422
            .coordinator_channels
245✔
423
            .take()
245✔
424
            .expect("Run loop already started, can only start once after initialization.");
245✔
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);
245✔
428

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

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

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

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

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

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

472
        // have headers; boot up the chains coordinator and instantiate the chain state
473
        let coordinator_thread_handle = self.spawn_chains_coordinator(
245✔
474
            &burnchain_config,
245✔
475
            coordinator_receivers,
245✔
476
            globals.get_miner_status(),
245✔
477
        );
478
        self.start_prometheus();
245✔
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();
245✔
484

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

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

500
        globals.set_last_sortition(burnchain_tip_snapshot);
245✔
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);
245✔
505

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

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

521
        let mut sortition_db_height = rc_aligned_height;
245✔
522
        let mut burnchain_height = sortition_db_height;
245✔
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(
245✔
527
            burnchain_config.reward_cycle_to_block_height(
245✔
528
                burnchain_config
245✔
529
                    .block_height_to_reward_cycle(burnchain_height)
245✔
530
                    .expect("BUG: block height is not in a reward cycle")
245✔
531
                    + 1,
245✔
532
            ),
533
            burnchain.get_headers_height() - 1,
245✔
534
        );
535

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

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

541
        loop {
542
            if !globals.keep_running() {
20,785✔
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");
225✔
546
                info!("Terminating relayer");
225✔
547
                info!("Terminating chains-coordinator");
225✔
548

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

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

557
            let remote_chain_height = burnchain.get_headers_height() - 1;
20,560✔
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,560✔
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,560✔
569
            self.pox_watchdog_comms.set_ibd(ibd);
20,560✔
570

571
            // calculate burnchain sync percentage
572
            let percent: f64 = if remote_chain_height > 0 {
20,560✔
573
                burnchain_tip.block_snapshot.block_height as f64 / remote_chain_height as f64
20,560✔
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,560✔
584
                "Runloop: Download burnchain blocks up to reward cycle #{} (height {target_burnchain_block_height})",
UNCOV
585
                burnchain_config
×
UNCOV
586
                    .block_height_to_reward_cycle(target_burnchain_block_height)
×
UNCOV
587
                    .expect("FATAL: target burnchain block height does not have a reward cycle");
×
588
                "total_burn_sync_percent" => %percent,
UNCOV
589
                "local_burn_height" => burnchain_tip.block_snapshot.block_height,
×
UNCOV
590
                "remote_tip_height" => remote_chain_height
×
591
            );
592

593
            loop {
594
                if !globals.keep_running() {
40,261✔
595
                    break;
223✔
596
                }
40,038✔
597

598
                if poll_deadline > get_epoch_time_secs() {
40,038✔
599
                    sleep_ms(1_000);
19,718✔
600
                    continue;
19,718✔
601
                }
20,320✔
602
                poll_deadline = get_epoch_time_secs() + self.config().burnchain.poll_time_secs;
20,320✔
603

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

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

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

620
                if next_sortition_height != last_tenure_sortition_height {
20,318✔
621
                    info!(
1,785✔
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,533✔
625

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

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

633
                    debug!("Runloop: block mining until we process all sortitions");
1,785✔
634
                    signal_mining_blocked(globals.get_miner_status());
1,785✔
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,538✔
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,538✔
643
                            let ic = burnchain.sortdb_ref().index_conn();
4,538✔
644
                            SortitionDB::get_ancestor_snapshot(&ic, block_to_process, sortition_tip)
4,538✔
645
                                .unwrap()
4,538✔
646
                                .expect(
4,538✔
647
                                    "Failed to find block in fork processed by burnchain indexer",
4,538✔
648
                                )
649
                        };
650
                        if block.sortition {
4,538✔
651
                            sort_count += 1;
4,366✔
652
                        }
4,366✔
653

654
                        let sortition_id = &block.sortition_id;
4,538✔
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,538✔
658
                            self.config(),
4,538✔
659
                            burnchain.sortdb_mut(),
4,538✔
660
                            sortition_id,
4,538✔
661
                            ibd,
4,538✔
662
                        ) {
4,538✔
663
                            // relayer errored, exit.
664
                            error!("Runloop: Block relayer and miner errored, exiting."; "err" => ?e);
20✔
665
                            return;
20✔
666
                        }
4,518✔
667
                    }
668

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

672
                    num_sortitions_in_last_cycle = sort_count;
1,765✔
673
                    debug!(
1,765✔
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,765✔
678
                } else if ibd {
18,533✔
UNCOV
679
                    // drive block processing after we reach the burnchain tip.
×
UNCOV
680
                    // we may have downloaded all the blocks already,
×
UNCOV
681
                    // so we can't rely on the relayer alone to
×
UNCOV
682
                    // drive it.
×
UNCOV
683
                    globals.coord().announce_new_stacks_block();
×
684
                }
18,533✔
685

686
                if burnchain_height >= target_burnchain_block_height
20,298✔
687
                    || burnchain_height >= remote_chain_height
18✔
688
                {
689
                    break;
20,317✔
690
                }
18✔
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,540✔
697
                burnchain_config.reward_cycle_to_block_height(
20,540✔
698
                    burnchain_config
20,540✔
699
                        .block_height_to_reward_cycle(target_burnchain_block_height)
20,540✔
700
                        .expect("FATAL: burnchain height before system start")
20,540✔
701
                        + 1,
20,540✔
702
                ),
703
                remote_chain_height,
20,540✔
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,540✔
707
            target_burnchain_block_height = next_target_burnchain_block_height;
20,540✔
708

709
            if sortition_db_height >= burnchain_height && !ibd {
20,540✔
710
                let canonical_stacks_tip_height =
18,961✔
711
                    SortitionDB::get_canonical_burn_chain_tip(burnchain.sortdb_ref().conn())
18,961✔
712
                        .map(|snapshot| snapshot.canonical_stacks_tip_height)
18,961✔
713
                        .unwrap_or(0);
18,961✔
714
                if canonical_stacks_tip_height < mine_start {
18,961✔
UNCOV
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,961✔
722

723
                    // at tip, and not downloading. proceed to mine.
724
                    if last_tenure_sortition_height != sortition_db_height {
18,961✔
725
                        if is_miner {
1,780✔
726
                            info!(
1,768✔
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,780✔
735
                        globals.raise_initiative("runloop-synced".to_string());
1,780✔
736
                    }
17,181✔
737
                }
738
            }
1,579✔
739
        }
740
    }
245✔
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