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

stacks-network / stacks-core / 25903914664-1

15 May 2026 06:28AM UTC coverage: 47.122% (-38.8%) from 85.959%
25903914664-1

Pull #7199

github

94e391
web-flow
Merge 109f2828c into 1c7b8e6ac
Pull Request #7199: Feat: L1 and L2 early unlocks, updating signer

103343 of 219309 relevant lines covered (47.12%)

12880462.62 hits per line

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

0.0
/stacks-node/src/main.rs
1
#[macro_use]
2
extern crate serde_derive;
3
#[macro_use]
4
extern crate stacks_common;
5

6
extern crate clarity;
7
extern crate stacks;
8

9
#[allow(unused_imports)]
10
#[macro_use(o, slog_log, slog_trace, slog_debug, slog_info, slog_warn, slog_error)]
11
extern crate slog;
12

13
pub use stacks_common::util;
14
use stacks_common::util::hash::hex_bytes;
15

16
pub mod monitoring;
17

18
pub mod burnchains;
19
pub mod event_dispatcher;
20
pub mod genesis_data;
21
pub mod globals;
22
pub mod keychain;
23
pub mod nakamoto_node;
24
pub mod neon_node;
25
pub mod node;
26
pub mod operations;
27
pub mod run_loop;
28
pub mod syncctl;
29
pub mod tenure;
30

31
use std::collections::HashMap;
32
use std::{env, panic, process};
33

34
use backtrace::Backtrace;
35
use pico_args::Arguments;
36
use stacks::chainstate::burn::db::sortdb::SortitionDB;
37
use stacks::chainstate::burn::operations::leader_block_commit::RewardSetInfo;
38
use stacks::chainstate::coordinator::{get_next_recipients, OnChainRewardSetProvider};
39
use stacks::chainstate::stacks::address::PoxAddress;
40
use stacks::chainstate::stacks::db::blocks::DummyEventDispatcher;
41
use stacks::chainstate::stacks::db::StacksChainState;
42
use stacks::config::chain_data::MinerStats;
43
pub use stacks::config::{Config, ConfigFile};
44
#[cfg(not(any(target_os = "macos", target_os = "windows", target_arch = "arm")))]
45
use tikv_jemallocator::Jemalloc;
46

47
pub use self::burnchains::{
48
    BitcoinRegtestController, BurnchainController, BurnchainTip, MocknetController,
49
};
50
pub use self::event_dispatcher::EventDispatcher;
51
pub use self::keychain::Keychain;
52
pub use self::node::{ChainTip, Node};
53
pub use self::run_loop::{helium, neon};
54
pub use self::tenure::Tenure;
55
use crate::neon_node::{BlockMinerThread, TipCandidate};
56
use crate::run_loop::boot_nakamoto;
57

58
#[cfg(not(any(target_os = "macos", target_os = "windows", target_arch = "arm")))]
59
#[global_allocator]
60
static GLOBAL: Jemalloc = Jemalloc;
61

62
/// Implmentation of `pick_best_tip` CLI option
63
fn cli_pick_best_tip(config_path: &str, at_stacks_height: Option<u64>) -> TipCandidate {
×
64
    info!("Loading config at path {config_path}");
×
65
    let config = match ConfigFile::from_path(config_path) {
×
66
        Ok(config_file) => Config::from_config_file(config_file, true).unwrap(),
×
67
        Err(e) => {
×
68
            warn!("Invalid config file: {e}");
×
69
            process::exit(1);
×
70
        }
71
    };
72
    let burn_db_path = config.get_burn_db_file_path();
×
73
    let stacks_chainstate_path = config.get_chainstate_path_str();
×
74
    let burnchain = config.get_burnchain();
×
75
    let (mut chainstate, _) = StacksChainState::open(
×
76
        config.is_mainnet(),
×
77
        config.burnchain.chain_id,
×
78
        &stacks_chainstate_path,
×
79
        Some(config.node.get_marf_opts()),
×
80
    )
×
81
    .unwrap();
×
82
    let mut sortdb = SortitionDB::open(
×
83
        &burn_db_path,
×
84
        false,
85
        burnchain.pox_constants,
×
86
        Some(config.node.get_marf_opts()),
×
87
    )
88
    .unwrap();
×
89

90
    let max_depth = config.miner.max_reorg_depth;
×
91

92
    // There could be more than one possible chain tip. Go find them.
93
    let stacks_tips = BlockMinerThread::load_candidate_tips(
×
94
        &mut sortdb,
×
95
        &mut chainstate,
×
96
        max_depth,
×
97
        at_stacks_height,
×
98
    );
99

100
    BlockMinerThread::inner_pick_best_tip(stacks_tips, HashMap::new()).unwrap()
×
101
}
×
102

103
/// Implementation of `get_miner_spend` CLI option
104
#[allow(clippy::incompatible_msrv)]
105
fn cli_get_miner_spend(
×
106
    config_path: &str,
×
107
    mine_start: Option<u64>,
×
108
    at_burnchain_height: Option<u64>,
×
109
) -> u64 {
×
110
    info!("Loading config at path {config_path}");
×
111
    let config = match ConfigFile::from_path(config_path) {
×
112
        Ok(config_file) => Config::from_config_file(config_file, true).unwrap(),
×
113
        Err(e) => {
×
114
            warn!("Invalid config file: {e}");
×
115
            process::exit(1);
×
116
        }
117
    };
118
    let keychain = Keychain::default(config.node.seed.clone());
×
119
    let burn_db_path = config.get_burn_db_file_path();
×
120
    let stacks_chainstate_path = config.get_chainstate_path_str();
×
121
    let burnchain = config.get_burnchain();
×
122
    let (mut chainstate, _) = StacksChainState::open(
×
123
        config.is_mainnet(),
×
124
        config.burnchain.chain_id,
×
125
        &stacks_chainstate_path,
×
126
        Some(config.node.get_marf_opts()),
×
127
    )
×
128
    .unwrap();
×
129
    let mut sortdb = SortitionDB::open(
×
130
        &burn_db_path,
×
131
        true,
132
        burnchain.pox_constants.clone(),
×
133
        Some(config.node.get_marf_opts()),
×
134
    )
135
    .unwrap();
×
136
    let tip = if let Some(at_burnchain_height) = at_burnchain_height {
×
137
        let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
×
138
        let ih = sortdb.index_handle(&tip.sortition_id);
×
139
        ih.get_block_snapshot_by_height(at_burnchain_height)
×
140
            .unwrap()
×
141
            .unwrap()
×
142
    } else {
143
        SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap()
×
144
    };
145

146
    let no_dispatcher: Option<&DummyEventDispatcher> = None;
×
147
    let recipients = get_next_recipients(
×
148
        &tip,
×
149
        &mut chainstate,
×
150
        &mut sortdb,
×
151
        &burnchain,
×
152
        &OnChainRewardSetProvider(no_dispatcher),
×
153
    )
154
    .unwrap();
×
155

156
    let commit_outs = if !burnchain.is_in_prepare_phase(tip.block_height + 1) {
×
157
        RewardSetInfo::into_commit_outs(recipients, config.is_mainnet())
×
158
    } else {
159
        vec![PoxAddress::standard_burn_address(config.is_mainnet())]
×
160
    };
161

162
    let spend_amount = BlockMinerThread::get_mining_spend_amount(
×
163
        &config,
×
164
        &keychain,
×
165
        &burnchain,
×
166
        &sortdb,
×
167
        &commit_outs,
×
168
        mine_start.unwrap_or(tip.block_height),
×
169
        at_burnchain_height,
×
170
        |burn_block_height| {
×
171
            let sortdb = SortitionDB::open(
×
172
                &burn_db_path,
×
173
                true,
174
                burnchain.pox_constants.clone(),
×
175
                Some(config.node.get_marf_opts()),
×
176
            )
177
            .unwrap();
×
178
            let Some(miner_stats) = config.get_miner_stats() else {
×
179
                return 0.0;
×
180
            };
181
            let Ok(active_miners_and_commits) =
×
182
                MinerStats::get_active_miners(&sortdb, Some(burn_block_height))
×
183
                    .inspect_err(|e| warn!("Failed to get active miners: {e:?}"))
×
184
            else {
185
                return 0.0;
×
186
            };
187
            if active_miners_and_commits.is_empty() {
×
188
                warn!("No active miners detected; using config file burn_fee_cap");
×
189
                return 0.0;
×
190
            }
×
191

192
            let active_miners: Vec<_> = active_miners_and_commits
×
193
                .iter()
×
194
                .map(|(miner, _cmt)| miner.as_str())
×
195
                .collect();
×
196

197
            info!("Active miners: {active_miners:?}");
×
198

199
            let Ok(unconfirmed_block_commits) = miner_stats
×
200
                .get_unconfirmed_commits(burn_block_height + 1, &active_miners)
×
201
                .inspect_err(|e| warn!("Failed to find unconfirmed block-commits: {e}"))
×
202
            else {
203
                return 0.0;
×
204
            };
205

206
            let unconfirmed_miners_and_amounts: Vec<(String, u64)> = unconfirmed_block_commits
×
207
                .iter()
×
208
                .map(|cmt| (format!("{}", &cmt.apparent_sender), cmt.burn_fee))
×
209
                .collect();
×
210

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

213
            let (spend_dist, _total_spend) = MinerStats::get_spend_distribution(
×
214
                &active_miners_and_commits,
×
215
                &unconfirmed_block_commits,
×
216
                &commit_outs,
×
217
            );
×
218
            let win_probs = if config.miner.fast_rampup {
×
219
                // look at spends 6+ blocks in the future
220
                MinerStats::get_future_win_distribution(
×
221
                    &active_miners_and_commits,
×
222
                    &unconfirmed_block_commits,
×
223
                    &commit_outs,
×
224
                )
225
            } else {
226
                // look at the current spends
227
                let Ok(unconfirmed_burn_dist) = miner_stats
×
228
                    .get_unconfirmed_burn_distribution(
×
229
                        &burnchain,
×
230
                        &sortdb,
×
231
                        &active_miners_and_commits,
×
232
                        unconfirmed_block_commits,
×
233
                        &commit_outs,
×
234
                        at_burnchain_height,
×
235
                    )
236
                    .inspect_err(|e| warn!("Failed to get unconfirmed burn distribution: {e:?}"))
×
237
                else {
238
                    return 0.0;
×
239
                };
240

241
                MinerStats::burn_dist_to_prob_dist(&unconfirmed_burn_dist)
×
242
            };
243

244
            info!("Unconfirmed spend distribution: {spend_dist:?}");
×
245
            info!(
×
246
                "Unconfirmed win probabilities (fast_rampup={}): {win_probs:?}",
247
                config.miner.fast_rampup
248
            );
249

250
            let miner_addrs = BlockMinerThread::get_miner_addrs(&config, &keychain);
×
251
            let win_prob = miner_addrs
×
252
                .iter()
×
253
                .find_map(|x| win_probs.get(x))
×
254
                .copied()
×
255
                .unwrap_or(0.0);
×
256

257
            info!(
×
258
                "This miner's win probability at {} is {win_prob}",
259
                tip.block_height
260
            );
261
            win_prob
×
262
        },
×
263
        |_burn_block_height, _win_prob| {},
×
264
    );
265
    spend_amount
×
266
}
×
267

268
fn main() {
×
269
    panic::set_hook(Box::new(|panic_info| {
×
270
        error!("Process abort due to thread panic: {panic_info}");
×
271
        let bt = Backtrace::new();
×
272
        error!("Panic backtrace: {bt:?}");
×
273

274
        // force a core dump
275
        #[cfg(unix)]
276
        {
277
            let pid = process::id();
×
278
            eprintln!("Dumping core for pid {}", std::process::id());
×
279

280
            use libc::{kill, SIGQUIT};
281

282
            // *should* trigger a core dump, if you run `ulimit -c unlimited` first!
283
            unsafe { kill(pid.try_into().unwrap(), SIGQUIT) };
×
284
        }
285

286
        // just in case
287
        process::exit(1);
×
288
    }));
289

290
    let mut args = Arguments::from_env();
×
291
    let subcommand = args.subcommand().unwrap().unwrap_or_default();
×
292

293
    info!("{}", version());
×
294

295
    let mine_start: Option<u64> = args
×
296
        .opt_value_from_str("--mine-at-height")
×
297
        .expect("Failed to parse --mine-at-height argument");
×
298

299
    if let Some(mine_start) = mine_start {
×
300
        info!("Will begin mining once Stacks chain has synced to height >= {mine_start}");
×
301
    }
×
302

303
    let config_file = match subcommand.as_str() {
×
304
        "mocknet" => {
×
305
            args.finish();
×
306
            ConfigFile::mocknet()
×
307
        }
308
        "helium" => {
×
309
            args.finish();
×
310
            ConfigFile::helium()
×
311
        }
312
        "testnet" => {
×
313
            args.finish();
×
314
            ConfigFile::xenon()
×
315
        }
316
        "mainnet" => {
×
317
            args.finish();
×
318
            ConfigFile::mainnet()
×
319
        }
320
        "check-config" => {
×
321
            let config_path: String = args.value_from_str("--config").unwrap();
×
322
            args.finish();
×
323
            info!("Loading config at path {config_path}");
×
324
            let config_file = match ConfigFile::from_path(&config_path) {
×
325
                Ok(config_file) => {
×
326
                    debug!("Loaded config file: {config_file:?}");
×
327
                    config_file
×
328
                }
329
                Err(e) => {
×
330
                    warn!("Invalid config file: {e}");
×
331
                    process::exit(1);
×
332
                }
333
            };
334
            match Config::from_config_file(config_file, true) {
×
335
                Ok(_) => {
336
                    info!("Loaded config!");
×
337
                    process::exit(0);
×
338
                }
339
                Err(e) => {
×
340
                    warn!("Invalid config: {e}");
×
341
                    process::exit(1);
×
342
                }
343
            };
344
        }
345
        "start" => {
×
346
            let config_path: String = args.value_from_str("--config").unwrap();
×
347
            args.finish();
×
348
            info!("Loading config at path {config_path}");
×
349
            match ConfigFile::from_path(&config_path) {
×
350
                Ok(config_file) => config_file,
×
351
                Err(e) => {
×
352
                    warn!("Invalid config file: {e}");
×
353
                    process::exit(1);
×
354
                }
355
            }
356
        }
357
        "version" => {
×
358
            println!("{}", &version());
×
359
            return;
×
360
        }
361
        "key-for-seed" => {
×
362
            let seed = {
×
363
                let config_path: Option<String> = args.opt_value_from_str("--config").unwrap();
×
364
                if let Some(config_path) = config_path {
×
365
                    let conf = Config::from_config_file(
×
366
                        ConfigFile::from_path(&config_path).unwrap(),
×
367
                        true,
368
                    )
369
                    .unwrap();
×
370
                    args.finish();
×
371
                    conf.node.seed
×
372
                } else {
373
                    let free_args = args.finish();
×
374
                    let seed_hex = free_args
×
375
                        .first()
×
376
                        .expect("`wif-for-seed` must be passed either a config file via the `--config` flag or a hex seed string");
×
377
                    hex_bytes(seed_hex.to_str().unwrap())
×
378
                        .expect("Seed should be a hex encoded string")
×
379
                }
380
            };
381
            let keychain = Keychain::default(seed);
×
382
            println!(
×
383
                "Hex formatted secret key: {}",
384
                keychain.generate_op_signer().get_secret_key_as_hex()
×
385
            );
386
            println!(
×
387
                "WIF formatted secret key: {}",
388
                keychain.generate_op_signer().get_secret_key_as_wif()
×
389
            );
390
            return;
×
391
        }
392
        "pick-best-tip" => {
×
393
            let config_path: String = args.value_from_str("--config").unwrap();
×
394
            let at_stacks_height: Option<u64> =
×
395
                args.opt_value_from_str("--at-stacks-height").unwrap();
×
396
            args.finish();
×
397

398
            let best_tip = cli_pick_best_tip(&config_path, at_stacks_height);
×
399
            println!("Best tip is {best_tip:?}");
×
400
            process::exit(0);
×
401
        }
402
        "get-spend-amount" => {
×
403
            let config_path: String = args.value_from_str("--config").unwrap();
×
404
            let at_burnchain_height: Option<u64> =
×
405
                args.opt_value_from_str("--at-bitcoin-height").unwrap();
×
406
            args.finish();
×
407

408
            let spend_amount = cli_get_miner_spend(&config_path, mine_start, at_burnchain_height);
×
409
            println!("Will spend {spend_amount}");
×
410
            process::exit(0);
×
411
        }
412
        _ => {
413
            print_help();
×
414
            return;
×
415
        }
416
    };
417

418
    let conf = match Config::from_config_file(config_file, true) {
×
419
        Ok(conf) => conf,
×
420
        Err(e) => {
×
421
            warn!("Invalid config: {e}");
×
422
            process::exit(1);
×
423
        }
424
    };
425

426
    debug!("node configuration {:?}", &conf.node);
×
427
    debug!("burnchain configuration {:?}", &conf.burnchain);
×
428
    debug!("connection configuration {:?}", &conf.connection_options);
×
429

430
    let num_round: u64 = 0; // Infinite number of rounds
×
431

432
    if conf.burnchain.mode == "helium" || conf.burnchain.mode == "mocknet" {
×
433
        let mut run_loop = helium::RunLoop::new(conf);
×
434
        if let Err(e) = run_loop.start(num_round) {
×
435
            warn!("Helium runloop exited: {e}");
×
436
        }
×
437
    } else if conf.burnchain.mode == "neon"
×
438
        || conf.burnchain.mode == "nakamoto-neon"
×
439
        || conf.burnchain.mode == "xenon"
×
440
        || conf.burnchain.mode == "krypton"
×
441
        || conf.burnchain.mode == "mainnet"
×
442
    {
×
443
        let mut run_loop = boot_nakamoto::BootRunLoop::new(conf).unwrap();
×
444
        run_loop.start(None, 0);
×
445
    } else {
×
446
        println!("Burnchain mode '{}' not supported", conf.burnchain.mode);
×
447
    }
×
448
}
×
449

450
fn version() -> String {
×
451
    stacks::version_string("stacks-node", option_env!("STACKS_NODE_VERSION"))
×
452
}
×
453

454
fn print_help() {
×
455
    let argv: Vec<_> = env::args().collect();
×
456

457
    eprintln!(
×
458
        "\
459
{} <SUBCOMMAND>
460
Run a stacks-node.
461

462
USAGE:
463
stacks-node <SUBCOMMAND>
464

465
SUBCOMMANDS:
466

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

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

471
helium\t\tStart a node based on a local setup relying on a local instance of bitcoind.
472
\t\tThe following bitcoin.conf is expected:
473
\t\t  chain=regtest
474
\t\t  disablewallet=0
475
\t\t  txindex=1
476
\t\t  server=1
477
\t\t  rpcuser=helium
478
\t\t  rpcpassword=helium
479

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

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

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

490
version\t\tDisplay information about the current version and our release cycle.
491

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

496
replay-mock-mining\tReplay mock mined blocks from <dir>
497
\t\tArguments:
498
\t\t  --path: path to directory of mock mined blocks
499
\t\t  --config: path to the config file
500

501
help\t\tDisplay this help.
502

503
OPTIONAL ARGUMENTS:
504

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

507
", argv[0]);
×
508
}
×
509

510
#[cfg(test)]
511
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