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

stacks-network / stacks-core / 23943270448

03 Apr 2026 10:32AM UTC coverage: 77.559% (-8.2%) from 85.712%
23943270448

Pull #7077

github

52f01d
web-flow
Merge fa3f939ed into c529ad924
Pull Request #7077: feat: add burnchain DB copy and validation

3654 of 4220 new or added lines in 18 files covered. (86.59%)

19324 existing lines in 182 files now uncovered.

171991 of 221755 relevant lines covered (77.56%)

7658447.9 hits per line

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

0.0
/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
UNCOV
45
    pub fn new(globals: NeonGlobals, peer_network: Option<PeerNetwork>) -> Self {
×
UNCOV
46
        let key_state = globals
×
UNCOV
47
            .leader_key_registration_state
×
UNCOV
48
            .lock()
×
UNCOV
49
            .unwrap_or_else(|e| {
×
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

UNCOV
55
        Self {
×
UNCOV
56
            leader_key_registration_state: (*key_state).clone(),
×
UNCOV
57
            peer_network,
×
UNCOV
58
        }
×
UNCOV
59
    }
×
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 {
UNCOV
78
    pub fn new(config: Config) -> Result<Self, String> {
×
UNCOV
79
        let (coordinator_channels, active_loop) = if !Self::reached_epoch_30_transition(&config)? {
×
UNCOV
80
            let neon = NeonRunLoop::new(config.clone());
×
UNCOV
81
            (
×
UNCOV
82
                neon.get_coordinator_channel().unwrap(),
×
UNCOV
83
                InnerLoops::Epoch2(neon),
×
UNCOV
84
            )
×
85
        } else {
UNCOV
86
            let naka = NakaRunLoop::new(config.clone(), None, None, None);
×
UNCOV
87
            (
×
UNCOV
88
                naka.get_coordinator_channel().unwrap(),
×
UNCOV
89
                InnerLoops::Epoch3(naka),
×
UNCOV
90
            )
×
91
        };
92

UNCOV
93
        Ok(BootRunLoop {
×
UNCOV
94
            config,
×
UNCOV
95
            active_loop,
×
UNCOV
96
            coordinator_channels: Arc::new(Mutex::new(coordinator_channels)),
×
UNCOV
97
        })
×
UNCOV
98
    }
×
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.
UNCOV
105
    pub fn coordinator_channels(&self) -> Arc<Mutex<CoordinatorChannels>> {
×
UNCOV
106
        self.coordinator_channels.clone()
×
UNCOV
107
    }
×
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.
UNCOV
112
    pub fn counters(&self) -> Counters {
×
UNCOV
113
        match &self.active_loop {
×
UNCOV
114
            InnerLoops::Epoch2(x) => x.get_counters(),
×
UNCOV
115
            InnerLoops::Epoch3(x) => x.get_counters(),
×
116
        }
UNCOV
117
    }
×
118

119
    /// Get the termination switch from the active run loop.
UNCOV
120
    pub fn get_termination_switch(&self) -> Arc<AtomicBool> {
×
UNCOV
121
        match &self.active_loop {
×
UNCOV
122
            InnerLoops::Epoch2(x) => x.get_termination_switch(),
×
UNCOV
123
            InnerLoops::Epoch3(x) => x.get_termination_switch(),
×
124
        }
UNCOV
125
    }
×
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.
UNCOV
137
    pub fn start(&mut self, burnchain_opt: Option<Burnchain>, mine_start: u64) {
×
UNCOV
138
        match self.active_loop {
×
UNCOV
139
            InnerLoops::Epoch2(_) => self.start_from_neon(burnchain_opt, mine_start),
×
UNCOV
140
            InnerLoops::Epoch3(_) => self.start_from_naka(burnchain_opt, mine_start),
×
141
        }
UNCOV
142
    }
×
143

UNCOV
144
    fn start_from_naka(&mut self, burnchain_opt: Option<Burnchain>, mine_start: u64) {
×
UNCOV
145
        let InnerLoops::Epoch3(ref mut naka_loop) = self.active_loop else {
×
146
            panic!("FATAL: unexpectedly invoked start_from_naka when active loop wasn't nakamoto");
×
147
        };
UNCOV
148
        naka_loop.start(burnchain_opt, mine_start, None)
×
UNCOV
149
    }
×
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)]
UNCOV
155
    fn start_from_neon(&mut self, burnchain_opt: Option<Burnchain>, mine_start: u64) {
×
UNCOV
156
        let InnerLoops::Epoch2(ref mut neon_loop) = self.active_loop else {
×
157
            panic!("FATAL: unexpectedly invoked start_from_neon when active loop wasn't neon");
×
158
        };
UNCOV
159
        let termination_switch = neon_loop.get_termination_switch();
×
UNCOV
160
        let counters = neon_loop.get_counters();
×
161

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

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

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

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

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

UNCOV
240
        Ok(u64::from(burn_height) >= epoch_3.start_height - 1)
×
UNCOV
241
    }
×
242

UNCOV
243
    fn get_burn_height(config: &Config) -> u32 {
×
UNCOV
244
        let burnchain = config.get_burnchain();
×
UNCOV
245
        let sortdb_path = config.get_burn_db_file_path();
×
UNCOV
246
        if fs::metadata(&sortdb_path).is_err() {
×
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.
UNCOV
249
            info!("Failed to open Sortition DB while checking current burn height, assuming height = 0");
×
UNCOV
250
            return 0;
×
UNCOV
251
        }
×
252

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

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

UNCOV
268
        u32::try_from(tip_sn.block_height).expect("FATAL: burn height exceeded u32")
×
UNCOV
269
    }
×
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