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

stacks-network / stacks-core / 25404138305-1

05 May 2026 09:47PM UTC coverage: 85.69% (-0.02%) from 85.712%
25404138305-1

Pull #7169

github

497ffd
web-flow
Merge 35db1183d into 53ffba0ab
Pull Request #7169: Feat: add defensive memory allocation for miners/signers

134 of 139 new or added lines in 11 files covered. (96.4%)

4591 existing lines in 96 files now uncovered.

187733 of 219085 relevant lines covered (85.69%)

18687545.45 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 {
241✔
46
        let key_state = globals
241✔
47
            .leader_key_registration_state
241✔
48
            .lock()
241✔
49
            .unwrap_or_else(|e| {
241✔
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 {
241✔
56
            leader_key_registration_state: (*key_state).clone(),
241✔
57
            peer_network,
241✔
58
        }
241✔
59
    }
241✔
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> {
246✔
79
        let (coordinator_channels, active_loop) = if !Self::reached_epoch_30_transition(&config)? {
246✔
80
            let neon = NeonRunLoop::new(config.clone());
244✔
81
            (
244✔
82
                neon.get_coordinator_channel().unwrap(),
244✔
83
                InnerLoops::Epoch2(neon),
244✔
84
            )
244✔
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 {
246✔
94
            config,
246✔
95
            active_loop,
246✔
96
            coordinator_channels: Arc::new(Mutex::new(coordinator_channels)),
246✔
97
        })
246✔
98
    }
246✔
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>> {
245✔
106
        self.coordinator_channels.clone()
245✔
107
    }
245✔
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 {
297✔
113
        match &self.active_loop {
297✔
114
            InnerLoops::Epoch2(x) => x.get_counters(),
295✔
115
            InnerLoops::Epoch3(x) => x.get_counters(),
2✔
116
        }
117
    }
297✔
118

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

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

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

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

203
    fn spawn_stopper(
244✔
204
        config: &Config,
244✔
205
        neon: &NeonRunLoop,
244✔
206
    ) -> Result<JoinHandle<bool>, std::io::Error> {
244✔
207
        let neon_term_switch = neon.get_termination_switch();
244✔
208
        let config = config.clone();
244✔
209
        thread::Builder::new()
244✔
210
            .name("epoch-2/3-boot".into())
244✔
211
            .spawn(move || {
244✔
212
                loop {
213
                    let do_transition = Self::reached_epoch_30_transition(&config)
25,223✔
214
                        .unwrap_or_else(|err| {
25,223✔
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,223✔
219
                        break;
239✔
220
                    }
24,984✔
221
                    if !neon_term_switch.load(Ordering::SeqCst) {
24,984✔
222
                        info!("Stop requested, exiting epoch-2/3-boot thread");
5✔
223
                        return false;
5✔
224
                    }
24,979✔
225
                    thread::sleep(Duration::from_secs(1));
24,979✔
226
                }
227
                // if loop exited, do the transition
228
                info!("Epoch-3.0 boundary reached, stopping Epoch-2.x run loop");
239✔
229
                neon_term_switch.store(false, Ordering::SeqCst);
239✔
230
                true
239✔
231
            })
244✔
232
    }
244✔
233

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

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

244
    fn get_burn_height(config: &Config) -> u32 {
25,469✔
245
        let burnchain = config.get_burnchain();
25,469✔
246
        let sortdb_path = config.get_burn_db_file_path();
25,469✔
247
        if fs::metadata(&sortdb_path).is_err() {
25,469✔
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");
487✔
251
            return 0;
487✔
252
        }
24,982✔
253

254
        let Ok(sortdb) = SortitionDB::open(
24,982✔
255
            &sortdb_path,
24,982✔
256
            false,
24,982✔
257
            burnchain.pox_constants,
24,982✔
258
            Some(config.node.get_marf_opts()),
24,982✔
259
        ) else {
24,982✔
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 {
24,982✔
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")
24,982✔
270
    }
25,469✔
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