• 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

0.0
/stacks-node/src/main.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

17
#[macro_use]
18
extern crate serde_derive;
19
#[macro_use]
20
extern crate stacks_common;
21

22
extern crate clarity;
23
extern crate stacks;
24

25
#[allow(unused_imports)]
26
#[macro_use(o, slog_log, slog_trace, slog_debug, slog_info, slog_warn, slog_error)]
27
extern crate slog;
28

29
pub use stacks_common::util;
30
use stacks_common::util::hash::hex_bytes;
31

32
pub mod monitoring;
33

34
pub mod burnchains;
35
pub mod event_dispatcher;
36
pub mod genesis_data;
37
pub mod globals;
38
pub mod keychain;
39
pub mod nakamoto_node;
40
pub mod neon_node;
41
pub mod node;
42
pub mod operations;
43
pub mod run_loop;
44
pub mod syncctl;
45
pub mod tenure;
46

47
use std::collections::HashMap;
48
use std::{env, panic, process};
49

50
use backtrace::Backtrace;
51
use pico_args::Arguments;
52
use stacks::chainstate::burn::db::sortdb::SortitionDB;
53
use stacks::chainstate::burn::operations::leader_block_commit::RewardSetInfo;
54
use stacks::chainstate::coordinator::{get_next_recipients, OnChainRewardSetProvider};
55
use stacks::chainstate::stacks::address::PoxAddress;
56
use stacks::chainstate::stacks::db::blocks::DummyEventDispatcher;
57
use stacks::chainstate::stacks::db::StacksChainState;
58
use stacks::config::chain_data::MinerStats;
59
pub use stacks::config::{Config, ConfigFile};
60
#[cfg(not(any(target_os = "macos", target_os = "windows", target_arch = "arm")))]
61
use tikv_jemallocator::Jemalloc;
62

63
pub use self::burnchains::{
64
    BitcoinRegtestController, BurnchainController, BurnchainTip, MocknetController,
65
};
66
pub use self::event_dispatcher::EventDispatcher;
67
pub use self::keychain::Keychain;
68
pub use self::node::{ChainTip, Node};
69
pub use self::run_loop::{helium, neon};
70
pub use self::tenure::Tenure;
71
use crate::neon_node::{BlockMinerThread, TipCandidate};
72
use crate::run_loop::boot_nakamoto;
73

74
#[cfg(not(any(target_os = "macos", target_os = "windows", target_arch = "arm")))]
75
#[global_allocator]
76
static GLOBAL: Jemalloc = Jemalloc;
77

78
/// Implmentation of `pick_best_tip` CLI option
79
fn cli_pick_best_tip(config_path: &str, at_stacks_height: Option<u64>) -> TipCandidate {
×
80
    info!("Loading config at path {config_path}");
×
81
    let config = match ConfigFile::from_path(config_path) {
×
82
        Ok(config_file) => Config::from_config_file(config_file, true).unwrap(),
×
83
        Err(e) => {
×
UNCOV
84
            warn!("Invalid config file: {e}");
×
85
            process::exit(1);
×
86
        }
87
    };
88
    let burn_db_path = config.get_burn_db_file_path();
×
UNCOV
89
    let stacks_chainstate_path = config.get_chainstate_path_str();
×
90
    let burnchain = config.get_burnchain();
×
UNCOV
91
    let (mut chainstate, _) = StacksChainState::open(
×
UNCOV
92
        config.is_mainnet(),
×
93
        config.burnchain.chain_id,
×
94
        &stacks_chainstate_path,
×
95
        Some(config.node.get_marf_opts()),
×
96
    )
×
97
    .unwrap();
×
UNCOV
98
    let mut sortdb = SortitionDB::open(
×
UNCOV
99
        &burn_db_path,
×
100
        false,
101
        burnchain.pox_constants,
×
UNCOV
102
        Some(config.node.get_marf_opts()),
×
103
    )
UNCOV
104
    .unwrap();
×
105

106
    let max_depth = config.miner.max_reorg_depth;
×
107

108
    // There could be more than one possible chain tip. Go find them.
109
    let stacks_tips = BlockMinerThread::load_candidate_tips(
×
110
        &mut sortdb,
×
111
        &mut chainstate,
×
112
        max_depth,
×
113
        at_stacks_height,
×
114
    );
115

UNCOV
116
    BlockMinerThread::inner_pick_best_tip(stacks_tips, HashMap::new()).unwrap()
×
UNCOV
117
}
×
118

119
/// Implementation of `get_miner_spend` CLI option
120
#[allow(clippy::incompatible_msrv)]
121
fn cli_get_miner_spend(
×
122
    config_path: &str,
×
123
    mine_start: Option<u64>,
×
124
    at_burnchain_height: Option<u64>,
×
125
) -> u64 {
×
126
    info!("Loading config at path {config_path}");
×
127
    let config = match ConfigFile::from_path(config_path) {
×
128
        Ok(config_file) => Config::from_config_file(config_file, true).unwrap(),
×
129
        Err(e) => {
×
130
            warn!("Invalid config file: {e}");
×
UNCOV
131
            process::exit(1);
×
132
        }
133
    };
UNCOV
134
    let keychain = Keychain::default(config.node.seed.clone());
×
135
    let burn_db_path = config.get_burn_db_file_path();
×
136
    let stacks_chainstate_path = config.get_chainstate_path_str();
×
137
    let burnchain = config.get_burnchain();
×
138
    let (mut chainstate, _) = StacksChainState::open(
×
139
        config.is_mainnet(),
×
140
        config.burnchain.chain_id,
×
141
        &stacks_chainstate_path,
×
UNCOV
142
        Some(config.node.get_marf_opts()),
×
143
    )
×
UNCOV
144
    .unwrap();
×
UNCOV
145
    let mut sortdb = SortitionDB::open(
×
146
        &burn_db_path,
×
147
        true,
148
        burnchain.pox_constants.clone(),
×
149
        Some(config.node.get_marf_opts()),
×
150
    )
151
    .unwrap();
×
152
    let tip = if let Some(at_burnchain_height) = at_burnchain_height {
×
UNCOV
153
        let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
×
154
        let ih = sortdb.index_handle(&tip.sortition_id);
×
UNCOV
155
        ih.get_block_snapshot_by_height(at_burnchain_height)
×
156
            .unwrap()
×
157
            .unwrap()
×
158
    } else {
159
        SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap()
×
160
    };
161

162
    let no_dispatcher: Option<&DummyEventDispatcher> = None;
×
163
    let recipients = get_next_recipients(
×
164
        &tip,
×
165
        &mut chainstate,
×
166
        &mut sortdb,
×
167
        &burnchain,
×
168
        &OnChainRewardSetProvider(no_dispatcher),
×
169
    )
170
    .unwrap();
×
171

172
    let commit_outs = if !burnchain.is_in_prepare_phase(tip.block_height + 1) {
×
UNCOV
173
        RewardSetInfo::into_commit_outs(recipients, config.is_mainnet())
×
174
    } else {
175
        vec![PoxAddress::standard_burn_address(config.is_mainnet())]
×
176
    };
177

178
    let spend_amount = BlockMinerThread::get_mining_spend_amount(
×
179
        &config,
×
UNCOV
180
        &keychain,
×
181
        &burnchain,
×
182
        &sortdb,
×
183
        &commit_outs,
×
UNCOV
184
        mine_start.unwrap_or(tip.block_height),
×
185
        at_burnchain_height,
×
UNCOV
186
        |burn_block_height| {
×
187
            let sortdb = SortitionDB::open(
×
188
                &burn_db_path,
×
189
                true,
190
                burnchain.pox_constants.clone(),
×
UNCOV
191
                Some(config.node.get_marf_opts()),
×
192
            )
193
            .unwrap();
×
194
            let Some(miner_stats) = config.get_miner_stats() else {
×
195
                return 0.0;
×
196
            };
197
            let Ok(active_miners_and_commits) =
×
UNCOV
198
                MinerStats::get_active_miners(&sortdb, Some(burn_block_height))
×
199
                    .inspect_err(|e| warn!("Failed to get active miners: {e:?}"))
×
200
            else {
201
                return 0.0;
×
202
            };
203
            if active_miners_and_commits.is_empty() {
×
UNCOV
204
                warn!("No active miners detected; using config file burn_fee_cap");
×
UNCOV
205
                return 0.0;
×
206
            }
×
207

208
            let active_miners: Vec<_> = active_miners_and_commits
×
209
                .iter()
×
UNCOV
210
                .map(|(miner, _cmt)| miner.as_str())
×
211
                .collect();
×
212

213
            info!("Active miners: {active_miners:?}");
×
214

215
            let Ok(unconfirmed_block_commits) = miner_stats
×
216
                .get_unconfirmed_commits(burn_block_height + 1, &active_miners)
×
217
                .inspect_err(|e| warn!("Failed to find unconfirmed block-commits: {e}"))
×
218
            else {
UNCOV
219
                return 0.0;
×
220
            };
221

222
            let unconfirmed_miners_and_amounts: Vec<(String, u64)> = unconfirmed_block_commits
×
223
                .iter()
×
UNCOV
224
                .map(|cmt| (format!("{}", &cmt.apparent_sender), cmt.burn_fee))
×
UNCOV
225
                .collect();
×
226

227
            info!("Found unconfirmed block-commits: {unconfirmed_miners_and_amounts:?}");
×
228

229
            let (spend_dist, _total_spend) = MinerStats::get_spend_distribution(
×
230
                &active_miners_and_commits,
×
231
                &unconfirmed_block_commits,
×
232
                &commit_outs,
×
233
            );
×
234
            let win_probs = if config.miner.fast_rampup {
×
235
                // look at spends 6+ blocks in the future
236
                MinerStats::get_future_win_distribution(
×
UNCOV
237
                    &active_miners_and_commits,
×
238
                    &unconfirmed_block_commits,
×
UNCOV
239
                    &commit_outs,
×
240
                )
241
            } else {
242
                // look at the current spends
UNCOV
243
                let Ok(unconfirmed_burn_dist) = miner_stats
×
244
                    .get_unconfirmed_burn_distribution(
×
245
                        &burnchain,
×
UNCOV
246
                        &sortdb,
×
UNCOV
247
                        &active_miners_and_commits,
×
UNCOV
248
                        unconfirmed_block_commits,
×
UNCOV
249
                        &commit_outs,
×
250
                        at_burnchain_height,
×
251
                    )
252
                    .inspect_err(|e| warn!("Failed to get unconfirmed burn distribution: {e:?}"))
×
253
                else {
254
                    return 0.0;
×
255
                };
256

257
                MinerStats::burn_dist_to_prob_dist(&unconfirmed_burn_dist)
×
258
            };
259

UNCOV
260
            info!("Unconfirmed spend distribution: {spend_dist:?}");
×
261
            info!(
×
262
                "Unconfirmed win probabilities (fast_rampup={}): {win_probs:?}",
263
                config.miner.fast_rampup
264
            );
265

266
            let miner_addrs = BlockMinerThread::get_miner_addrs(&config, &keychain);
×
UNCOV
267
            let win_prob = miner_addrs
×
268
                .iter()
×
269
                .find_map(|x| win_probs.get(x))
×
270
                .copied()
×
271
                .unwrap_or(0.0);
×
272

UNCOV
273
            info!(
×
274
                "This miner's win probability at {} is {win_prob}",
275
                tip.block_height
276
            );
277
            win_prob
×
278
        },
×
UNCOV
279
        |_burn_block_height, _win_prob| {},
×
280
    );
UNCOV
281
    spend_amount
×
UNCOV
282
}
×
283

284
/// If the previous session was terminated before all the pending events had been sent,
285
/// the DB will still contain them. Work through that before doing anything new.
286
/// Pending events for observers that are no longer registered will be discarded.
287
fn send_pending_event_payloads(conf: &Config) {
×
288
    // This dispatcher gets a queue size of 0 to ensure that it blocks. Technically
289
    // process_pending_payloads() always blocks; this is just an additional safeguard.
290
    let mut event_dispatcher =
×
291
        EventDispatcher::new_with_custom_queue_size(conf.get_working_dir(), 0);
×
UNCOV
292
    for observer in &conf.events_observers {
×
293
        event_dispatcher.register_observer(observer);
×
UNCOV
294
    }
×
295
    event_dispatcher.process_pending_payloads();
×
296
}
×
297

UNCOV
298
fn main() {
×
299
    panic::set_hook(Box::new(|panic_info| {
×
300
        error!("Process abort due to thread panic: {panic_info}");
×
301
        let bt = Backtrace::new();
×
UNCOV
302
        error!("Panic backtrace: {bt:?}");
×
303

304
        // force a core dump
305
        #[cfg(unix)]
306
        {
UNCOV
307
            let pid = process::id();
×
308
            eprintln!("Dumping core for pid {}", std::process::id());
×
309

310
            use libc::{kill, SIGQUIT};
311

312
            // *should* trigger a core dump, if you run `ulimit -c unlimited` first!
313
            unsafe { kill(pid.try_into().unwrap(), SIGQUIT) };
×
314
        }
315

316
        // just in case
317
        process::exit(1);
×
318
    }));
319

320
    let mut args = Arguments::from_env();
×
321
    let subcommand = args.subcommand().unwrap().unwrap_or_default();
×
322

323
    info!("{}", version());
×
324

325
    let mine_start: Option<u64> = args
×
326
        .opt_value_from_str("--mine-at-height")
×
327
        .expect("Failed to parse --mine-at-height argument");
×
328

329
    if let Some(mine_start) = mine_start {
×
330
        info!("Will begin mining once Stacks chain has synced to height >= {mine_start}");
×
331
    }
×
332

UNCOV
333
    let config_file = match subcommand.as_str() {
×
334
        "mocknet" => {
×
UNCOV
335
            args.finish();
×
336
            ConfigFile::mocknet()
×
337
        }
UNCOV
338
        "helium" => {
×
339
            args.finish();
×
340
            ConfigFile::helium()
×
341
        }
UNCOV
342
        "testnet" => {
×
UNCOV
343
            args.finish();
×
UNCOV
344
            ConfigFile::xenon()
×
345
        }
346
        "mainnet" => {
×
347
            args.finish();
×
348
            ConfigFile::mainnet()
×
349
        }
350
        "check-config" => {
×
351
            let config_path: String = args.value_from_str("--config").unwrap();
×
352
            args.finish();
×
353
            info!("Loading config at path {config_path}");
×
UNCOV
354
            let config_file = match ConfigFile::from_path(&config_path) {
×
UNCOV
355
                Ok(config_file) => {
×
UNCOV
356
                    debug!("Loaded config file: {config_file:?}");
×
357
                    config_file
×
358
                }
359
                Err(e) => {
×
UNCOV
360
                    warn!("Invalid config file: {e}");
×
361
                    process::exit(1);
×
362
                }
363
            };
364
            match Config::from_config_file(config_file, true) {
×
365
                Ok(_) => {
366
                    info!("Loaded config!");
×
UNCOV
367
                    process::exit(0);
×
368
                }
369
                Err(e) => {
×
370
                    warn!("Invalid config: {e}");
×
371
                    process::exit(1);
×
372
                }
373
            };
374
        }
375
        "start" => {
×
376
            let config_path: String = args.value_from_str("--config").unwrap();
×
377
            args.finish();
×
378
            info!("Loading config at path {config_path}");
×
UNCOV
379
            match ConfigFile::from_path(&config_path) {
×
UNCOV
380
                Ok(config_file) => config_file,
×
381
                Err(e) => {
×
382
                    warn!("Invalid config file: {e}");
×
UNCOV
383
                    process::exit(1);
×
384
                }
385
            }
386
        }
UNCOV
387
        "version" => {
×
388
            println!("{}", &version());
×
UNCOV
389
            return;
×
390
        }
UNCOV
391
        "key-for-seed" => {
×
392
            let seed = {
×
393
                let config_path: Option<String> = args.opt_value_from_str("--config").unwrap();
×
394
                if let Some(config_path) = config_path {
×
395
                    let conf = Config::from_config_file(
×
396
                        ConfigFile::from_path(&config_path).unwrap(),
×
397
                        true,
398
                    )
399
                    .unwrap();
×
400
                    args.finish();
×
UNCOV
401
                    conf.node.seed
×
402
                } else {
403
                    let free_args = args.finish();
×
404
                    let seed_hex = free_args
×
405
                        .first()
×
406
                        .expect("`wif-for-seed` must be passed either a config file via the `--config` flag or a hex seed string");
×
UNCOV
407
                    hex_bytes(seed_hex.to_str().unwrap())
×
408
                        .expect("Seed should be a hex encoded string")
×
409
                }
410
            };
UNCOV
411
            let keychain = Keychain::default(seed);
×
UNCOV
412
            println!(
×
413
                "Hex formatted secret key: {}",
414
                keychain.generate_op_signer().get_secret_key_as_hex()
×
415
            );
UNCOV
416
            println!(
×
417
                "WIF formatted secret key: {}",
418
                keychain.generate_op_signer().get_secret_key_as_wif()
×
419
            );
420
            return;
×
421
        }
422
        "pick-best-tip" => {
×
UNCOV
423
            let config_path: String = args.value_from_str("--config").unwrap();
×
UNCOV
424
            let at_stacks_height: Option<u64> =
×
UNCOV
425
                args.opt_value_from_str("--at-stacks-height").unwrap();
×
426
            args.finish();
×
427

428
            let best_tip = cli_pick_best_tip(&config_path, at_stacks_height);
×
UNCOV
429
            println!("Best tip is {best_tip:?}");
×
430
            process::exit(0);
×
431
        }
432
        "get-spend-amount" => {
×
433
            let config_path: String = args.value_from_str("--config").unwrap();
×
434
            let at_burnchain_height: Option<u64> =
×
435
                args.opt_value_from_str("--at-bitcoin-height").unwrap();
×
436
            args.finish();
×
437

438
            let spend_amount = cli_get_miner_spend(&config_path, mine_start, at_burnchain_height);
×
439
            println!("Will spend {spend_amount}");
×
440
            process::exit(0);
×
441
        }
442
        _ => {
443
            print_help();
×
444
            return;
×
445
        }
446
    };
447

448
    let conf = match Config::from_config_file(config_file, true) {
×
UNCOV
449
        Ok(conf) => conf,
×
450
        Err(e) => {
×
451
            warn!("Invalid config: {e}");
×
452
            process::exit(1);
×
453
        }
454
    };
455

UNCOV
456
    debug!("node configuration {:?}", &conf.node);
×
457
    debug!("burnchain configuration {:?}", &conf.burnchain);
×
UNCOV
458
    debug!("connection configuration {:?}", &conf.connection_options);
×
459

UNCOV
460
    send_pending_event_payloads(&conf);
×
461

UNCOV
462
    let num_round: u64 = 0; // Infinite number of rounds
×
463

UNCOV
464
    if conf.burnchain.mode == "helium" || conf.burnchain.mode == "mocknet" {
×
UNCOV
465
        let mut run_loop = helium::RunLoop::new(conf);
×
UNCOV
466
        if let Err(e) = run_loop.start(num_round) {
×
UNCOV
467
            warn!("Helium runloop exited: {e}");
×
UNCOV
468
        }
×
UNCOV
469
    } else if conf.burnchain.mode == "neon"
×
UNCOV
470
        || conf.burnchain.mode == "nakamoto-neon"
×
UNCOV
471
        || conf.burnchain.mode == "xenon"
×
UNCOV
472
        || conf.burnchain.mode == "krypton"
×
UNCOV
473
        || conf.burnchain.mode == "mainnet"
×
UNCOV
474
    {
×
UNCOV
475
        let mut run_loop = boot_nakamoto::BootRunLoop::new(conf).unwrap();
×
UNCOV
476
        run_loop.start(None, 0);
×
UNCOV
477
    } else {
×
UNCOV
478
        println!("Burnchain mode '{}' not supported", conf.burnchain.mode);
×
UNCOV
479
    }
×
UNCOV
480
}
×
481

UNCOV
482
fn version() -> String {
×
UNCOV
483
    stacks::version_string("stacks-node", option_env!("STACKS_NODE_VERSION"))
×
UNCOV
484
}
×
485

UNCOV
486
fn print_help() {
×
UNCOV
487
    let argv: Vec<_> = env::args().collect();
×
488

UNCOV
489
    eprintln!(
×
490
        "\
491
{} <SUBCOMMAND>
492
Run a stacks-node.
493

494
USAGE:
495
stacks-node <SUBCOMMAND>
496

497
SUBCOMMANDS:
498

499
mainnet\t\tStart a node that will join and stream blocks from the public mainnet.
500

501
mocknet\t\tStart a node based on a fast local setup emulating a burnchain. Ideal for smart contract development.
502

503
helium\t\tStart a node based on a local setup relying on a local instance of bitcoind.
504
\t\tThe following bitcoin.conf is expected:
505
\t\t  chain=regtest
506
\t\t  disablewallet=0
507
\t\t  txindex=1
508
\t\t  server=1
509
\t\t  rpcuser=helium
510
\t\t  rpcpassword=helium
511

512
testnet\t\tStart a node that will join and stream blocks from the public testnet, relying on Bitcoin Testnet.
513

514
start\t\tStart a node with a config of your own. Can be used for joining a network, starting new chain, etc.
515
\t\tArguments:
516
\t\t  --config: path of the config (such as https://github.com/blockstack/stacks-blockchain/blob/master/sample/conf/testnet-follower-conf.toml).
517
\t\tExample:
518
\t\t  stacks-node start --config /path/to/config.toml
519

520
check-config\t\tValidates the config file without starting up the node. Uses same arguments as start subcommand.
521

522
version\t\tDisplay information about the current version and our release cycle.
523

524
key-for-seed\tOutput the associated secret key for a burnchain signer created with a given seed.
525
\t\tCan be passed a config file for the seed via the `--config <file>` option *or* by supplying the hex seed on
526
\t\tthe command line directly.
527

528
replay-mock-mining\tReplay mock mined blocks from <dir>
529
\t\tArguments:
530
\t\t  --path: path to directory of mock mined blocks
531
\t\t  --config: path to the config file
532

533
help\t\tDisplay this help.
534

535
OPTIONAL ARGUMENTS:
536

537
\t\t--mine-at-height=<height>: optional argument for a miner to not attempt mining until Stacks block has sync'ed to <height>
538

UNCOV
539
", argv[0]);
×
UNCOV
540
}
×
541

542
#[cfg(test)]
543
pub mod tests;
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