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

stacks-network / stacks-core / 25903914664-1

15 May 2026 06:28AM UTC coverage: 47.122% (-38.8%) from 85.959%
25903914664-1

Pull #7199

github

94e391
web-flow
Merge 109f2828c into 1c7b8e6ac
Pull Request #7199: Feat: L1 and L2 early unlocks, updating signer

103343 of 219309 relevant lines covered (47.12%)

12880462.62 hits per line

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

89.45
/stacks-node/src/run_loop/nakamoto.rs
1
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2
// Copyright (C) 2020-2023 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
    pub fn new(
241✔
81
        config: Config,
241✔
82
        should_keep_running: Option<Arc<AtomicBool>>,
241✔
83
        counters: Option<Counters>,
241✔
84
        monitoring_thread: Option<JoinHandle<Result<(), MonitoringError>>>,
241✔
85
    ) -> Self {
241✔
86
        let channels = CoordinatorCommunication::instantiate();
241✔
87
        let should_keep_running =
241✔
88
            should_keep_running.unwrap_or_else(|| Arc::new(AtomicBool::new(true)));
241✔
89
        let pox_watchdog_comms = PoxSyncWatchdogComms::new(should_keep_running.clone());
241✔
90
        let miner_status = Arc::new(Mutex::new(MinerStatus::make_ready(
241✔
91
            config.burnchain.burn_fee_cap,
241✔
92
        )));
93

94
        let mut event_dispatcher = EventDispatcher::new(config.get_working_dir());
241✔
95
        for observer in config.events_observers.iter() {
942✔
96
            event_dispatcher.register_observer(observer);
931✔
97
        }
931✔
98
        event_dispatcher.process_pending_payloads();
241✔
99

100
        Self {
241✔
101
            config,
241✔
102
            globals: None,
241✔
103
            coordinator_channels: Some(channels),
241✔
104
            counters: counters.unwrap_or_default(),
241✔
105
            should_keep_running,
241✔
106
            event_dispatcher,
241✔
107
            pox_watchdog: None,
241✔
108
            is_miner: None,
241✔
109
            burnchain: None,
241✔
110
            pox_watchdog_comms,
241✔
111
            miner_status,
241✔
112
            monitoring_thread,
241✔
113
        }
241✔
114
    }
241✔
115

116
    pub(crate) fn get_globals(&self) -> Globals {
482✔
117
        self.globals
482✔
118
            .clone()
482✔
119
            .expect("FATAL: globals not instantiated")
482✔
120
    }
482✔
121

122
    fn set_globals(&mut self, globals: Globals) {
241✔
123
        self.globals = Some(globals);
241✔
124
    }
241✔
125

126
    pub(crate) fn get_coordinator_channel(&self) -> Option<CoordinatorChannels> {
241✔
127
        self.coordinator_channels.as_ref().map(|x| x.1.clone())
241✔
128
    }
241✔
129

130
    pub(crate) fn get_counters(&self) -> Counters {
2✔
131
        self.counters.clone()
2✔
132
    }
2✔
133

134
    pub(crate) fn config(&self) -> &Config {
24,865✔
135
        &self.config
24,865✔
136
    }
24,865✔
137

138
    pub(crate) fn get_event_dispatcher(&self) -> EventDispatcher {
482✔
139
        self.event_dispatcher.clone()
482✔
140
    }
482✔
141

142
    pub(crate) fn is_miner(&self) -> bool {
482✔
143
        self.is_miner.unwrap_or(false)
482✔
144
    }
482✔
145

146
    pub(crate) fn get_termination_switch(&self) -> Arc<AtomicBool> {
2✔
147
        self.should_keep_running.clone()
2✔
148
    }
2✔
149

150
    pub(crate) fn get_burnchain(&self) -> Burnchain {
964✔
151
        self.burnchain
964✔
152
            .clone()
964✔
153
            .expect("FATAL: tried to get runloop burnchain before calling .start()")
964✔
154
    }
964✔
155

156
    pub(crate) fn get_miner_status(&self) -> Arc<Mutex<MinerStatus>> {
241✔
157
        self.miner_status.clone()
241✔
158
    }
241✔
159

160
    /// Seconds to wait before retrying UTXO check during startup
161
    const UTXO_RETRY_INTERVAL: u64 = 10;
162
    /// Number of times to retry UTXO check during startup
163
    const UTXO_RETRY_COUNT: u64 = 6;
164

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

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

223
    /// Boot up the stacks chainstate.
224
    /// Instantiate the chainstate and push out the boot receipts to observers
225
    /// This is only public so we can test it.
226
    fn boot_chainstate(&mut self, burnchain_config: &Burnchain) -> StacksChainState {
241✔
227
        let use_test_genesis_data = use_test_genesis_chainstate(&self.config);
241✔
228

229
        // load up genesis balances
230
        let initial_balances = self
241✔
231
            .config
241✔
232
            .initial_balances
241✔
233
            .iter()
241✔
234
            .map(|e| (e.address.clone(), e.amount))
2,028✔
235
            .collect();
241✔
236

237
        // instantiate chainstate
238
        let mut boot_data = ChainStateBootData {
241✔
239
            initial_balances,
241✔
240
            post_flight_callback: None,
241✔
241
            first_burnchain_block_hash: burnchain_config.first_block_hash.clone(),
241✔
242
            first_burnchain_block_height: burnchain_config.first_block_height as u32,
241✔
243
            first_burnchain_block_timestamp: burnchain_config.first_block_timestamp,
241✔
244
            pox_constants: burnchain_config.pox_constants.clone(),
241✔
245
            get_bulk_initial_lockups: Some(Box::new(move || {
241✔
246
                get_account_lockups(use_test_genesis_data)
×
247
            })),
×
248
            get_bulk_initial_balances: Some(Box::new(move || {
241✔
249
                get_account_balances(use_test_genesis_data)
×
250
            })),
×
251
            get_bulk_initial_namespaces: Some(Box::new(move || {
241✔
252
                get_namespaces(use_test_genesis_data)
×
253
            })),
×
254
            get_bulk_initial_names: Some(Box::new(move || get_names(use_test_genesis_data))),
241✔
255
        };
256

257
        let (chain_state_db, receipts) = StacksChainState::open_and_exec(
241✔
258
            self.config.is_mainnet(),
241✔
259
            self.config.burnchain.chain_id,
241✔
260
            &self.config.get_chainstate_path_str(),
241✔
261
            Some(&mut boot_data),
241✔
262
            Some(self.config.node.get_marf_opts()),
241✔
263
        )
241✔
264
        .unwrap();
241✔
265
        run_loop::announce_boot_receipts(
241✔
266
            &mut self.event_dispatcher,
241✔
267
            &chain_state_db,
241✔
268
            &burnchain_config.pox_constants,
241✔
269
            &receipts,
241✔
270
        );
271
        chain_state_db
241✔
272
    }
241✔
273

274
    /// Instantiate the Stacks chain state and start the chains coordinator thread.
275
    /// Returns the coordinator thread handle, and the receiving end of the coordinator's atlas
276
    /// attachment channel.
277
    fn spawn_chains_coordinator(
241✔
278
        &mut self,
241✔
279
        burnchain_config: &Burnchain,
241✔
280
        coordinator_receivers: CoordinatorReceivers,
241✔
281
        miner_status: Arc<Mutex<MinerStatus>>,
241✔
282
    ) -> JoinHandle<()> {
241✔
283
        let use_test_genesis_data = use_test_genesis_chainstate(&self.config);
241✔
284

285
        // load up genesis Atlas attachments
286
        let mut atlas_config = AtlasConfig::new(self.config.is_mainnet());
241✔
287
        let genesis_attachments = GenesisData::new(use_test_genesis_data)
241✔
288
            .read_name_zonefiles()
241✔
289
            .map(|z| Attachment::new(z.zonefile_content.as_bytes().to_vec()))
2,892✔
290
            .collect();
241✔
291
        atlas_config.genesis_attachments = Some(genesis_attachments);
241✔
292

293
        let chain_state_db = self.boot_chainstate(burnchain_config);
241✔
294

295
        // NOTE: re-instantiate AtlasConfig so we don't have to keep the genesis attachments around
296
        let moved_atlas_config = self.config.atlas.clone();
241✔
297
        let moved_config = self.config.clone();
241✔
298
        let moved_burnchain_config = burnchain_config.clone();
241✔
299
        let coordinator_dispatcher = self.event_dispatcher.clone();
241✔
300
        let atlas_db = AtlasDB::connect(
241✔
301
            moved_atlas_config.clone(),
241✔
302
            &self.config.get_atlas_db_file_path(),
241✔
303
            true,
304
        )
305
        .expect("Failed to connect Atlas DB during startup");
241✔
306
        let coordinator_indexer =
241✔
307
            make_bitcoin_indexer(&self.config, Some(self.should_keep_running.clone()));
241✔
308

309
        let rpc_port = moved_config
241✔
310
            .node
241✔
311
            .rpc_bind_addr()
241✔
312
            .unwrap_or_else(|| panic!("Failed to parse socket: {}", &moved_config.node.rpc_bind))
241✔
313
            .port();
241✔
314
        let coordinator_thread_handle = thread::Builder::new()
241✔
315
            .name(format!("chains-coordinator:{rpc_port}"))
241✔
316
            .stack_size(BLOCK_PROCESSOR_STACK_SIZE)
241✔
317
            .spawn(move || {
241✔
318
                debug!(
241✔
319
                    "chains-coordinator thread ID is {:?}",
320
                    thread::current().id()
×
321
                );
322
                let mut cost_estimator = moved_config.make_cost_estimator();
241✔
323
                let mut fee_estimator = moved_config.make_fee_estimator();
241✔
324

325
                let coord_config = ChainsCoordinatorConfig {
241✔
326
                    txindex: moved_config.node.txindex,
241✔
327
                };
241✔
328
                ChainsCoordinator::run(
241✔
329
                    coord_config,
241✔
330
                    chain_state_db,
241✔
331
                    moved_burnchain_config,
241✔
332
                    &coordinator_dispatcher,
241✔
333
                    coordinator_receivers,
241✔
334
                    moved_atlas_config,
241✔
335
                    cost_estimator.as_deref_mut(),
241✔
336
                    fee_estimator.as_deref_mut(),
241✔
337
                    miner_status,
241✔
338
                    coordinator_indexer,
241✔
339
                    atlas_db,
241✔
340
                );
341
            })
241✔
342
            .expect("FATAL: failed to start chains coordinator thread");
241✔
343

344
        coordinator_thread_handle
241✔
345
    }
241✔
346

347
    /// Start Prometheus logging
348
    fn start_prometheus(&mut self) {
241✔
349
        if self.monitoring_thread.is_some() {
241✔
350
            info!("Monitoring thread already running, nakamoto run-loop will not restart it");
6✔
351
            return;
6✔
352
        }
235✔
353
        let Some(prometheus_bind) = self.config.node.prometheus_bind.clone() else {
235✔
354
            return;
234✔
355
        };
356
        let monitoring_thread = thread::Builder::new()
1✔
357
            .name("prometheus".to_string())
1✔
358
            .spawn(move || {
1✔
359
                debug!("prometheus thread ID is {:?}", thread::current().id());
1✔
360
                start_serving_monitoring_metrics(prometheus_bind)
1✔
361
            })
1✔
362
            .expect("FATAL: failed to start monitoring thread");
1✔
363

364
        self.monitoring_thread.replace(monitoring_thread);
1✔
365
    }
241✔
366

367
    /// Get the sortition DB's highest block height, aligned to a reward cycle boundary, and the
368
    /// highest sortition.
369
    /// Returns (height at rc start, sortition)
370
    fn get_reward_cycle_sortition_db_height(
241✔
371
        sortdb: &SortitionDB,
241✔
372
        burnchain_config: &Burnchain,
241✔
373
    ) -> (u64, BlockSnapshot) {
241✔
374
        let (stacks_ch, _) = SortitionDB::get_canonical_stacks_chain_tip_hash(sortdb.conn())
241✔
375
            .expect("BUG: failed to load canonical stacks chain tip hash");
241✔
376

377
        let sn = match SortitionDB::get_block_snapshot_consensus(sortdb.conn(), &stacks_ch)
241✔
378
            .expect("BUG: failed to query sortition DB")
241✔
379
        {
380
            Some(sn) => sn,
241✔
381
            None => {
382
                debug!("No canonical stacks chain tip hash present");
×
383
                let sn = SortitionDB::get_first_block_snapshot(sortdb.conn())
×
384
                    .expect("BUG: failed to get first-ever block snapshot");
×
385
                sn
×
386
            }
387
        };
388

389
        (
241✔
390
            burnchain_config.reward_cycle_to_block_height(
241✔
391
                burnchain_config
241✔
392
                    .block_height_to_reward_cycle(sn.block_height)
241✔
393
                    .expect("BUG: snapshot preceeds first reward cycle"),
241✔
394
            ),
241✔
395
            sn,
241✔
396
        )
241✔
397
    }
241✔
398

399
    /// Starts the node runloop.
400
    ///
401
    /// This function will block by looping infinitely.
402
    /// It will start the burnchain (separate thread), set-up a channel in
403
    /// charge of coordinating the new blocks coming from the burnchain and
404
    /// the nodes, taking turns on tenures.
405
    pub fn start(
241✔
406
        &mut self,
241✔
407
        burnchain_opt: Option<Burnchain>,
241✔
408
        mut mine_start: u64,
241✔
409
        data_from_neon: Option<Neon2NakaData>,
241✔
410
    ) {
241✔
411
        let (coordinator_receivers, coordinator_senders) = self
241✔
412
            .coordinator_channels
241✔
413
            .take()
241✔
414
            .expect("Run loop already started, can only start once after initialization.");
241✔
415

416
        // Apply config-driven process-wide state before any chainstate is opened.
417
        self.config.apply_runtime_state();
241✔
418

419
        // setup the termination handler, allow it to error if a prior runloop already set it
420
        neon::RunLoop::setup_termination_handler(self.should_keep_running.clone(), true);
241✔
421

422
        let burnchain_result = neon::RunLoop::instantiate_burnchain_state(
241✔
423
            &self.config,
241✔
424
            self.should_keep_running.clone(),
241✔
425
            burnchain_opt,
241✔
426
            coordinator_senders.clone(),
241✔
427
        );
428

429
        let mut burnchain = match burnchain_result {
241✔
430
            Ok(burnchain_controller) => burnchain_controller,
241✔
431
            Err(burnchain_error::ShutdownInitiated) => {
432
                info!("Exiting stacks-node");
×
433
                return;
×
434
            }
435
            Err(e) => {
×
436
                error!("Error initializing burnchain: {e}");
×
437
                info!("Exiting stacks-node");
×
438
                return;
×
439
            }
440
        };
441

442
        let burnchain_config = burnchain.get_burnchain();
241✔
443
        self.burnchain = Some(burnchain_config.clone());
241✔
444

445
        // can we mine?
446
        let is_miner = self.check_is_miner(&mut burnchain);
241✔
447
        self.is_miner = Some(is_miner);
241✔
448

449
        // relayer linkup
450
        let (relay_send, relay_recv) = sync_channel(RELAYER_MAX_BUFFER);
241✔
451

452
        // set up globals so other subsystems can instantiate off of the runloop state.
453
        let globals = Globals::new(
241✔
454
            coordinator_senders,
241✔
455
            self.get_miner_status(),
241✔
456
            relay_send,
241✔
457
            self.counters.clone(),
241✔
458
            self.pox_watchdog_comms.clone(),
241✔
459
            self.should_keep_running.clone(),
241✔
460
            mine_start,
241✔
461
            LeaderKeyRegistrationState::default(),
241✔
462
        );
463
        self.set_globals(globals.clone());
241✔
464

465
        // have headers; boot up the chains coordinator and instantiate the chain state
466
        let coordinator_thread_handle = self.spawn_chains_coordinator(
241✔
467
            &burnchain_config,
241✔
468
            coordinator_receivers,
241✔
469
            globals.get_miner_status(),
241✔
470
        );
471
        self.start_prometheus();
241✔
472

473
        // We announce a new burn block so that the chains coordinator
474
        // can resume prior work and handle eventual unprocessed sortitions
475
        // stored during a previous session.
476
        globals.coord().announce_new_burn_block();
241✔
477

478
        // Make sure at least one sortition has happened, and make sure it's globally available
479
        let sortdb = burnchain.sortdb_mut();
241✔
480
        let (rc_aligned_height, sn) =
241✔
481
            RunLoop::get_reward_cycle_sortition_db_height(sortdb, &burnchain_config);
241✔
482

483
        let burnchain_tip_snapshot = if sn.block_height == burnchain_config.first_block_height {
241✔
484
            // need at least one sortition to happen.
485
            burnchain
×
486
                .wait_for_sortitions(globals.coord().clone(), sn.block_height + 1)
×
487
                .expect("Unable to get burnchain tip")
×
488
                .block_snapshot
×
489
        } else {
490
            sn
241✔
491
        };
492

493
        globals.set_last_sortition(burnchain_tip_snapshot);
241✔
494

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

499
        // Wait for all pending sortitions to process
500
        let burnchain_db = burnchain_config
241✔
501
            .open_burnchain_db(false)
241✔
502
            .expect("FATAL: failed to open burnchain DB");
241✔
503
        let burnchain_db_tip = burnchain_db
241✔
504
            .get_canonical_chain_tip()
241✔
505
            .expect("FATAL: failed to query burnchain DB");
241✔
506
        let mut burnchain_tip = burnchain
241✔
507
            .wait_for_sortitions(globals.coord().clone(), burnchain_db_tip.block_height)
241✔
508
            .expect("Unable to get burnchain tip");
241✔
509

510
        // Start the runloop
511
        debug!("Runloop: Begin run loop");
241✔
512
        self.counters.bump_blocks_processed();
241✔
513

514
        let mut sortition_db_height = rc_aligned_height;
241✔
515
        let mut burnchain_height = sortition_db_height;
241✔
516
        let mut num_sortitions_in_last_cycle;
517

518
        // prepare to fetch the first reward cycle!
519
        let mut target_burnchain_block_height = cmp::min(
241✔
520
            burnchain_config.reward_cycle_to_block_height(
241✔
521
                burnchain_config
241✔
522
                    .block_height_to_reward_cycle(burnchain_height)
241✔
523
                    .expect("BUG: block height is not in a reward cycle")
241✔
524
                    + 1,
241✔
525
            ),
526
            burnchain.get_headers_height() - 1,
241✔
527
        );
528

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

531
        let mut last_tenure_sortition_height = 0;
241✔
532
        let mut poll_deadline = 0;
241✔
533

534
        loop {
535
            if !globals.keep_running() {
20,084✔
536
                // The p2p thread relies on the same atomic_bool, it will
537
                // discontinue its execution after completing its ongoing runloop epoch.
538
                info!("Terminating p2p process");
221✔
539
                info!("Terminating relayer");
221✔
540
                info!("Terminating chains-coordinator");
221✔
541

542
                globals.coord().stop_chains_coordinator();
221✔
543
                coordinator_thread_handle.join().unwrap();
221✔
544
                node.join();
221✔
545

546
                info!("Exiting stacks-node");
221✔
547
                break;
221✔
548
            }
19,863✔
549

550
            let remote_chain_height = burnchain.get_headers_height() - 1;
19,863✔
551

552
            // wait for the p2p state-machine to do at least one pass
553
            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,863✔
554

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

564
            // calculate burnchain sync percentage
565
            let percent: f64 = if remote_chain_height > 0 {
19,863✔
566
                burnchain_tip.block_snapshot.block_height as f64 / remote_chain_height as f64
19,863✔
567
            } else {
568
                0.0
×
569
            };
570

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

586
            loop {
587
                if !globals.keep_running() {
38,833✔
588
                    break;
219✔
589
                }
38,614✔
590

591
                if poll_deadline > get_epoch_time_secs() {
38,614✔
592
                    sleep_ms(1_000);
18,982✔
593
                    continue;
18,982✔
594
                }
19,632✔
595
                poll_deadline = get_epoch_time_secs() + self.config().burnchain.poll_time_secs;
19,632✔
596

597
                let (next_burnchain_tip, tip_burnchain_height) =
19,625✔
598
                    match burnchain.sync(Some(target_burnchain_block_height)) {
19,632✔
599
                        Ok(x) => x,
19,625✔
600
                        Err(e) => {
7✔
601
                            warn!("Runloop: Burnchain controller stopped: {e}");
7✔
602
                            continue;
7✔
603
                        }
604
                    };
605

606
                // *now* we know the burnchain height
607
                burnchain_tip = next_burnchain_tip;
19,625✔
608
                burnchain_height = tip_burnchain_height;
19,625✔
609

610
                let sortition_tip = &burnchain_tip.block_snapshot.sortition_id;
19,625✔
611
                let next_sortition_height = burnchain_tip.block_snapshot.block_height;
19,625✔
612

613
                if next_sortition_height != last_tenure_sortition_height {
19,625✔
614
                    info!(
1,829✔
615
                        "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}"
616
                    );
617
                }
17,796✔
618

619
                if next_sortition_height > sortition_db_height {
19,625✔
620
                    debug!(
1,829✔
621
                        "Runloop: New burnchain block height {next_sortition_height} > {sortition_db_height}"
622
                    );
623

624
                    let mut sort_count = 0;
1,829✔
625

626
                    debug!("Runloop: block mining until we process all sortitions");
1,829✔
627
                    signal_mining_blocked(globals.get_miner_status());
1,829✔
628

629
                    // first, let's process all blocks in (sortition_db_height, next_sortition_height]
630
                    for block_to_process in (sortition_db_height + 1)..(next_sortition_height + 1) {
4,530✔
631
                        // stop mining so we can advance the sortition DB and so our
632
                        // ProcessTenure() directive (sent by relayer_sortition_notify() below)
633
                        // will be unblocked.
634

635
                        let block = {
4,530✔
636
                            let ic = burnchain.sortdb_ref().index_conn();
4,530✔
637
                            SortitionDB::get_ancestor_snapshot(&ic, block_to_process, sortition_tip)
4,530✔
638
                                .unwrap()
4,530✔
639
                                .expect(
4,530✔
640
                                    "Failed to find block in fork processed by burnchain indexer",
4,530✔
641
                                )
642
                        };
643
                        if block.sortition {
4,530✔
644
                            sort_count += 1;
4,359✔
645
                        }
4,359✔
646

647
                        let sortition_id = &block.sortition_id;
4,530✔
648

649
                        // Have the node process the new block, that can include, or not, a sortition.
650
                        if let Err(e) = node.process_burnchain_state(
4,530✔
651
                            self.config(),
4,530✔
652
                            burnchain.sortdb_mut(),
4,530✔
653
                            sortition_id,
4,530✔
654
                            ibd,
4,530✔
655
                        ) {
4,530✔
656
                            // relayer errored, exit.
657
                            error!("Runloop: Block relayer and miner errored, exiting."; "err" => ?e);
20✔
658
                            return;
20✔
659
                        }
4,510✔
660
                    }
661

662
                    debug!("Runloop: enable miner after processing sortitions");
1,809✔
663
                    signal_mining_ready(globals.get_miner_status());
1,809✔
664

665
                    num_sortitions_in_last_cycle = sort_count;
1,809✔
666
                    debug!(
1,809✔
667
                        "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"
668
                    );
669

670
                    sortition_db_height = next_sortition_height;
1,809✔
671
                } else if ibd {
17,796✔
672
                    // drive block processing after we reach the burnchain tip.
×
673
                    // we may have downloaded all the blocks already,
×
674
                    // so we can't rely on the relayer alone to
×
675
                    // drive it.
×
676
                    globals.coord().announce_new_stacks_block();
×
677
                }
17,796✔
678

679
                if burnchain_height >= target_burnchain_block_height
19,605✔
680
                    || burnchain_height >= remote_chain_height
18✔
681
                {
682
                    break;
19,624✔
683
                }
18✔
684
            }
685

686
            // advance one reward cycle at a time.
687
            // If we're still downloading, then this is simply target_burnchain_block_height + reward_cycle_len.
688
            // Otherwise, this is burnchain_tip + reward_cycle_len
689
            let next_target_burnchain_block_height = cmp::min(
19,843✔
690
                burnchain_config.reward_cycle_to_block_height(
19,843✔
691
                    burnchain_config
19,843✔
692
                        .block_height_to_reward_cycle(target_burnchain_block_height)
19,843✔
693
                        .expect("FATAL: burnchain height before system start")
19,843✔
694
                        + 1,
19,843✔
695
                ),
696
                remote_chain_height,
19,843✔
697
            );
698

699
            debug!("Runloop: Advance target burnchain block height from {target_burnchain_block_height} to {next_target_burnchain_block_height} (sortition height {sortition_db_height})");
19,843✔
700
            target_burnchain_block_height = next_target_burnchain_block_height;
19,843✔
701

702
            if sortition_db_height >= burnchain_height && !ibd {
19,843✔
703
                let canonical_stacks_tip_height =
18,210✔
704
                    SortitionDB::get_canonical_burn_chain_tip(burnchain.sortdb_ref().conn())
18,210✔
705
                        .map(|snapshot| snapshot.canonical_stacks_tip_height)
18,210✔
706
                        .unwrap_or(0);
18,210✔
707
                if canonical_stacks_tip_height < mine_start {
18,210✔
708
                    info!(
×
709
                        "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"
710
                    );
711
                } else {
712
                    // once we've synced to the chain tip once, don't apply this check again.
713
                    //  this prevents a possible corner case in the event of a PoX fork.
714
                    mine_start = 0;
18,210✔
715

716
                    // at tip, and not downloading. proceed to mine.
717
                    if last_tenure_sortition_height != sortition_db_height {
18,210✔
718
                        if is_miner {
1,824✔
719
                            info!(
1,812✔
720
                                "Runloop: Synchronized full burnchain up to height {sortition_db_height}. Proceeding to mine blocks"
721
                            );
722
                        } else {
723
                            info!(
12✔
724
                                "Runloop: Synchronized full burnchain up to height {sortition_db_height}."
725
                            );
726
                        }
727
                        last_tenure_sortition_height = sortition_db_height;
1,824✔
728
                        globals.raise_initiative("runloop-synced".to_string());
1,824✔
729
                    }
16,386✔
730
                }
731
            }
1,633✔
732
        }
733
    }
241✔
734
}
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