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

stacks-network / stacks-core / 25404138305-1

05 May 2026 09:47PM UTC coverage: 85.69% (-0.02%) from 85.712%
25404138305-1

Pull #7169

github

497ffd
web-flow
Merge 35db1183d into 53ffba0ab
Pull Request #7169: Feat: add defensive memory allocation for miners/signers

134 of 139 new or added lines in 11 files covered. (96.4%)

4591 existing lines in 96 files now uncovered.

187733 of 219085 relevant lines covered (85.69%)

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

99
        let event_dispatcher = event_dispatcher.unwrap_or_else(|| {
241✔
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 {
241✔
111
            config,
241✔
112
            globals: None,
241✔
113
            coordinator_channels: Some(channels),
241✔
114
            counters: counters.unwrap_or_default(),
241✔
115
            should_keep_running,
241✔
116
            event_dispatcher,
241✔
117
            pox_watchdog: None,
241✔
118
            is_miner: None,
241✔
119
            burnchain: None,
241✔
120
            pox_watchdog_comms,
241✔
121
            miner_status,
241✔
122
            monitoring_thread,
241✔
123
        }
241✔
124
    }
241✔
125

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

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

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

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

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

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

211
            // retry UTXO check a few times, in case bitcoind is still starting up
212
            for _ in 0..Self::UTXO_RETRY_COUNT {
232✔
213
                for (epoch_id, btc_addr) in &btc_addrs {
232✔
214
                    info!("Miner node: checking UTXOs at address: {btc_addr}");
232✔
215
                    let utxos =
232✔
216
                        burnchain.get_utxos(*epoch_id, &op_signer.get_public_key(), 1, None, 0);
232✔
217
                    if utxos.is_none() {
232✔
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");
232✔
221
                        return true;
232✔
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
    }
241✔
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 {
241✔
237
        let use_test_genesis_data = use_test_genesis_chainstate(&self.config);
241✔
238

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

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

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

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

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

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

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

354
        coordinator_thread_handle
241✔
355
    }
241✔
356

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

387
        let sn = match SortitionDB::get_block_snapshot_consensus(sortdb.conn(), &stacks_ch)
241✔
388
            .expect("BUG: failed to query sortition DB")
241✔
389
        {
390
            Some(sn) => sn,
241✔
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
        (
241✔
400
            burnchain_config.reward_cycle_to_block_height(
241✔
401
                burnchain_config
241✔
402
                    .block_height_to_reward_cycle(sn.block_height)
241✔
403
                    .expect("BUG: snapshot preceeds first reward cycle"),
241✔
404
            ),
241✔
405
            sn,
241✔
406
        )
241✔
407
    }
241✔
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(
241✔
416
        &mut self,
241✔
417
        burnchain_opt: Option<Burnchain>,
241✔
418
        mut mine_start: u64,
241✔
419
        data_from_neon: Option<Neon2NakaData>,
241✔
420
    ) {
241✔
421
        let (coordinator_receivers, coordinator_senders) = self
241✔
422
            .coordinator_channels
241✔
423
            .take()
241✔
424
            .expect("Run loop already started, can only start once after initialization.");
241✔
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);
241✔
428

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

436
        let mut burnchain = match burnchain_result {
241✔
437
            Ok(burnchain_controller) => burnchain_controller,
241✔
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();
241✔
450
        self.burnchain = Some(burnchain_config.clone());
241✔
451

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

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

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

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

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

490
        let burnchain_tip_snapshot = if sn.block_height == burnchain_config.first_block_height {
241✔
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
241✔
498
        };
499

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

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

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

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

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

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

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

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

553
                info!("Exiting stacks-node");
221✔
554
                break;
221✔
555
            }
19,787✔
556

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

571
            // calculate burnchain sync percentage
572
            let percent: f64 = if remote_chain_height > 0 {
19,787✔
573
                burnchain_tip.block_snapshot.block_height as f64 / remote_chain_height as f64
19,787✔
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!(
19,787✔
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() {
38,743✔
595
                    break;
220✔
596
                }
38,523✔
597

598
                if poll_deadline > get_epoch_time_secs() {
38,523✔
599
                    sleep_ms(1_000);
18,973✔
600
                    continue;
18,973✔
601
                }
19,550✔
602
                poll_deadline = get_epoch_time_secs() + self.config().burnchain.poll_time_secs;
19,550✔
603

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

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

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

620
                if next_sortition_height != last_tenure_sortition_height {
19,547✔
621
                    info!(
1,745✔
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
                }
17,802✔
625

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

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

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

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

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

672
                    num_sortitions_in_last_cycle = sort_count;
1,725✔
673
                    debug!(
1,725✔
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,725✔
678
                } else if ibd {
17,802✔
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
                }
17,802✔
685

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

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

723
                    // at tip, and not downloading. proceed to mine.
724
                    if last_tenure_sortition_height != sortition_db_height {
18,221✔
725
                        if is_miner {
1,741✔
726
                            info!(
1,729✔
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,741✔
735
                        globals.raise_initiative("runloop-synced".to_string());
1,741✔
736
                    }
16,480✔
737
                }
738
            }
1,546✔
739
        }
740
    }
241✔
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