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

stacks-network / stacks-core / 25904007932-1

15 May 2026 06:31AM UTC coverage: 47.459% (-38.5%) from 85.959%
25904007932-1

Pull #7210

github

869a54
web-flow
Merge 27877974d into 1c7b8e6ac
Pull Request #7210: [wip] epoch 4 release branch

36 of 53 new or added lines in 1 file covered. (67.92%)

88645 existing lines in 346 files now uncovered.

104136 of 219422 relevant lines covered (47.46%)

12897381.15 hits per line

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

88.67
/stacks-node/src/run_loop/boot_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, Ordering};
17
use std::sync::{Arc, Mutex};
18
use std::thread::JoinHandle;
19
use std::time::Duration;
20
use std::{fs, thread};
21

22
use stacks::burnchains::Burnchain;
23
use stacks::chainstate::burn::db::sortdb::SortitionDB;
24
use stacks::chainstate::coordinator::comm::CoordinatorChannels;
25
use stacks::net::p2p::PeerNetwork;
26
use stacks_common::types::StacksEpochId;
27

28
use crate::event_dispatcher::EventDispatcher;
29
use crate::globals::NeonGlobals;
30
use crate::neon::Counters;
31
use crate::neon_node::LeaderKeyRegistrationState;
32
use crate::run_loop::nakamoto::RunLoop as NakaRunLoop;
33
use crate::run_loop::neon::RunLoop as NeonRunLoop;
34
use crate::Config;
35

36
/// Data which should persist through transition from Neon => Nakamoto run loop
37
#[derive(Default)]
38
pub struct Neon2NakaData {
39
    pub leader_key_registration_state: LeaderKeyRegistrationState,
40
    pub peer_network: Option<PeerNetwork>,
41
}
42

43
impl Neon2NakaData {
44
    /// Take needed values from `NeonGlobals` and optionally `PeerNetwork`, consuming them
45
    pub fn new(globals: NeonGlobals, peer_network: Option<PeerNetwork>) -> Self {
243✔
46
        let key_state = globals
243✔
47
            .leader_key_registration_state
243✔
48
            .lock()
243✔
49
            .unwrap_or_else(|e| {
243✔
50
                // can only happen due to a thread panic in the relayer
51
                error!("FATAL: leader key registration mutex is poisoned: {e:?}");
×
52
                panic!();
×
53
            });
54

55
        Self {
243✔
56
            leader_key_registration_state: (*key_state).clone(),
243✔
57
            peer_network,
243✔
58
        }
243✔
59
    }
243✔
60
}
61

62
/// This runloop handles booting to Nakamoto:
63
/// During epochs [1.0, 2.5], it runs a neon run_loop.
64
/// Once epoch 3.0 is reached, it stops the neon run_loop
65
///  and starts nakamoto.
66
pub struct BootRunLoop {
67
    config: Config,
68
    active_loop: InnerLoops,
69
    coordinator_channels: Arc<Mutex<CoordinatorChannels>>,
70
}
71

72
enum InnerLoops {
73
    Epoch2(NeonRunLoop),
74
    Epoch3(NakaRunLoop),
75
}
76

77
impl BootRunLoop {
78
    pub fn new(config: Config) -> Result<Self, String> {
248✔
79
        let (coordinator_channels, active_loop) = if !Self::reached_epoch_30_transition(&config)? {
248✔
80
            let neon = NeonRunLoop::new(config.clone());
246✔
81
            (
246✔
82
                neon.get_coordinator_channel().unwrap(),
246✔
83
                InnerLoops::Epoch2(neon),
246✔
84
            )
246✔
85
        } else {
86
            let naka = NakaRunLoop::new(config.clone(), None, None, None);
2✔
87
            (
2✔
88
                naka.get_coordinator_channel().unwrap(),
2✔
89
                InnerLoops::Epoch3(naka),
2✔
90
            )
2✔
91
        };
92

93
        Ok(BootRunLoop {
248✔
94
            config,
248✔
95
            active_loop,
248✔
96
            coordinator_channels: Arc::new(Mutex::new(coordinator_channels)),
248✔
97
        })
248✔
98
    }
248✔
99

100
    /// Get a mutex-guarded pointer to this run-loops coordinator channels.
101
    ///  The reason this must be mutex guarded is that the run loop will switch
102
    ///  from a "neon" coordinator to a "nakamoto" coordinator, and update the
103
    ///  backing coordinator channel. That way, anyone still holding the Arc<>
104
    ///  should be able to query the new coordinator channel.
105
    pub fn coordinator_channels(&self) -> Arc<Mutex<CoordinatorChannels>> {
247✔
106
        self.coordinator_channels.clone()
247✔
107
    }
247✔
108

109
    /// Get the runtime counters for the inner runloop. The nakamoto
110
    ///  runloop inherits the counters object from the neon node,
111
    ///  so no need for another layer of indirection/mutex.
112
    pub fn counters(&self) -> Counters {
298✔
113
        match &self.active_loop {
298✔
114
            InnerLoops::Epoch2(x) => x.get_counters(),
296✔
115
            InnerLoops::Epoch3(x) => x.get_counters(),
2✔
116
        }
117
    }
298✔
118

119
    /// Get the termination switch from the active run loop.
120
    pub fn get_termination_switch(&self) -> Arc<AtomicBool> {
248✔
121
        match &self.active_loop {
248✔
122
            InnerLoops::Epoch2(x) => x.get_termination_switch(),
246✔
123
            InnerLoops::Epoch3(x) => x.get_termination_switch(),
2✔
124
        }
125
    }
248✔
126

127
    /// Get the event dispatcher
128
    pub fn get_event_dispatcher(&self) -> EventDispatcher {
×
129
        match &self.active_loop {
×
130
            InnerLoops::Epoch2(x) => x.get_event_dispatcher(),
×
131
            InnerLoops::Epoch3(x) => x.get_event_dispatcher(),
×
132
        }
133
    }
×
134

135
    /// The main entry point for the run loop. This starts either a 2.x-neon or 3.x-nakamoto
136
    /// node depending on the current burnchain height.
137
    pub fn start(&mut self, burnchain_opt: Option<Burnchain>, mine_start: u64) {
248✔
138
        match self.active_loop {
248✔
139
            InnerLoops::Epoch2(_) => self.start_from_neon(burnchain_opt, mine_start),
246✔
140
            InnerLoops::Epoch3(_) => self.start_from_naka(burnchain_opt, mine_start),
2✔
141
        }
142
    }
248✔
143

144
    fn start_from_naka(&mut self, burnchain_opt: Option<Burnchain>, mine_start: u64) {
2✔
145
        let InnerLoops::Epoch3(ref mut naka_loop) = self.active_loop else {
2✔
146
            panic!("FATAL: unexpectedly invoked start_from_naka when active loop wasn't nakamoto");
×
147
        };
148
        naka_loop.start(burnchain_opt, mine_start, None)
2✔
149
    }
2✔
150

151
    // configuring mutants::skip -- this function is covered through integration tests (this function
152
    //  is pretty definitionally an integration, so thats unavoidable), and the integration tests
153
    //  do not get counted in mutants coverage.
154
    #[cfg_attr(test, mutants::skip)]
155
    fn start_from_neon(&mut self, burnchain_opt: Option<Burnchain>, mine_start: u64) {
246✔
156
        let InnerLoops::Epoch2(ref mut neon_loop) = self.active_loop else {
246✔
157
            panic!("FATAL: unexpectedly invoked start_from_neon when active loop wasn't neon");
×
158
        };
159
        let termination_switch = neon_loop.get_termination_switch();
246✔
160
        let counters = neon_loop.get_counters();
246✔
161

162
        let boot_thread = Self::spawn_stopper(&self.config, neon_loop)
246✔
163
            .expect("FATAL: failed to spawn epoch-2/3-boot thread");
246✔
164
        let data_to_naka = neon_loop.start(burnchain_opt.clone(), mine_start);
246✔
165

166
        let monitoring_thread = neon_loop.take_monitoring_thread();
246✔
167
        // did we exit because of the epoch-3.0 transition, or some other reason?
168
        let exited_for_transition = boot_thread
246✔
169
            .join()
246✔
170
            .expect("FATAL: failed to join epoch-2/3-boot thread");
246✔
171
        if !exited_for_transition {
246✔
172
            info!("Shutting down epoch-2/3 transition thread");
5✔
173
            return;
5✔
174
        }
241✔
175

176
        info!(
241✔
177
            "Reached Epoch-3.0 boundary, starting nakamoto node";
178
            "with_neon_data" => data_to_naka.is_some(),
241✔
179
            "with_p2p_stack" => data_to_naka.as_ref().map(|x| x.peer_network.is_some()).unwrap_or(false)
241✔
180
        );
181
        termination_switch.store(true, Ordering::SeqCst);
241✔
182
        let naka = NakaRunLoop::new(
241✔
183
            self.config.clone(),
241✔
184
            Some(termination_switch),
241✔
185
            Some(counters),
241✔
186
            monitoring_thread,
241✔
187
        );
188
        let new_coord_channels = naka
241✔
189
            .get_coordinator_channel()
241✔
190
            .expect("FATAL: should have coordinator channel in newly instantiated runloop");
241✔
191
        {
241✔
192
            let mut coord_channel = self.coordinator_channels.lock().expect("Mutex poisoned");
241✔
193
            *coord_channel = new_coord_channels;
241✔
194
        }
241✔
195
        self.active_loop = InnerLoops::Epoch3(naka);
241✔
196
        let InnerLoops::Epoch3(ref mut naka_loop) = self.active_loop else {
241✔
UNCOV
197
            panic!("FATAL: unexpectedly found epoch2 loop after setting epoch3 active");
×
198
        };
199
        naka_loop.start(burnchain_opt, mine_start, data_to_naka)
241✔
200
    }
246✔
201

202
    fn spawn_stopper(
246✔
203
        config: &Config,
246✔
204
        neon: &NeonRunLoop,
246✔
205
    ) -> Result<JoinHandle<bool>, std::io::Error> {
246✔
206
        let neon_term_switch = neon.get_termination_switch();
246✔
207
        let config = config.clone();
246✔
208
        thread::Builder::new()
246✔
209
            .name("epoch-2/3-boot".into())
246✔
210
            .spawn(move || {
246✔
211
                loop {
212
                    let do_transition = Self::reached_epoch_30_transition(&config)
26,318✔
213
                        .unwrap_or_else(|err| {
26,318✔
UNCOV
214
                            warn!("Error checking for Epoch-3.0 transition: {err:?}. Assuming transition did not occur yet.");
×
215
                            false
×
216
                        });
×
217
                    if do_transition {
26,318✔
218
                        break;
241✔
219
                    }
26,077✔
220
                    if !neon_term_switch.load(Ordering::SeqCst) {
26,077✔
221
                        info!("Stop requested, exiting epoch-2/3-boot thread");
5✔
222
                        return false;
5✔
223
                    }
26,072✔
224
                    thread::sleep(Duration::from_secs(1));
26,072✔
225
                }
226
                // if loop exited, do the transition
227
                info!("Epoch-3.0 boundary reached, stopping Epoch-2.x run loop");
241✔
228
                neon_term_switch.store(false, Ordering::SeqCst);
241✔
229
                true
241✔
230
            })
246✔
231
    }
246✔
232

233
    fn reached_epoch_30_transition(config: &Config) -> Result<bool, String> {
26,566✔
234
        let burn_height = Self::get_burn_height(config);
26,566✔
235
        let epochs = config.burnchain.get_epoch_list();
26,566✔
236
        let epoch_3 = epochs
26,566✔
237
            .get(StacksEpochId::Epoch30)
26,566✔
238
            .ok_or("No Epoch-3.0 defined")?;
26,566✔
239

240
        Ok(u64::from(burn_height) >= epoch_3.start_height - 1)
26,566✔
241
    }
26,566✔
242

243
    fn get_burn_height(config: &Config) -> u32 {
26,566✔
244
        let burnchain = config.get_burnchain();
26,566✔
245
        let sortdb_path = config.get_burn_db_file_path();
26,566✔
246
        if fs::metadata(&sortdb_path).is_err() {
26,566✔
247
            // if the sortition db doesn't exist yet, don't try to open() it, because that creates the
248
            // db file even if it doesn't instantiate the tables, which breaks connect() logic.
249
            info!("Failed to open Sortition DB while checking current burn height, assuming height = 0");
492✔
250
            return 0;
492✔
251
        }
26,074✔
252

253
        let Ok(sortdb) = SortitionDB::open(
26,074✔
254
            &sortdb_path,
26,074✔
255
            false,
26,074✔
256
            burnchain.pox_constants,
26,074✔
257
            Some(config.node.get_marf_opts()),
26,074✔
258
        ) else {
26,074✔
UNCOV
259
            info!("Failed to open Sortition DB while checking current burn height, assuming height = 0");
×
260
            return 0;
×
261
        };
262

263
        let Ok(tip_sn) = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()) else {
26,074✔
UNCOV
264
            info!("Failed to query Sortition DB for current burn height, assuming height = 0");
×
265
            return 0;
×
266
        };
267

268
        u32::try_from(tip_sn.block_height).expect("FATAL: burn height exceeded u32")
26,074✔
269
    }
26,566✔
270
}
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