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

stacks-network / stacks-core / 25801484257-1

13 May 2026 01:15PM UTC coverage: 85.648% (-0.06%) from 85.712%
25801484257-1

Pull #7183

github

2d7e6d
web-flow
Merge 420cb597a into 31276d071
Pull Request #7183: Fix problematic transaction handling

110 of 130 new or added lines in 7 files covered. (84.62%)

5464 existing lines in 98 files now uncovered.

188263 of 219809 relevant lines covered (85.65%)

18940648.33 hits per line

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

88.74
/stacks-node/src/run_loop/boot_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, 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 {
245✔
46
        let key_state = globals
245✔
47
            .leader_key_registration_state
245✔
48
            .lock()
245✔
49
            .unwrap_or_else(|e| {
245✔
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 {
245✔
56
            leader_key_registration_state: (*key_state).clone(),
245✔
57
            peer_network,
245✔
58
        }
245✔
59
    }
245✔
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> {
250✔
79
        let (coordinator_channels, active_loop) = if !Self::reached_epoch_30_transition(&config)? {
250✔
80
            let neon = NeonRunLoop::new(config.clone());
248✔
81
            (
248✔
82
                neon.get_coordinator_channel().unwrap(),
248✔
83
                InnerLoops::Epoch2(neon),
248✔
84
            )
248✔
85
        } else {
86
            let naka = NakaRunLoop::new(config.clone(), None, 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 {
250✔
94
            config,
250✔
95
            active_loop,
250✔
96
            coordinator_channels: Arc::new(Mutex::new(coordinator_channels)),
250✔
97
        })
250✔
98
    }
250✔
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>> {
249✔
106
        self.coordinator_channels.clone()
249✔
107
    }
249✔
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 {
301✔
113
        match &self.active_loop {
301✔
114
            InnerLoops::Epoch2(x) => x.get_counters(),
299✔
115
            InnerLoops::Epoch3(x) => x.get_counters(),
2✔
116
        }
117
    }
301✔
118

119
    /// Get the termination switch from the active run loop.
120
    pub fn get_termination_switch(&self) -> Arc<AtomicBool> {
250✔
121
        match &self.active_loop {
250✔
122
            InnerLoops::Epoch2(x) => x.get_termination_switch(),
248✔
123
            InnerLoops::Epoch3(x) => x.get_termination_switch(),
2✔
124
        }
125
    }
250✔
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) {
250✔
138
        match self.active_loop {
250✔
139
            InnerLoops::Epoch2(_) => self.start_from_neon(burnchain_opt, mine_start),
248✔
140
            InnerLoops::Epoch3(_) => self.start_from_naka(burnchain_opt, mine_start),
2✔
141
        }
142
    }
250✔
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) {
248✔
156
        let InnerLoops::Epoch2(ref mut neon_loop) = self.active_loop else {
248✔
157
            panic!("FATAL: unexpectedly invoked start_from_neon when active loop wasn't neon");
×
158
        };
159
        let termination_switch = neon_loop.get_termination_switch();
248✔
160
        let counters = neon_loop.get_counters();
248✔
161

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

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

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

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

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

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

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

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

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

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