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

stacks-network / stacks-core / 25394992339-1

05 May 2026 06:34PM UTC coverage: 85.691% (-0.02%) from 85.712%
25394992339-1

Pull #7183

github

6ed6d5
web-flow
Merge ffc2334e6 into 53ffba0ab
Pull Request #7183: Fix problematic transaction handling

115 of 135 new or added lines in 7 files covered. (85.19%)

4590 existing lines in 100 files now uncovered.

187724 of 219072 relevant lines covered (85.69%)

17710471.37 hits per line

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

93.18
/stacks-node/src/nakamoto_node.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::collections::HashSet;
17
use std::io::Write;
18
use std::sync::mpsc::Receiver;
19
use std::thread::JoinHandle;
20
use std::{fs, thread};
21

22
use stacks::burnchains::{BurnchainSigner, Txid};
23
use stacks::chainstate::burn::db::sortdb::SortitionDB;
24
use stacks::chainstate::burn::BlockSnapshot;
25
use stacks::chainstate::stacks::Error as ChainstateError;
26
use stacks::libstackerdb::StackerDBChunkAckData;
27
use stacks::monitoring;
28
use stacks::monitoring::update_active_miners_count_gauge;
29
use stacks::net::atlas::AtlasConfig;
30
use stacks::net::relay::Relayer;
31
use stacks::net::stackerdb::StackerDBs;
32
use stacks::net::Error as NetError;
33
use stacks::util_lib::db::Error as DBError;
34
use stacks_common::types::chainstate::SortitionId;
35
use stacks_common::types::StacksEpochId;
36

37
use super::{Config, EventDispatcher, Keychain};
38
use crate::burnchains::Error as BurnchainsError;
39
use crate::neon_node::{LeaderKeyRegistrationState, StacksNode as NeonNode};
40
use crate::run_loop::boot_nakamoto::Neon2NakaData;
41
use crate::run_loop::nakamoto::{Globals, RunLoop};
42
use crate::run_loop::RegisteredKey;
43

44
pub mod miner;
45
pub mod miner_db;
46
pub mod peer;
47
pub mod relayer;
48
pub mod signer_coordinator;
49
pub mod stackerdb_listener;
50

51
#[cfg(test)]
52
mod tests;
53

54
use self::peer::PeerThread;
55
use self::relayer::{RelayerDirective, RelayerThread};
56

57
pub const RELAYER_MAX_BUFFER: usize = 1;
58
const VRF_MOCK_MINER_KEY: u64 = 1;
59

60
pub const BLOCK_PROCESSOR_STACK_SIZE: usize = 32 * 1024 * 1024; // 32 MB
61

62
pub type BlockCommits = HashSet<Txid>;
63

64
/// Node implementation for both miners and followers.
65
/// This struct is used to set up the node proper and launch the p2p thread and relayer thread.
66
/// It is further used by the main thread to communicate with these two threads.
67
pub struct StacksNode {
68
    /// Atlas network configuration
69
    pub atlas_config: AtlasConfig,
70
    /// Global inter-thread communication handle
71
    pub globals: Globals,
72
    /// True if we're a miner
73
    is_miner: bool,
74
    /// handle to the p2p thread
75
    pub p2p_thread_handle: JoinHandle<()>,
76
    /// handle to the relayer thread
77
    pub relayer_thread_handle: JoinHandle<()>,
78
}
79

80
/// Types of errors that can arise during Nakamoto StacksNode operation
81
#[derive(thiserror::Error, Debug)]
82
pub enum Error {
83
    /// Can't find the block sortition snapshot for the chain tip
84
    #[error("Can't find the block sortition snapshot for the chain tip")]
85
    SnapshotNotFoundForChainTip,
86
    /// The burnchain tip changed while this operation was in progress
87
    #[error("The burnchain tip changed while this operation was in progress")]
88
    BurnchainTipChanged,
89
    /// The Stacks tip changed while this operation was in progress
90
    #[error("The Stacks tip changed while this operation was in progress")]
91
    StacksTipChanged,
92
    /// Signers rejected a block
93
    #[error("Signers rejected a block")]
94
    SignersRejected {
95
        /// Transaction IDs to exclude from the next block build (e.g., due to contextual rejections)
96
        temporarily_excluded_txids: HashSet<Txid>,
97
        /// Transaction IDs to permanently ban from the mempool
98
        permanently_excluded_txids: HashSet<Txid>,
99
    },
100
    /// Error while spawning a subordinate thread
101
    #[error("Error while spawning a subordinate thread: {0}")]
102
    SpawnError(std::io::Error),
103
    /// Injected testing errors
104
    #[error("Injected testing errors")]
105
    FaultInjection,
106
    /// This miner was elected, but another sortition occurred before mining started
107
    #[error("This miner was elected, but another sortition occurred before mining started")]
108
    MissedMiningOpportunity,
109
    /// Attempted to mine while there was no active VRF key
110
    #[error("Attempted to mine while there was no active VRF key")]
111
    NoVRFKeyActive,
112
    /// The parent block or tenure could not be found
113
    #[error("The parent block or tenure could not be found")]
114
    ParentNotFound,
115
    /// Something unexpected happened (e.g., hash mismatches)
116
    #[error("Something unexpected happened (e.g., hash mismatches)")]
117
    UnexpectedChainState,
118
    /// A burnchain operation failed when submitting it to the burnchain
119
    #[error("A burnchain operation failed when submitting it to the burnchain: {0}")]
120
    BurnchainSubmissionFailed(BurnchainsError),
121
    /// A new parent has been discovered since mining started
122
    #[error("A new parent has been discovered since mining started")]
123
    NewParentDiscovered,
124
    /// A failure occurred while constructing a VRF Proof
125
    #[error("A failure occurred while constructing a VRF Proof")]
126
    BadVrfConstruction,
127
    #[error("A failure occurred while mining: {0}")]
128
    MiningFailure(#[from] ChainstateError),
129
    /// The miner didn't accept their own block
130
    #[error("The miner didn't accept their own block: {0}")]
131
    AcceptFailure(ChainstateError),
132
    #[error("A failure occurred while signing a miner's block: {0}")]
133
    MinerSignatureError(&'static str),
134
    #[error("A failure occurred while signing a signer's block: {0}")]
135
    SignerSignatureError(String),
136
    /// A failure occurred while configuring the miner thread
137
    #[error("A failure occurred while configuring the miner thread: {0}")]
138
    MinerConfigurationFailed(&'static str),
139
    /// An error occurred while operating as the signing coordinator
140
    #[error("An error occurred while operating as the signing coordinator: {0}")]
141
    SigningCoordinatorFailure(String),
142
    /// An error occurred on StackerDB post
143
    #[error("An error occurred while uploading data to StackerDB: {0}")]
144
    StackerDBUploadError(StackerDBChunkAckData),
145
    // The thread that we tried to send to has closed
146
    #[error("The thread that we tried to send to has closed")]
147
    ChannelClosed,
148
    /// DBError wrapper
149
    #[error("DBError: {0}")]
150
    DBError(#[from] DBError),
151
    /// NetError wrapper
152
    #[error("NetError: {0}")]
153
    NetError(#[from] NetError),
154
    #[error("Timed out waiting for signatures")]
155
    SignatureTimeout,
156
}
157

158
impl StacksNode {
159
    /// This function sets the global var `GLOBAL_BURNCHAIN_SIGNER`.
160
    ///
161
    /// This variable is used for prometheus monitoring (which only
162
    /// runs when the feature flag `monitoring_prom` is activated).
163
    /// The address is set using the single-signature BTC address
164
    /// associated with `keychain`'s public key. This address always
165
    /// assumes Epoch-2.1 rules for the miner address: if the
166
    /// node is configured for segwit, then the miner address generated
167
    /// is a segwit address, otherwise it is a p2pkh.
168
    ///
169
    fn set_monitoring_miner_address(keychain: &Keychain, relayer_thread: &RelayerThread) {
245✔
170
        let public_key = keychain.get_pub_key();
245✔
171
        let miner_addr = relayer_thread
245✔
172
            .bitcoin_controller
245✔
173
            .get_miner_address(StacksEpochId::Epoch21, &public_key);
245✔
174
        let miner_addr_str = miner_addr.to_string();
245✔
175
        let _ = monitoring::set_burnchain_signer(BurnchainSigner(miner_addr_str)).map_err(|e| {
245✔
176
            warn!("Failed to set global burnchain signer: {e:?}");
245✔
177
            e
245✔
178
        });
245✔
179
    }
245✔
180

181
    pub fn spawn(
245✔
182
        runloop: &RunLoop,
245✔
183
        globals: Globals,
245✔
184
        // relay receiver endpoint for the p2p thread, so the relayer can feed it data to push
245✔
185
        relay_recv: Receiver<RelayerDirective>,
245✔
186
        data_from_neon: Option<Neon2NakaData>,
245✔
187
    ) -> StacksNode {
245✔
188
        let config = runloop.config().clone();
245✔
189
        let is_miner = runloop.is_miner();
245✔
190
        let burnchain = runloop.get_burnchain();
245✔
191
        let atlas_config = config.atlas.clone();
245✔
192
        let mut keychain = Keychain::default(config.node.seed.clone());
245✔
193
        if let Some(mining_key) = config.miner.mining_key.clone() {
245✔
194
            keychain.set_nakamoto_sk(mining_key);
245✔
195
        }
245✔
196

197
        let _ = config
245✔
198
            .connect_mempool_db()
245✔
199
            .expect("FATAL: database failure opening mempool");
245✔
200

201
        let data_from_neon = data_from_neon.unwrap_or_default();
245✔
202

203
        let mut p2p_net = data_from_neon
245✔
204
            .peer_network
245✔
205
            .unwrap_or_else(|| NeonNode::setup_peer_network(&config, &atlas_config, burnchain));
245✔
206

207
        let stackerdbs = StackerDBs::connect(&config.get_stacker_db_file_path(), true)
245✔
208
            .expect("FATAL: failed to connect to stacker DB");
245✔
209

210
        let relayer = Relayer::from_p2p(&mut p2p_net, stackerdbs);
245✔
211

212
        let local_peer = p2p_net.local_peer.clone();
245✔
213

214
        // setup initial key registration
215
        let leader_key_registration_state = if config.get_node_config(false).mock_mining {
245✔
216
            // mock mining, pretend to have a registered key
217
            let (vrf_public_key, _) = keychain.make_vrf_keypair(VRF_MOCK_MINER_KEY);
5✔
218
            LeaderKeyRegistrationState::Active(RegisteredKey {
5✔
219
                target_block_height: VRF_MOCK_MINER_KEY,
5✔
220
                block_height: 1,
5✔
221
                op_vtxindex: 1,
5✔
222
                vrf_public_key,
5✔
223
                memo: keychain.get_nakamoto_pkh().as_bytes().to_vec(),
5✔
224
            })
5✔
225
        } else {
226
            match &data_from_neon.leader_key_registration_state {
240✔
227
                LeaderKeyRegistrationState::Active(registered_key) => {
233✔
228
                    let pubkey_hash = keychain.get_nakamoto_pkh();
233✔
229
                    if pubkey_hash.as_ref() == registered_key.memo {
233✔
230
                        data_from_neon.leader_key_registration_state
197✔
231
                    } else {
232
                        LeaderKeyRegistrationState::Inactive
36✔
233
                    }
234
                }
235
                _ => LeaderKeyRegistrationState::Inactive,
7✔
236
            }
237
        };
238

239
        globals.set_initial_leader_key_registration_state(leader_key_registration_state);
245✔
240

241
        let relayer_thread =
245✔
242
            RelayerThread::new(runloop, local_peer.clone(), relayer, keychain.clone());
245✔
243

244
        StacksNode::set_monitoring_miner_address(&keychain, &relayer_thread);
245✔
245

246
        let relayer_thread_name = format!("relayer:{}", local_peer.port);
245✔
247
        let relayer_thread_handle = thread::Builder::new()
245✔
248
            .name(relayer_thread_name)
245✔
249
            .stack_size(BLOCK_PROCESSOR_STACK_SIZE)
245✔
250
            .spawn(move || {
245✔
251
                relayer_thread.main(relay_recv);
245✔
252
            })
245✔
253
            .expect("FATAL: failed to start relayer thread");
245✔
254

255
        let p2p_port = config
245✔
256
            .node
245✔
257
            .p2p_bind_addr()
245✔
258
            .unwrap_or_else(|| panic!("Failed to parse socket: {}", &config.node.p2p_bind))
245✔
259
            .port();
245✔
260
        let rpc_port = config
245✔
261
            .node
245✔
262
            .rpc_bind_addr()
245✔
263
            .unwrap_or_else(|| panic!("Failed to parse socket: {}", &config.node.rpc_bind))
245✔
264
            .port();
245✔
265

266
        let p2p_event_dispatcher = runloop.get_event_dispatcher();
245✔
267
        let p2p_thread = PeerThread::new(runloop, p2p_net);
245✔
268
        let p2p_thread_handle = thread::Builder::new()
245✔
269
            .stack_size(BLOCK_PROCESSOR_STACK_SIZE)
245✔
270
            .name(format!("p2p:({p2p_port},{rpc_port})"))
245✔
271
            .spawn(move || {
245✔
272
                p2p_thread.main(p2p_event_dispatcher);
245✔
273
            })
245✔
274
            .expect("FATAL: failed to start p2p thread");
245✔
275

276
        info!("Start HTTP server on: {}", &config.node.rpc_bind);
245✔
277
        info!("Start P2P server on: {}", &config.node.p2p_bind);
245✔
278

279
        StacksNode {
245✔
280
            atlas_config,
245✔
281
            globals,
245✔
282
            is_miner,
245✔
283
            p2p_thread_handle,
245✔
284
            relayer_thread_handle,
245✔
285
        }
245✔
286
    }
245✔
287

288
    /// Notify the relayer that a new burn block has been processed by the sortition db,
289
    ///  telling it to process the block and begin mining if this miner won.
290
    /// returns _false_ if the relayer hung up the channel.
291
    /// Called from the main thread.
292
    fn relayer_burnchain_notify(&self, snapshot: BlockSnapshot) -> Result<(), Error> {
4,517✔
293
        if !self.is_miner {
4,517✔
294
            // node is a follower, don't need to notify the relayer of these events.
295
            return Ok(());
95✔
296
        }
4,422✔
297

298
        info!(
4,422✔
299
            "Tenure: Notify burn block!";
300
            "consensus_hash" => %snapshot.consensus_hash,
301
            "burn_block_hash" => %snapshot.burn_header_hash,
302
            "winning_stacks_block_hash" => %snapshot.winning_stacks_block_hash,
303
            "burn_block_height" => &snapshot.block_height,
4,422✔
304
            "sortition_id" => %snapshot.sortition_id
305
        );
306

307
        // unlike in neon_node, the nakamoto node should *always* notify the relayer of
308
        //  a new burnchain block
309

310
        self.globals
4,422✔
311
            .relay_send
4,422✔
312
            .send(RelayerDirective::ProcessedBurnBlock(
4,422✔
313
                snapshot.consensus_hash,
4,422✔
314
                snapshot.parent_burn_header_hash,
4,422✔
315
                snapshot.winning_stacks_block_hash,
4,422✔
316
            ))
4,422✔
317
            .map_err(|_| Error::ChannelClosed)?;
4,422✔
318

319
        Ok(())
4,422✔
320
    }
4,517✔
321

322
    /// Process a state coming from the burnchain, by extracting the validated KeyRegisterOp
323
    /// and inspecting if a sortition was won.
324
    /// `ibd`: boolean indicating whether or not we are in the initial block download
325
    /// Called from the main thread.
326
    pub fn process_burnchain_state(
4,517✔
327
        &mut self,
4,517✔
328
        config: &Config,
4,517✔
329
        sortdb: &SortitionDB,
4,517✔
330
        sort_id: &SortitionId,
4,517✔
331
        ibd: bool,
4,517✔
332
    ) -> Result<(), Error> {
4,517✔
333
        let ic = sortdb.index_conn();
4,517✔
334

335
        let block_snapshot = SortitionDB::get_block_snapshot(&ic, sort_id)
4,517✔
336
            .expect("Failed to obtain block snapshot for processed burn block.")
4,517✔
337
            .expect("Failed to obtain block snapshot for processed burn block.");
4,517✔
338
        let block_height = block_snapshot.block_height;
4,517✔
339

340
        let block_commits =
4,517✔
341
            SortitionDB::get_block_commits_by_block(&ic, &block_snapshot.sortition_id)
4,517✔
342
                .expect("Unexpected SortitionDB error fetching block commits");
4,517✔
343

344
        let num_block_commits = block_commits.len();
4,517✔
345

346
        update_active_miners_count_gauge(block_commits.len() as i64);
4,517✔
347

348
        for op in block_commits.into_iter() {
4,611✔
349
            if op.txid == block_snapshot.winning_block_txid {
4,464✔
350
                info!(
4,364✔
351
                    "Received burnchain block #{block_height} including block_commit_op (winning) - {} ({})",
352
                    op.apparent_sender, &op.block_header_hash
4,364✔
353
                );
354
            } else if self.is_miner {
100✔
355
                info!(
100✔
356
                    "Received burnchain block #{block_height} including block_commit_op - {} ({})",
357
                    op.apparent_sender, &op.block_header_hash
100✔
358
                );
UNCOV
359
            }
×
360
        }
361

362
        let key_registers =
4,517✔
363
            SortitionDB::get_leader_keys_by_block(&ic, &block_snapshot.sortition_id)
4,517✔
364
                .expect("Unexpected SortitionDB error fetching key registers");
4,517✔
365

366
        let num_key_registers = key_registers.len();
4,517✔
367

368
        let activated_key_opt = self
4,517✔
369
            .globals
4,517✔
370
            .try_activate_leader_key_registration(block_height, key_registers);
4,517✔
371

372
        // save the registered VRF key
373
        if let (Some(activated_key), Some(path)) = (
38✔
374
            activated_key_opt,
4,517✔
375
            config.miner.activated_vrf_key_path.as_ref(),
4,517✔
376
        ) {
38✔
377
            save_activated_vrf_key(path, &activated_key);
38✔
378
        }
4,479✔
379

380
        debug!(
4,517✔
381
            "Processed burnchain state";
UNCOV
382
            "burn_height" => block_height,
×
UNCOV
383
            "leader_keys_count" => num_key_registers,
×
UNCOV
384
            "block_commits_count" => num_block_commits,
×
UNCOV
385
            "in_initial_block_download?" => ibd,
×
386
        );
387

388
        self.globals.set_last_sortition(block_snapshot.clone());
4,517✔
389

390
        // notify the relayer thread of the new sortition state
391
        self.relayer_burnchain_notify(block_snapshot)
4,517✔
392
    }
4,517✔
393

394
    /// Join all inner threads
395
    pub fn join(self) {
225✔
396
        self.relayer_thread_handle.join().unwrap();
225✔
397
        self.p2p_thread_handle.join().unwrap();
225✔
398
    }
225✔
399
}
400

401
pub(crate) fn save_activated_vrf_key(path: &str, activated_key: &RegisteredKey) {
40✔
402
    info!("Activated VRF key; saving to {path}");
40✔
403

404
    let Ok(key_json) = serde_json::to_string(&activated_key) else {
40✔
UNCOV
405
        warn!("Failed to serialize VRF key");
×
406
        return;
×
407
    };
408

409
    let mut f = match fs::File::create(path) {
40✔
410
        Ok(f) => f,
40✔
UNCOV
411
        Err(e) => {
×
UNCOV
412
            warn!("Failed to create {path}: {e:?}");
×
413
            return;
×
414
        }
415
    };
416

417
    if let Err(e) = f.write_all(key_json.as_bytes()) {
40✔
UNCOV
418
        warn!("Failed to write activated VRF key to {path}: {e:?}");
×
UNCOV
419
        return;
×
420
    }
40✔
421

422
    info!("Saved activated VRF key to {path}");
40✔
423
}
40✔
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