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

stacks-network / stacks-core / 26829844658-1

02 Jun 2026 03:24PM UTC coverage: 86.038% (+0.08%) from 85.959%
26829844658-1

Pull #7249

github

06b7ec
web-flow
Merge e017b11f4 into b945664c8
Pull Request #7249: feat: seamless rollovers between bond and stx-only staking positions

382 of 407 new or added lines in 3 files covered. (93.86%)

6474 existing lines in 95 files now uncovered.

194813 of 226426 relevant lines covered (86.04%)

18725269.97 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

354
        coordinator_thread_handle
256✔
355
    }
256✔
356

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

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

426
        // Apply config-driven process-wide state before any chainstate is opened.
427
        self.config.apply_runtime_state();
256✔
428

429
        // setup the termination handler, allow it to error if a prior runloop already set it
430
        neon::RunLoop::setup_termination_handler(self.should_keep_running.clone(), true);
256✔
431

432
        let burnchain_result = neon::RunLoop::instantiate_burnchain_state(
256✔
433
            &self.config,
256✔
434
            self.should_keep_running.clone(),
256✔
435
            burnchain_opt,
256✔
436
            coordinator_senders.clone(),
256✔
437
        );
438

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

452
        let burnchain_config = burnchain.get_burnchain();
256✔
453
        self.burnchain = Some(burnchain_config.clone());
256✔
454

455
        // can we mine?
456
        let is_miner = self.check_is_miner(&mut burnchain);
256✔
457
        self.is_miner = Some(is_miner);
256✔
458

459
        // relayer linkup
460
        let (relay_send, relay_recv) = sync_channel(RELAYER_MAX_BUFFER);
256✔
461

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

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

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

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

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

503
        globals.set_last_sortition(burnchain_tip_snapshot);
256✔
504

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

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

520
        // Start the runloop
521
        debug!("Runloop: Begin run loop");
256✔
522
        self.counters.bump_blocks_processed();
256✔
523

524
        let mut sortition_db_height = rc_aligned_height;
256✔
525
        let mut burnchain_height = sortition_db_height;
256✔
526
        let mut num_sortitions_in_last_cycle;
527

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

539
        debug!("Runloop: Begin main runloop starting a burnchain block {sortition_db_height}");
256✔
540

541
        let mut last_tenure_sortition_height = 0;
256✔
542
        let mut poll_deadline = 0;
256✔
543

544
        loop {
545
            if !globals.keep_running() {
23,837✔
546
                // The p2p thread relies on the same atomic_bool, it will
547
                // discontinue its execution after completing its ongoing runloop epoch.
548
                info!("Terminating p2p process");
234✔
549
                info!("Terminating relayer");
234✔
550
                info!("Terminating chains-coordinator");
234✔
551

552
                globals.coord().stop_chains_coordinator();
234✔
553
                coordinator_thread_handle.join().unwrap();
234✔
554
                node.join();
234✔
555

556
                info!("Exiting stacks-node");
234✔
557
                break;
234✔
558
            }
23,603✔
559

560
            let remote_chain_height = burnchain.get_headers_height() - 1;
23,603✔
561

562
            // wait for the p2p state-machine to do at least one pass
563
            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);
23,603✔
564

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

574
            // calculate burnchain sync percentage
575
            let percent: f64 = if remote_chain_height > 0 {
23,603✔
576
                burnchain_tip.block_snapshot.block_height as f64 / remote_chain_height as f64
23,603✔
577
            } else {
UNCOV
578
                0.0
×
579
            };
580

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

596
            loop {
597
                if !globals.keep_running() {
46,277✔
598
                    break;
232✔
599
                }
46,045✔
600

601
                if poll_deadline > get_epoch_time_secs() {
46,045✔
602
                    sleep_ms(1_000);
22,692✔
603
                    continue;
22,692✔
604
                }
23,353✔
605
                poll_deadline = get_epoch_time_secs() + self.config().burnchain.poll_time_secs;
23,353✔
606

607
                let (next_burnchain_tip, tip_burnchain_height) =
23,350✔
608
                    match burnchain.sync(Some(target_burnchain_block_height)) {
23,353✔
609
                        Ok(x) => x,
23,350✔
610
                        Err(e) => {
3✔
611
                            warn!("Runloop: Burnchain controller stopped: {e}");
3✔
612
                            continue;
3✔
613
                        }
614
                    };
615

616
                // *now* we know the burnchain height
617
                burnchain_tip = next_burnchain_tip;
23,350✔
618
                burnchain_height = tip_burnchain_height;
23,350✔
619

620
                let sortition_tip = &burnchain_tip.block_snapshot.sortition_id;
23,350✔
621
                let next_sortition_height = burnchain_tip.block_snapshot.block_height;
23,350✔
622

623
                if next_sortition_height != last_tenure_sortition_height {
23,350✔
624
                    info!(
2,193✔
625
                        "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}"
626
                    );
627
                }
21,157✔
628

629
                if next_sortition_height > sortition_db_height {
23,350✔
630
                    debug!(
2,193✔
631
                        "Runloop: New burnchain block height {next_sortition_height} > {sortition_db_height}"
632
                    );
633

634
                    let mut sort_count = 0;
2,193✔
635

636
                    debug!("Runloop: block mining until we process all sortitions");
2,193✔
637
                    signal_mining_blocked(globals.get_miner_status());
2,193✔
638

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

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

657
                        let sortition_id = &block.sortition_id;
5,075✔
658

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

672
                    debug!("Runloop: enable miner after processing sortitions");
2,171✔
673
                    signal_mining_ready(globals.get_miner_status());
2,171✔
674

675
                    num_sortitions_in_last_cycle = sort_count;
2,171✔
676
                    debug!(
2,171✔
677
                        "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"
678
                    );
679

680
                    sortition_db_height = next_sortition_height;
2,171✔
681
                } else if ibd {
21,157✔
682
                    // drive block processing after we reach the burnchain tip.
×
683
                    // we may have downloaded all the blocks already,
×
UNCOV
684
                    // so we can't rely on the relayer alone to
×
UNCOV
685
                    // drive it.
×
UNCOV
686
                    globals.coord().announce_new_stacks_block();
×
687
                }
21,157✔
688

689
                if burnchain_height >= target_burnchain_block_height
23,328✔
690
                    || burnchain_height >= remote_chain_height
1✔
691
                {
692
                    break;
23,349✔
693
                }
1✔
694
            }
695

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

709
            debug!("Runloop: Advance target burnchain block height from {target_burnchain_block_height} to {next_target_burnchain_block_height} (sortition height {sortition_db_height})");
23,581✔
710
            target_burnchain_block_height = next_target_burnchain_block_height;
23,581✔
711

712
            if sortition_db_height >= burnchain_height && !ibd {
23,581✔
713
                let canonical_stacks_tip_height =
21,595✔
714
                    SortitionDB::get_canonical_burn_chain_tip(burnchain.sortdb_ref().conn())
21,595✔
715
                        .map(|snapshot| snapshot.canonical_stacks_tip_height)
21,595✔
716
                        .unwrap_or(0);
21,595✔
717
                if canonical_stacks_tip_height < mine_start {
21,595✔
UNCOV
718
                    info!(
×
719
                        "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"
720
                    );
721
                } else {
722
                    // once we've synced to the chain tip once, don't apply this check again.
723
                    //  this prevents a possible corner case in the event of a PoX fork.
724
                    mine_start = 0;
21,595✔
725

726
                    // at tip, and not downloading. proceed to mine.
727
                    if last_tenure_sortition_height != sortition_db_height {
21,595✔
728
                        if is_miner {
2,186✔
729
                            info!(
2,174✔
730
                                "Runloop: Synchronized full burnchain up to height {sortition_db_height}. Proceeding to mine blocks"
731
                            );
732
                        } else {
733
                            info!(
12✔
734
                                "Runloop: Synchronized full burnchain up to height {sortition_db_height}."
735
                            );
736
                        }
737
                        last_tenure_sortition_height = sortition_db_height;
2,186✔
738
                        globals.raise_initiative("runloop-synced".to_string());
2,186✔
739
                    }
19,409✔
740
                }
741
            }
1,986✔
742
        }
743
    }
256✔
744
}
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