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

stacks-network / stacks-core / 26250451051-1

21 May 2026 08:11PM UTC coverage: 85.585% (-0.1%) from 85.712%
26250451051-1

Pull #7215

github

ec9d4c
web-flow
Merge 9487bf852 into af1280aac
Pull Request #7215: Chore: fix flake in non_blocking_minority_configured_to_favour_...

188844 of 220651 relevant lines covered (85.58%)

18975267.44 hits per line

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

68.96
/stackslib/src/config/mod.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
pub mod chain_data;
18

19
use std::collections::{HashMap, HashSet};
20
use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs};
21
use std::path::PathBuf;
22
use std::str::FromStr;
23
use std::sync::{Arc, LazyLock, Mutex};
24
use std::time::Duration;
25
use std::{cmp, fs, thread};
26

27
use clarity::vm::costs::ExecutionCost;
28
use clarity::vm::types::{AssetIdentifier, PrincipalData, QualifiedContractIdentifier};
29
use rand::RngCore;
30
use serde::Deserialize;
31
use stacks_common::consts::SIGNER_SLOTS_PER_USER;
32
use stacks_common::types::chainstate::StacksAddress;
33
use stacks_common::types::net::PeerAddress;
34
use stacks_common::types::Address;
35
use stacks_common::util::get_epoch_time_ms;
36
use stacks_common::util::hash::hex_bytes;
37
use stacks_common::util::secp256k1::{Secp256k1PrivateKey, Secp256k1PublicKey};
38

39
use crate::burnchains::bitcoin::BitcoinNetworkType;
40
use crate::burnchains::{Burnchain, MagicBytes, BLOCKSTACK_MAGIC_MAINNET};
41
use crate::chainstate::nakamoto::signer_set::NakamotoSigners;
42
use crate::chainstate::stacks::boot::MINERS_NAME;
43
use crate::chainstate::stacks::index::marf::MARFOpenOpts;
44
use crate::chainstate::stacks::index::storage::TrieHashCalculationMode;
45
use crate::chainstate::stacks::miner::{BlockBuilderSettings, MinerStatus};
46
use crate::chainstate::stacks::MAX_BLOCK_LEN;
47
use crate::config::chain_data::MinerStats;
48
use crate::core::mempool::{MemPoolWalkSettings, MemPoolWalkStrategy, MemPoolWalkTxTypes};
49
use crate::core::{
50
    MemPoolDB, StacksEpoch, StacksEpochExtension, StacksEpochId, CHAIN_ID_MAINNET,
51
    CHAIN_ID_TESTNET, PEER_VERSION_MAINNET, PEER_VERSION_TESTNET, STACKS_EPOCHS_REGTEST,
52
    STACKS_EPOCHS_TESTNET,
53
};
54
use crate::cost_estimates::fee_medians::WeightedMedianFeeRateEstimator;
55
use crate::cost_estimates::fee_rate_fuzzer::FeeRateFuzzer;
56
use crate::cost_estimates::fee_scalar::ScalarFeeRateEstimator;
57
use crate::cost_estimates::metrics::{CostMetric, ProportionalDotProduct, UnitMetric};
58
use crate::cost_estimates::{CostEstimator, FeeEstimator, PessimisticEstimator, UnitEstimator};
59
use crate::net::atlas::AtlasConfig;
60
use crate::net::connection::{
61
    ConnectionOptions, DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS,
62
    DEFAULT_BLOCK_PROPOSAL_MAX_TX_EXECUTION_TIME_SECS,
63
    DEFAULT_BLOCK_PROPOSAL_VALIDATION_TIMEOUT_SECS,
64
};
65
use crate::net::{Neighbor, NeighborAddress, NeighborKey};
66
use crate::types::chainstate::BurnchainHeaderHash;
67
use crate::types::EpochList;
68
use crate::util::hash::to_hex;
69
use crate::util_lib::boot::boot_code_id;
70
use crate::util_lib::db::Error as DBError;
71

72
pub const DEFAULT_SATS_PER_VB: u64 = 50;
73
pub const OP_TX_BLOCK_COMMIT_ESTIM_SIZE: u64 = 380;
74
pub const OP_TX_DELEGATE_STACKS_ESTIM_SIZE: u64 = 230;
75
pub const OP_TX_LEADER_KEY_ESTIM_SIZE: u64 = 290;
76
pub const OP_TX_PRE_STACKS_ESTIM_SIZE: u64 = 280;
77
pub const OP_TX_STACK_STX_ESTIM_SIZE: u64 = 250;
78
pub const OP_TX_TRANSFER_STACKS_ESTIM_SIZE: u64 = 230;
79
pub const OP_TX_VOTE_AGG_ESTIM_SIZE: u64 = 230;
80

81
pub const OP_TX_ANY_ESTIM_SIZE: u64 = fmax!(
82
    OP_TX_BLOCK_COMMIT_ESTIM_SIZE,
83
    OP_TX_DELEGATE_STACKS_ESTIM_SIZE,
84
    OP_TX_LEADER_KEY_ESTIM_SIZE,
85
    OP_TX_PRE_STACKS_ESTIM_SIZE,
86
    OP_TX_STACK_STX_ESTIM_SIZE,
87
    OP_TX_TRANSFER_STACKS_ESTIM_SIZE,
88
    OP_TX_VOTE_AGG_ESTIM_SIZE
89
);
90

91
/// Default maximum percentage of `satoshis_per_byte` that a Bitcoin fee rate
92
/// may be increased to when RBFing a transaction
93
const DEFAULT_MAX_RBF_RATE: u64 = 150; // 1.5x
94
/// Amount to increment the fee by, in Sats/vByte, when RBFing a Bitcoin
95
/// transaction
96
const DEFAULT_RBF_FEE_RATE_INCREMENT: u64 = 5;
97
/// Default number of reward cycles of blocks to sync in a non-full inventory
98
/// sync
99
const INV_REWARD_CYCLES_TESTNET: u64 = 6;
100
/// Default minimum time to wait between mining blocks in milliseconds. The
101
/// value must be greater than or equal to 1000 ms because if a block is mined
102
/// within the same second as its parent, it will be rejected by the signers.
103
const DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS: u64 = 1_000;
104
/// Default time in milliseconds to pause after receiving the first threshold
105
/// rejection, before proposing a new block.
106
const DEFAULT_FIRST_REJECTION_PAUSE_MS: u64 = 5_000;
107
/// Default time in milliseconds to pause after receiving subsequent threshold
108
/// rejections, before proposing a new block.
109
const DEFAULT_SUBSEQUENT_REJECTION_PAUSE_MS: u64 = 10_000;
110
/// Default time in milliseconds to wait for a Nakamoto block after seeing a
111
/// burnchain block before submitting a block commit.
112
const DEFAULT_BLOCK_COMMIT_DELAY_MS: u64 = 40_000;
113
/// Default percentage of the remaining tenure cost limit to consume each block
114
const DEFAULT_TENURE_COST_LIMIT_PER_BLOCK_PERCENTAGE: u8 = 25;
115
/// Default percentage of the block limit to consume by non-boot contract calls
116
pub const DEFAULT_CONTRACT_COST_LIMIT_PERCENTAGE: u8 = 95;
117
/// Default number of seconds to wait in-between polling the sortition DB to
118
/// see if we need to extend the ongoing tenure (e.g. because the current
119
/// sortition is empty or invalid).
120
const DEFAULT_TENURE_EXTEND_POLL_SECS: u64 = 1;
121
/// Default number of millis to wait before trying to continue a tenure because the next miner did not produce blocks
122
const DEFAULT_TENURE_EXTEND_WAIT_MS: u64 = 120_000;
123
/// Default duration to wait before attempting to issue a tenure extend.
124
/// This should be greater than the signers' timeout. This is used for issuing
125
/// fallback tenure extends
126
const DEFAULT_TENURE_TIMEOUT_SECS: u64 = 180;
127
/// Default percentage of block budget that must be used before attempting a
128
/// time-based tenure extend
129
const DEFAULT_TENURE_EXTEND_COST_THRESHOLD: u64 = 50;
130
/// Default percentage of block budget that must be used before attempting a
131
/// time-based read-count extend
132
const DEFAULT_READ_COUNT_EXTEND_COST_THRESHOLD: u64 = 25;
133
/// Default number of milliseconds that the miner should sleep between mining
134
/// attempts when the mempool is empty.
135
const DEFAULT_EMPTY_MEMPOOL_SLEEP_MS: u64 = 2_500;
136
/// Default maximum execution time in seconds for a miner to process a transaction
137
/// before timing out.
138
const DEFAULT_MAX_EXECUTION_TIME_SECS: u64 = 30;
139
/// Default number of seconds that a miner should wait before timing out an HTTP request to StackerDB.
140
const DEFAULT_STACKERDB_TIMEOUT_SECS: u64 = 120;
141
/// Default maximum size for a tenure (note: the counter is reset on tenure extend).
142
pub const DEFAULT_MAX_TENURE_BYTES: u64 = 10 * 1024 * 1024; // 10 MB
143
/// Default maximum memory allocation during miner block assembly
144
const DEFAULT_MINER_ASSEMBLY_MEMORY_BYTES: u64 = 2 * 1024 * 1024 * 1024; // 2 GB
145
/// Default maximum memory allocation during block proposal evaluation. Defaults higher than miner default
146
///  to avoid miner/signer environment skews.
147
pub const DEFAULT_PROPOSAL_MEMORY_BYTES: u64 = 3 * 1024 * 1024 * 1024; // 3 GB
148

149
static HELIUM_DEFAULT_CONNECTION_OPTIONS: LazyLock<ConnectionOptions> =
150
    LazyLock::new(|| ConnectionOptions {
151
        inbox_maxlen: 100,
152
        outbox_maxlen: 100,
153
        timeout: 15,
154
        idle_timeout: 15, // how long a HTTP connection can be idle before it's closed
155
        heartbeat: 3600,
156
        // can't use u64::max, because sqlite stores as i64.
157
        private_key_lifetime: 9223372036854775807,
158
        num_neighbors: 32,         // number of neighbors whose inventories we track
159
        num_clients: 750,          // number of inbound p2p connections
160
        soft_num_neighbors: 16, // soft-limit on the number of neighbors whose inventories we track
161
        soft_num_clients: 750,  // soft limit on the number of inbound p2p connections
162
        max_neighbors_per_host: 1, // maximum number of neighbors per host we permit
163
        max_clients_per_host: 4, // maximum number of inbound p2p connections per host we permit
164
        soft_max_neighbors_per_host: 1, // soft limit on the number of neighbors per host we permit
165
        soft_max_neighbors_per_org: 32, // soft limit on the number of neighbors per AS we permit (TODO: for now it must be greater than num_neighbors)
166
        soft_max_clients_per_host: 4, // soft limit on how many inbound p2p connections per host we permit
167
        max_http_clients: 1000,       // maximum number of HTTP connections
168
        max_neighbors_of_neighbor: 10, // maximum number of neighbors we'll handshake with when doing a neighbor walk (I/O for this can be expensive, so keep small-ish)
169
        walk_interval: 60,             // how often, in seconds, we do a neighbor walk
170
        walk_seed_probability: 0.1, // 10% of the time when not in IBD, walk to a non-seed node even if we aren't connected to a seed node
171
        log_neighbors_freq: 60_000, // every minute, log all peer connections
172
        inv_sync_interval: 45,      // how often, in seconds, we refresh block inventories
173
        inv_reward_cycles: 3,       // how many reward cycles to look back on, for mainnet
174
        download_interval: 10, // how often, in seconds, we do a block download scan (should be less than inv_sync_interval)
175
        dns_timeout: 15_000,
176
        max_inflight_blocks: 6,
177
        max_inflight_attachments: 6,
178
        ..std::default::Default::default()
2,604✔
179
    });
2,604✔
180

181
pub static DEFAULT_MAINNET_CONFIG: LazyLock<Config> = LazyLock::new(|| {
9✔
182
    Config::from_config_file(ConfigFile::mainnet(), false)
9✔
183
        .expect("Failed to create default mainnet config")
9✔
184
});
9✔
185

186
#[derive(Clone, Deserialize, Default, Debug)]
187
#[serde(deny_unknown_fields)]
188
pub struct ConfigFile {
189
    pub __path: Option<String>, // Only used for config file reloads
190
    pub burnchain: Option<BurnchainConfigFile>,
191
    pub node: Option<NodeConfigFile>,
192
    /// Represents an initial STX balance allocation for an address at genesis
193
    /// for testing purposes.
194
    ///
195
    /// This struct is used to define pre-allocated STX balances that are credited to
196
    /// specific addresses when the Stacks node first initializes its chainstate. These balances
197
    /// are included in the genesis block and are immediately available for spending.
198
    ///
199
    /// **Configuration:**
200
    /// Configured as a list `[[ustx_balance]]` in TOML.
201
    ///
202
    /// Example TOML entry:
203
    /// ```toml
204
    /// [[ustx_balance]]
205
    /// address = "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2"
206
    /// amount = 10000000000000000
207
    /// ```
208
    ///
209
    /// This is intended strictly for testing purposes.
210
    /// Attempting to specify initial balances if [`BurnchainConfig::mode`] is "mainnet" will
211
    /// result in an invalid config error.
212
    ///
213
    /// Default: `None`
214
    pub ustx_balance: Option<Vec<InitialBalanceFile>>,
215
    /// Deprecated: use `ustx_balance` instead
216
    pub mstx_balance: Option<Vec<InitialBalanceFile>>,
217
    pub events_observer: Option<HashSet<EventObserverConfigFile>>,
218
    pub connection_options: Option<ConnectionOptionsFile>,
219
    pub fee_estimation: Option<FeeEstimationConfigFile>,
220
    pub miner: Option<MinerConfigFile>,
221
    pub atlas: Option<AtlasConfigFile>,
222
}
223

224
impl ConfigFile {
225
    pub fn from_path(path: &str) -> Result<ConfigFile, String> {
83,088✔
226
        let content = fs::read_to_string(path).map_err(|e| format!("Invalid path: {e}"))?;
83,088✔
227
        let mut f = Self::from_str(&content)?;
1,493✔
228
        f.__path = Some(path.to_string());
1,493✔
229
        Ok(f)
1,493✔
230
    }
83,088✔
231

232
    #[allow(clippy::should_implement_trait)]
233
    pub fn from_str(content: &str) -> Result<ConfigFile, String> {
1,512✔
234
        let mut config: ConfigFile =
1,502✔
235
            toml::from_str(content).map_err(|e| format!("Invalid toml: {e}"))?;
1,512✔
236
        if let Some(mstx_balance) = config.mstx_balance.take() {
1,502✔
237
            warn!("'mstx_balance' in the config is deprecated; please use 'ustx_balance' instead.");
1✔
238
            match config.ustx_balance {
1✔
239
                Some(ref mut ustx_balance) => {
1✔
240
                    ustx_balance.extend(mstx_balance);
1✔
241
                }
1✔
242
                None => {
×
243
                    config.ustx_balance = Some(mstx_balance);
×
244
                }
×
245
            }
246
        }
1,501✔
247
        Ok(config)
1,502✔
248
    }
1,512✔
249

250
    pub fn xenon() -> ConfigFile {
2,106✔
251
        let burnchain = BurnchainConfigFile {
2,106✔
252
            mode: Some("xenon".to_string()),
2,106✔
253
            rpc_port: Some(18332),
2,106✔
254
            peer_port: Some(18333),
2,106✔
255
            peer_host: Some("0.0.0.0".to_string()),
2,106✔
256
            magic_bytes: Some("T2".into()),
2,106✔
257
            ..BurnchainConfigFile::default()
2,106✔
258
        };
2,106✔
259

260
        let node = NodeConfigFile {
2,106✔
261
            bootstrap_node: Some("029266faff4c8e0ca4f934f34996a96af481df94a89b0c9bd515f3536a95682ddc@seed.testnet.hiro.so:30444".to_string()),
2,106✔
262
            miner: Some(false),
2,106✔
263
            stacker: Some(false),
2,106✔
264
            ..NodeConfigFile::default()
2,106✔
265
        };
2,106✔
266

267
        let balances = vec![
2,106✔
268
            InitialBalanceFile {
2,106✔
269
                address: "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2".to_string(),
2,106✔
270
                amount: 10000000000000000,
2,106✔
271
            },
2,106✔
272
            InitialBalanceFile {
2,106✔
273
                address: "ST319CF5WV77KYR1H3GT0GZ7B8Q4AQPY42ETP1VPF".to_string(),
2,106✔
274
                amount: 10000000000000000,
2,106✔
275
            },
2,106✔
276
            InitialBalanceFile {
2,106✔
277
                address: "ST221Z6TDTC5E0BYR2V624Q2ST6R0Q71T78WTAX6H".to_string(),
2,106✔
278
                amount: 10000000000000000,
2,106✔
279
            },
2,106✔
280
            InitialBalanceFile {
2,106✔
281
                address: "ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B".to_string(),
2,106✔
282
                amount: 10000000000000000,
2,106✔
283
            },
2,106✔
284
        ];
285

286
        ConfigFile {
2,106✔
287
            burnchain: Some(burnchain),
2,106✔
288
            node: Some(node),
2,106✔
289
            ustx_balance: Some(balances),
2,106✔
290
            ..ConfigFile::default()
2,106✔
291
        }
2,106✔
292
    }
2,106✔
293

294
    pub fn mainnet() -> ConfigFile {
21✔
295
        let burnchain = BurnchainConfigFile {
21✔
296
            mode: Some("mainnet".to_string()),
21✔
297
            rpc_port: Some(8332),
21✔
298
            peer_port: Some(8333),
21✔
299
            peer_host: Some("0.0.0.0".to_string()),
21✔
300
            username: Some("bitcoin".to_string()),
21✔
301
            password: Some("bitcoin".to_string()),
21✔
302
            magic_bytes: Some("X2".to_string()),
21✔
303
            ..BurnchainConfigFile::default()
21✔
304
        };
21✔
305

306
        let node = NodeConfigFile {
21✔
307
            bootstrap_node: Some("02196f005965cebe6ddc3901b7b1cc1aa7a88f305bb8c5893456b8f9a605923893@seed.mainnet.hiro.so:20444,02539449ad94e6e6392d8c1deb2b4e61f80ae2a18964349bc14336d8b903c46a8c@cet.stacksnodes.org:20444,02ececc8ce79b8adf813f13a0255f8ae58d4357309ba0cedd523d9f1a306fcfb79@sgt.stacksnodes.org:20444,0303144ba518fe7a0fb56a8a7d488f950307a4330f146e1e1458fc63fb33defe96@est.stacksnodes.org:20444".to_string()),
21✔
308
            miner: Some(false),
21✔
309
            stacker: Some(false),
21✔
310
            ..NodeConfigFile::default()
21✔
311
        };
21✔
312

313
        ConfigFile {
21✔
314
            burnchain: Some(burnchain),
21✔
315
            node: Some(node),
21✔
316
            ustx_balance: None,
21✔
317
            ..ConfigFile::default()
21✔
318
        }
21✔
319
    }
21✔
320

321
    pub fn helium() -> ConfigFile {
×
322
        // ## Settings for local testnet, relying on a local bitcoind server
323
        // ## running with the following bitcoin.conf:
324
        // ##
325
        // ##    chain=regtest
326
        // ##    disablewallet=0
327
        // ##    txindex=1
328
        // ##    server=1
329
        // ##    rpcuser=helium
330
        // ##    rpcpassword=helium
331
        // ##
332
        let burnchain = BurnchainConfigFile {
×
333
            mode: Some("helium".to_string()),
×
334
            commit_anchor_block_within: Some(10_000),
×
335
            rpc_port: Some(18443),
×
336
            peer_port: Some(18444),
×
337
            peer_host: Some("0.0.0.0".to_string()),
×
338
            username: Some("helium".to_string()),
×
339
            password: Some("helium".to_string()),
×
340
            local_mining_public_key: Some("04ee0b1602eb18fef7986887a7e8769a30c9df981d33c8380d255edef003abdcd243a0eb74afdf6740e6c423e62aec631519a24cf5b1d62bf8a3e06ddc695dcb77".to_string()),
×
341
            ..BurnchainConfigFile::default()
×
342
        };
×
343

344
        let node = NodeConfigFile {
×
345
            miner: Some(false),
×
346
            stacker: Some(false),
×
347
            ..NodeConfigFile::default()
×
348
        };
×
349

350
        ConfigFile {
×
351
            burnchain: Some(burnchain),
×
352
            node: Some(node),
×
353
            ..ConfigFile::default()
×
354
        }
×
355
    }
×
356

357
    pub fn mocknet() -> ConfigFile {
×
358
        let burnchain = BurnchainConfigFile {
×
359
            mode: Some("mocknet".to_string()),
×
360
            commit_anchor_block_within: Some(10_000),
×
361
            ..BurnchainConfigFile::default()
×
362
        };
×
363

364
        let node = NodeConfigFile {
×
365
            miner: Some(false),
×
366
            stacker: Some(false),
×
367
            ..NodeConfigFile::default()
×
368
        };
×
369

370
        let balances = vec![
×
371
            InitialBalanceFile {
×
372
                // "mnemonic": "point approve language letter cargo rough similar wrap focus edge polar task olympic tobacco cinnamon drop lawn boring sort trade senior screen tiger climb",
×
373
                // "privateKey": "539e35c740079b79f931036651ad01f76d8fe1496dbd840ba9e62c7e7b355db001",
×
374
                // "btcAddress": "n1htkoYKuLXzPbkn9avC2DJxt7X85qVNCK",
×
375
                address: "ST3EQ88S02BXXD0T5ZVT3KW947CRMQ1C6DMQY8H19".to_string(),
×
376
                amount: 10000000000000000,
×
377
            },
×
378
            InitialBalanceFile {
×
379
                // "mnemonic": "laugh capital express view pull vehicle cluster embark service clerk roast glance lumber glove purity project layer lyrics limb junior reduce apple method pear",
×
380
                // "privateKey": "075754fb099a55e351fe87c68a73951836343865cd52c78ae4c0f6f48e234f3601",
×
381
                // "btcAddress": "n2ZGZ7Zau2Ca8CLHGh11YRnLw93b4ufsDR",
×
382
                address: "ST3KCNDSWZSFZCC6BE4VA9AXWXC9KEB16FBTRK36T".to_string(),
×
383
                amount: 10000000000000000,
×
384
            },
×
385
            InitialBalanceFile {
×
386
                // "mnemonic": "level garlic bean design maximum inhale daring alert case worry gift frequent floor utility crowd twenty burger place time fashion slow produce column prepare",
×
387
                // "privateKey": "374b6734eaff979818c5f1367331c685459b03b1a2053310906d1408dc928a0001",
×
388
                // "btcAddress": "mhY4cbHAFoXNYvXdt82yobvVuvR6PHeghf",
×
389
                address: "STB2BWB0K5XZGS3FXVTG3TKS46CQVV66NAK3YVN8".to_string(),
×
390
                amount: 10000000000000000,
×
391
            },
×
392
            InitialBalanceFile {
×
393
                // "mnemonic": "drop guess similar uphold alarm remove fossil riot leaf badge lobster ability mesh parent lawn today student olympic model assault syrup end scorpion lab",
×
394
                // "privateKey": "26f235698d02803955b7418842affbee600fc308936a7ca48bf5778d1ceef9df01",
×
395
                // "btcAddress": "mkEDDqbELrKYGUmUbTAyQnmBAEz4V1MAro",
×
396
                address: "STSTW15D618BSZQB85R058DS46THH86YQQY6XCB7".to_string(),
×
397
                amount: 10000000000000000,
×
398
            },
×
399
        ];
400

401
        ConfigFile {
×
402
            burnchain: Some(burnchain),
×
403
            node: Some(node),
×
404
            ustx_balance: Some(balances),
×
405
            ..ConfigFile::default()
×
406
        }
×
407
    }
×
408
}
409

410
#[derive(Clone, Debug)]
411
pub struct Config {
412
    pub config_path: Option<String>,
413
    pub burnchain: BurnchainConfig,
414
    pub node: NodeConfig,
415
    pub initial_balances: Vec<InitialBalance>,
416
    pub events_observers: HashSet<EventObserverConfig>,
417
    pub connection_options: ConnectionOptions,
418
    pub miner: MinerConfig,
419
    pub estimation: FeeEstimationConfig,
420
    pub atlas: AtlasConfig,
421
}
422

423
impl Config {
424
    /// get the up-to-date burnchain options from the config.
425
    /// If the config file can't be loaded, then return the existing config
426
    pub fn get_burnchain_config(&self) -> BurnchainConfig {
2,663,514✔
427
        let Some(path) = &self.config_path else {
2,663,514✔
428
            return self.burnchain.clone();
2,659,491✔
429
        };
430
        let Ok(config_file) = ConfigFile::from_path(path.as_str()) else {
4,023✔
431
            return self.burnchain.clone();
3,564✔
432
        };
433
        let Ok(config) = Config::from_config_file(config_file, false) else {
459✔
434
            return self.burnchain.clone();
×
435
        };
436
        config.burnchain
459✔
437
    }
2,663,514✔
438

439
    /// get the up-to-date miner options from the config
440
    /// If the config can't be loaded for some reason, then return the existing config
441
    pub fn get_miner_config(&self) -> MinerConfig {
12,344,067✔
442
        let Some(path) = &self.config_path else {
12,344,067✔
443
            return self.miner.clone();
12,297,933✔
444
        };
445
        let Ok(config_file) = ConfigFile::from_path(path.as_str()) else {
46,134✔
446
            return self.miner.clone();
45,594✔
447
        };
448
        let Ok(config) = Config::from_config_file(config_file, false) else {
540✔
449
            return self.miner.clone();
×
450
        };
451
        config.miner
540✔
452
    }
12,344,067✔
453

454
    pub fn get_node_config(&self, resolve_bootstrap_nodes: bool) -> NodeConfig {
3,840,012✔
455
        let Some(path) = &self.config_path else {
3,840,012✔
456
            return self.node.clone();
3,807,090✔
457
        };
458
        let Ok(config_file) = ConfigFile::from_path(path.as_str()) else {
32,922✔
459
            return self.node.clone();
32,436✔
460
        };
461
        let Ok(config) = Config::from_config_file(config_file, resolve_bootstrap_nodes) else {
486✔
462
            return self.node.clone();
×
463
        };
464
        config.node
486✔
465
    }
3,840,012✔
466

467
    /// Apply any test settings to this burnchain config struct
468
    #[cfg_attr(test, mutants::skip)]
469
    fn apply_test_settings(&self, burnchain: &mut Burnchain) {
8,806,752✔
470
        if self.burnchain.get_bitcoin_network().1 == BitcoinNetworkType::Mainnet {
8,806,752✔
471
            return;
×
472
        }
8,806,752✔
473

474
        if let Some(first_burn_block_height) = self.burnchain.first_burn_block_height {
8,806,752✔
475
            debug!(
×
476
                "Override first_block_height from {} to {first_burn_block_height}",
477
                burnchain.first_block_height
478
            );
479
            burnchain.first_block_height = first_burn_block_height;
×
480
        }
8,806,752✔
481

482
        if let Some(first_burn_block_timestamp) = self.burnchain.first_burn_block_timestamp {
8,806,752✔
483
            debug!(
×
484
                "Override first_block_timestamp from {} to {first_burn_block_timestamp}",
485
                burnchain.first_block_timestamp
486
            );
487
            burnchain.first_block_timestamp = first_burn_block_timestamp;
×
488
        }
8,806,752✔
489

490
        if let Some(first_burn_block_hash) = &self.burnchain.first_burn_block_hash {
8,806,752✔
491
            debug!(
×
492
                "Override first_burn_block_hash from {} to {first_burn_block_hash}",
493
                burnchain.first_block_hash
494
            );
495
            burnchain.first_block_hash = BurnchainHeaderHash::from_hex(first_burn_block_hash)
×
496
                .expect("Invalid first_burn_block_hash");
×
497
        }
8,806,752✔
498

499
        if let Some(pox_prepare_length) = self.burnchain.pox_prepare_length {
8,806,752✔
500
            debug!("Override pox_prepare_length to {pox_prepare_length}");
7,537,806✔
501
            burnchain.pox_constants.prepare_length = pox_prepare_length;
7,537,806✔
502
        }
1,268,946✔
503

504
        if let Some(pox_reward_length) = self.burnchain.pox_reward_length {
8,806,752✔
505
            debug!("Override pox_reward_length to {pox_reward_length}");
7,566,165✔
506
            burnchain.pox_constants.reward_cycle_length = pox_reward_length;
7,566,165✔
507
        }
1,240,587✔
508

509
        if let Some(v1_unlock_height) = self.burnchain.pox_2_activation {
8,806,752✔
510
            debug!(
96,678✔
511
                "Override v1_unlock_height from {} to {v1_unlock_height}",
512
                burnchain.pox_constants.v1_unlock_height
513
            );
514
            burnchain.pox_constants.v1_unlock_height = v1_unlock_height;
96,678✔
515
        }
8,710,074✔
516

517
        if let Some(epochs) = &self.burnchain.epochs {
8,806,752✔
518
            if let Some(epoch) = epochs.get(StacksEpochId::Epoch10) {
8,806,536✔
519
                // Epoch 1.0 start height can be equal to the first block height iff epoch 2.0
520
                // start height is also equal to the first block height.
521
                assert!(
8,706,960✔
522
                    epoch.start_height <= burnchain.first_block_height,
8,706,960✔
523
                    "FATAL: Epoch 1.0 start height must be at or before the first block height"
524
                );
525
            }
99,576✔
526

527
            if let Some(epoch) = epochs.get(StacksEpochId::Epoch20) {
8,806,536✔
528
                assert_eq!(
8,806,536✔
529
                    epoch.start_height, burnchain.first_block_height,
530
                    "FATAL: Epoch 2.0 start height must match the first block height"
531
                );
532
            }
×
533

534
            if let Some(epoch) = epochs.get(StacksEpochId::Epoch21) {
8,806,536✔
535
                // Override v1_unlock_height to the start_height of epoch2.1
536
                debug!(
8,806,536✔
537
                    "Override v2_unlock_height from {} to {}",
538
                    burnchain.pox_constants.v1_unlock_height,
539
                    epoch.start_height + 1
×
540
                );
541
                burnchain.pox_constants.v1_unlock_height = epoch.start_height as u32 + 1;
8,806,536✔
542
            }
×
543

544
            if let Some(epoch) = epochs.get(StacksEpochId::Epoch22) {
8,806,536✔
545
                // Override v2_unlock_height to the start_height of epoch2.2
546
                debug!(
7,558,704✔
547
                    "Override v2_unlock_height from {} to {}",
548
                    burnchain.pox_constants.v2_unlock_height,
549
                    epoch.start_height + 1
×
550
                );
551
                burnchain.pox_constants.v2_unlock_height = epoch.start_height as u32 + 1;
7,558,704✔
552
            }
1,247,832✔
553

554
            if let Some(epoch) = epochs.get(StacksEpochId::Epoch24) {
8,806,536✔
555
                // Override pox_3_activation_height to the start_height of epoch2.4
556
                debug!(
7,558,704✔
557
                    "Override pox_3_activation_height from {} to {}",
558
                    burnchain.pox_constants.pox_3_activation_height, epoch.start_height
559
                );
560
                burnchain.pox_constants.pox_3_activation_height = epoch.start_height as u32;
7,558,704✔
561
            }
1,247,832✔
562

563
            if let Some(epoch) = epochs.get(StacksEpochId::Epoch25) {
8,806,536✔
564
                // Override pox_4_activation_height to the start_height of epoch2.5
565
                debug!(
7,558,704✔
566
                    "Override pox_4_activation_height from {} to {}",
567
                    burnchain.pox_constants.pox_4_activation_height, epoch.start_height
568
                );
569
                burnchain.pox_constants.pox_4_activation_height = epoch.start_height as u32;
7,558,704✔
570
                burnchain.pox_constants.v3_unlock_height = epoch.start_height as u32 + 1;
7,558,704✔
571
            }
1,247,832✔
572
        }
216✔
573

574
        if let Some(sunset_start) = self.burnchain.sunset_start {
8,806,752✔
575
            debug!(
×
576
                "Override sunset_start from {} to {sunset_start}",
577
                burnchain.pox_constants.sunset_start
578
            );
579
            burnchain.pox_constants.sunset_start = sunset_start.into();
×
580
        }
8,806,752✔
581

582
        if let Some(sunset_end) = self.burnchain.sunset_end {
8,806,752✔
583
            debug!(
×
584
                "Override sunset_end from {} to {sunset_end}",
585
                burnchain.pox_constants.sunset_end
586
            );
587
            burnchain.pox_constants.sunset_end = sunset_end.into();
×
588
        }
8,806,752✔
589

590
        // check if the Epoch 3.0 burnchain settings as configured are going to be valid.
591
        self.check_nakamoto_config(burnchain);
8,806,752✔
592
    }
8,806,752✔
593

594
    fn check_nakamoto_config(&self, burnchain: &Burnchain) {
8,806,752✔
595
        let epochs = self.burnchain.get_epoch_list();
8,806,752✔
596
        if epochs
8,806,752✔
597
            .iter()
8,806,752✔
598
            .all(|epoch| epoch.epoch_id < StacksEpochId::Epoch30)
72,901,134✔
599
        {
600
            return;
1,268,730✔
601
        }
7,538,022✔
602
        if burnchain.pox_constants.prepare_length < 3 {
7,538,022✔
603
            panic!(
×
604
                "FATAL: Nakamoto rules require a prepare length >= 3. Prepare length set to {}",
605
                burnchain.pox_constants.prepare_length
606
            );
607
        }
7,538,022✔
608
        StacksEpoch::validate_nakamoto_transition_schedule(&epochs, burnchain);
7,538,022✔
609
    }
8,806,752✔
610

611
    /// Connect to the MempoolDB using the configured cost estimation
612
    pub fn connect_mempool_db(&self) -> Result<MemPoolDB, DBError> {
154,890✔
613
        // create estimators, metric instances for RPC handler
614
        let cost_estimator = self
154,890✔
615
            .make_cost_estimator()
154,890✔
616
            .unwrap_or_else(|| Box::new(UnitEstimator));
154,890✔
617
        let metric = self
154,890✔
618
            .make_cost_metric()
154,890✔
619
            .unwrap_or_else(|| Box::new(UnitMetric));
154,890✔
620

621
        MemPoolDB::open(
154,890✔
622
            self.is_mainnet(),
154,890✔
623
            self.burnchain.chain_id,
154,890✔
624
            &self.get_chainstate_path_str(),
154,890✔
625
            cost_estimator,
154,890✔
626
            metric,
154,890✔
627
        )
628
    }
154,890✔
629

630
    /// Load up a Burnchain and apply config settings to it.
631
    /// Use this over the Burnchain constructors.
632
    /// Panics if we are unable to instantiate a burnchain (e.g. becase we're using an unrecognized
633
    /// chain ID or something).
634
    pub fn get_burnchain(&self) -> Burnchain {
8,806,752✔
635
        let (network_name, _) = self.burnchain.get_bitcoin_network();
8,806,752✔
636
        let mut burnchain = {
8,806,752✔
637
            let working_dir = self.get_burn_db_path();
8,806,752✔
638
            match Burnchain::new(
8,806,752✔
639
                &working_dir,
8,806,752✔
640
                &self.burnchain.chain,
8,806,752✔
641
                &network_name,
8,806,752✔
642
                Some(self.node.get_marf_opts()),
8,806,752✔
643
            ) {
8,806,752✔
644
                Ok(burnchain) => burnchain,
8,806,752✔
645
                Err(e) => {
×
646
                    error!("Failed to instantiate burnchain: {e}");
×
647
                    panic!()
×
648
                }
649
            }
650
        };
651
        self.apply_test_settings(&mut burnchain);
8,806,752✔
652
        burnchain
8,806,752✔
653
    }
8,806,752✔
654

655
    /// Assert that a burnchain's PoX constants are consistent with the list of epoch start and end
656
    /// heights.  Panics if this is not the case.
657
    pub fn assert_valid_epoch_settings(burnchain: &Burnchain, epochs: &[StacksEpoch]) {
4,896✔
658
        // sanity check: epochs must be contiguous and ordered
659
        // (this panics if it's not the case)
660
        test_debug!("Validate epochs: {:#?}", epochs);
4,896✔
661
        let validated = StacksEpoch::validate_epochs(epochs);
4,896✔
662

663
        // sanity check: v1_unlock_height must happen after pox-2 instantiation
664
        let epoch21 = validated
4,896✔
665
            .get(StacksEpochId::Epoch21)
4,896✔
666
            .expect("FATAL: no epoch 2.1 defined");
4,896✔
667
        let v1_unlock_height = burnchain.pox_constants.v1_unlock_height as u64;
4,896✔
668

669
        assert!(
4,896✔
670
            v1_unlock_height > epoch21.start_height,
4,896✔
671
            "FATAL: v1 unlock height occurs at or before pox-2 activation: {v1_unlock_height} <= {}\nburnchain: {burnchain:?}", epoch21.start_height
672
        );
673

674
        let epoch21_rc = burnchain
4,896✔
675
            .block_height_to_reward_cycle(epoch21.start_height)
4,896✔
676
            .expect("FATAL: epoch 21 starts before the first burnchain block");
4,896✔
677
        let v1_unlock_rc = burnchain
4,896✔
678
            .block_height_to_reward_cycle(v1_unlock_height)
4,896✔
679
            .expect("FATAL: v1 unlock height is before the first burnchain block");
4,896✔
680

681
        if epoch21_rc + 1 == v1_unlock_rc {
4,896✔
682
            // if v1_unlock_height is in the reward cycle after epoch_21, then it must not fall on
683
            // the reward cycle boundary.
684
            assert!(
270✔
685
                !burnchain.is_reward_cycle_start(v1_unlock_height),
270✔
686
                "FATAL: v1 unlock height is at a reward cycle boundary\nburnchain: {burnchain:?}"
687
            );
688
        }
4,626✔
689
        StacksEpoch::validate_nakamoto_transition_schedule(epochs, burnchain);
4,896✔
690
    }
4,896✔
691

692
    // TODO: add tests from mutation testing results #4866
693
    #[cfg_attr(test, mutants::skip)]
694
    fn make_epochs(
×
695
        conf_epochs: &[StacksEpochConfigFile],
×
696
        burn_mode: &str,
×
697
        bitcoin_network: BitcoinNetworkType,
×
698
        pox_2_activation: Option<u32>,
×
699
    ) -> Result<EpochList<ExecutionCost>, String> {
×
700
        let default_epochs = match bitcoin_network {
×
701
            BitcoinNetworkType::Mainnet => {
702
                Err("Cannot configure epochs in mainnet mode".to_string())
×
703
            }
704
            BitcoinNetworkType::Testnet => Ok(STACKS_EPOCHS_TESTNET.clone().to_vec()),
×
705
            BitcoinNetworkType::Regtest => Ok(STACKS_EPOCHS_REGTEST.clone().to_vec()),
×
706
        }?;
×
707
        let mut matched_epochs = vec![];
×
708
        for configured_epoch in conf_epochs.iter() {
×
709
            let epoch_name = &configured_epoch.epoch_name;
×
710
            let epoch_id = if epoch_name == EPOCH_CONFIG_1_0_0 {
×
711
                Ok(StacksEpochId::Epoch10)
×
712
            } else if epoch_name == EPOCH_CONFIG_2_0_0 {
×
713
                Ok(StacksEpochId::Epoch20)
×
714
            } else if epoch_name == EPOCH_CONFIG_2_0_5 {
×
715
                Ok(StacksEpochId::Epoch2_05)
×
716
            } else if epoch_name == EPOCH_CONFIG_2_1_0 {
×
717
                Ok(StacksEpochId::Epoch21)
×
718
            } else if epoch_name == EPOCH_CONFIG_2_2_0 {
×
719
                Ok(StacksEpochId::Epoch22)
×
720
            } else if epoch_name == EPOCH_CONFIG_2_3_0 {
×
721
                Ok(StacksEpochId::Epoch23)
×
722
            } else if epoch_name == EPOCH_CONFIG_2_4_0 {
×
723
                Ok(StacksEpochId::Epoch24)
×
724
            } else if epoch_name == EPOCH_CONFIG_2_5_0 {
×
725
                Ok(StacksEpochId::Epoch25)
×
726
            } else if epoch_name == EPOCH_CONFIG_3_0_0 {
×
727
                Ok(StacksEpochId::Epoch30)
×
728
            } else if epoch_name == EPOCH_CONFIG_3_1_0 {
×
729
                Ok(StacksEpochId::Epoch31)
×
730
            } else if epoch_name == EPOCH_CONFIG_3_2_0 {
×
731
                Ok(StacksEpochId::Epoch32)
×
732
            } else if epoch_name == EPOCH_CONFIG_3_3_0 {
×
733
                Ok(StacksEpochId::Epoch33)
×
734
            } else if epoch_name == EPOCH_CONFIG_3_4_0 {
×
735
                Ok(StacksEpochId::Epoch34)
×
736
            } else {
737
                Err(format!("Unknown epoch name specified: {epoch_name}"))
×
738
            }?;
×
739
            matched_epochs.push((epoch_id, configured_epoch.start_height));
×
740
        }
741

742
        matched_epochs.sort_by_key(|(epoch_id, _)| *epoch_id);
×
743
        // epochs must be sorted the same both by start height and by epoch
744
        let mut check_sort = matched_epochs.clone();
×
745
        check_sort.sort_by_key(|(_, start)| *start);
×
746
        if matched_epochs != check_sort {
×
747
            return Err(
×
748
                "Configured epochs must have start heights in the correct epoch order".to_string(),
×
749
            );
×
750
        }
×
751

752
        let expected_list = [
×
753
            StacksEpochId::Epoch10,
×
754
            StacksEpochId::Epoch20,
×
755
            StacksEpochId::Epoch2_05,
×
756
            StacksEpochId::Epoch21,
×
757
            StacksEpochId::Epoch22,
×
758
            StacksEpochId::Epoch23,
×
759
            StacksEpochId::Epoch24,
×
760
            StacksEpochId::Epoch25,
×
761
            StacksEpochId::Epoch30,
×
762
            StacksEpochId::Epoch31,
×
763
            StacksEpochId::Epoch32,
×
764
            StacksEpochId::Epoch33,
×
765
            StacksEpochId::Epoch34,
×
766
        ];
×
767
        for (expected_epoch, configured_epoch) in expected_list
×
768
            .iter()
×
769
            .zip(matched_epochs.iter().map(|(epoch_id, _)| epoch_id))
×
770
        {
771
            if expected_epoch != configured_epoch {
×
772
                return Err(format!("Configured epochs may not skip an epoch. Expected epoch = {expected_epoch}, Found epoch = {configured_epoch}"));
×
773
            }
×
774
        }
775

776
        // Stacks 1.0 must start at 0
777
        if matched_epochs
×
778
            .first()
×
779
            .ok_or_else(|| "Must configure at least 1 epoch")?
×
780
            .1
781
            != 0
782
        {
783
            return Err("Stacks 1.0 must start at height = 0".into());
×
784
        }
×
785

786
        let mut out_epochs = default_epochs
×
787
            .get(..matched_epochs.len())
×
788
            .ok_or_else(|| {
×
789
                format!(
×
790
                "Cannot configure more epochs than support by this node. Supported epoch count: {}",
791
                default_epochs.len()
×
792
            )
793
            })?
×
794
            .to_vec();
×
795

796
        for (i, ((epoch_id, start_height), out_epoch)) in
×
797
            matched_epochs.iter().zip(out_epochs.iter_mut()).enumerate()
×
798
        {
799
            if epoch_id != &out_epoch.epoch_id {
×
800
                return Err(
×
801
                    format!("Unmatched epochs in configuration and node implementation. Implemented = {epoch_id}, Configured = {}",
×
802
                            &out_epoch.epoch_id));
×
803
            }
×
804
            // end_height = next epoch's start height || i64::max if last epoch
805
            let end_height = if let Some(next_epoch) = matched_epochs.get(i + 1) {
×
806
                next_epoch.1
×
807
            } else {
808
                i64::MAX
×
809
            };
810
            out_epoch.start_height = u64::try_from(*start_height)
×
811
                .map_err(|_| "Start height must be a non-negative integer")?;
×
812
            out_epoch.end_height = u64::try_from(end_height)
×
813
                .map_err(|_| "End height must be a non-negative integer")?;
×
814
        }
815

816
        if burn_mode == "mocknet" {
×
817
            for epoch in out_epochs.iter_mut() {
×
818
                epoch.block_limit = ExecutionCost::max_value();
×
819
            }
×
820
        }
×
821

822
        if let Some(pox_2_activation) = pox_2_activation {
×
823
            let last_epoch = out_epochs
×
824
                .iter()
×
825
                .find(|&e| e.epoch_id == StacksEpochId::Epoch21)
×
826
                .ok_or("Cannot configure pox_2_activation if epoch 2.1 is not configured")?;
×
827
            if last_epoch.start_height > pox_2_activation as u64 {
×
828
                Err(format!("Cannot configure pox_2_activation at a lower height than the Epoch 2.1 start height. pox_2_activation = {pox_2_activation}, epoch 2.1 start height = {}", last_epoch.start_height))?;
×
829
            }
×
830
        }
×
831

832
        Ok(EpochList::new(&out_epochs))
×
833
    }
×
834

835
    pub fn from_config_file(
1,825✔
836
        config_file: ConfigFile,
1,825✔
837
        resolve_bootstrap_nodes: bool,
1,825✔
838
    ) -> Result<Config, String> {
1,825✔
839
        Self::from_config_default(config_file, Config::default(), resolve_bootstrap_nodes)
1,825✔
840
    }
1,825✔
841

842
    fn from_config_default(
1,825✔
843
        config_file: ConfigFile,
1,825✔
844
        default: Config,
1,825✔
845
        resolve_bootstrap_nodes: bool,
1,825✔
846
    ) -> Result<Config, String> {
1,825✔
847
        let Config {
848
            node: default_node_config,
1,825✔
849
            burnchain: default_burnchain_config,
1,825✔
850
            miner: miner_default_config,
1,825✔
851
            estimation: default_estimator,
1,825✔
852
            ..
853
        } = default;
1,825✔
854

855
        // First parse the burnchain config
856
        let burnchain = match config_file.burnchain {
1,825✔
857
            Some(burnchain) => burnchain.into_config_default(default_burnchain_config)?,
1,819✔
858
            None => default_burnchain_config,
6✔
859
        };
860

861
        let supported_modes = [
1,824✔
862
            "mocknet",
1,824✔
863
            "helium",
1,824✔
864
            "neon",
1,824✔
865
            "argon",
1,824✔
866
            "krypton",
1,824✔
867
            "xenon",
1,824✔
868
            "mainnet",
1,824✔
869
            "nakamoto-neon",
1,824✔
870
        ];
1,824✔
871

872
        if !supported_modes.contains(&burnchain.mode.as_str()) {
1,824✔
873
            return Err(format!(
×
874
                "Setting burnchain.network not supported (should be: {})",
×
875
                supported_modes.join(", ")
×
876
            ));
×
877
        }
1,824✔
878

879
        if burnchain.mode == "helium" && burnchain.local_mining_public_key.is_none() {
1,824✔
880
            return Err("Config is missing the setting `burnchain.local_mining_public_key` (mandatory for helium)".into());
×
881
        }
1,824✔
882

883
        let is_mainnet = burnchain.mode == "mainnet";
1,824✔
884

885
        // Parse the node config
886
        let (mut node, bootstrap_node, deny_nodes) = match config_file.node {
1,824✔
887
            Some(node) => {
337✔
888
                let deny_nodes = node.deny_nodes.clone();
337✔
889
                let bootstrap_node = node.bootstrap_node.clone();
337✔
890
                let node_config = node.into_config_default(default_node_config)?;
337✔
891
                (node_config, bootstrap_node, deny_nodes)
335✔
892
            }
893
            None => (default_node_config, None, None),
1,487✔
894
        };
895

896
        if let Some(bootstrap_node) = bootstrap_node {
1,822✔
897
            if resolve_bootstrap_nodes {
9✔
898
                node.set_bootstrap_nodes(
×
899
                    bootstrap_node,
×
900
                    burnchain.chain_id,
×
901
                    burnchain.peer_version,
×
902
                );
×
903
            }
9✔
904
        } else if is_mainnet && resolve_bootstrap_nodes {
1,813✔
905
            let bootstrap_node = ConfigFile::mainnet().node.unwrap().bootstrap_node.unwrap();
×
906
            node.set_bootstrap_nodes(bootstrap_node, burnchain.chain_id, burnchain.peer_version);
×
907
        }
1,813✔
908
        if let Some(deny_nodes) = deny_nodes {
1,822✔
909
            node.set_deny_nodes(deny_nodes, burnchain.chain_id, burnchain.peer_version);
×
910
        }
1,822✔
911

912
        // Validate the node config
913
        if is_mainnet && node.use_test_genesis_chainstate == Some(true) {
1,822✔
914
            return Err("Attempted to run mainnet node with `use_test_genesis_chainstate`".into());
×
915
        }
1,822✔
916

917
        if node.stacker || node.miner {
1,822✔
918
            node.add_miner_stackerdb(is_mainnet);
×
919
            node.add_signers_stackerdbs(is_mainnet);
×
920
        }
1,822✔
921

922
        let miner = match config_file.miner {
1,822✔
923
            Some(mut miner) => {
×
924
                if miner.mining_key.is_none() && !node.seed.is_empty() {
×
925
                    miner.mining_key = Some(to_hex(&node.seed));
×
926
                }
×
927
                miner.into_config_default(miner_default_config)?
×
928
            }
929
            None => miner_default_config,
1,822✔
930
        };
931

932
        if is_mainnet && miner.replay_transactions {
1,822✔
933
            return Err("Attempted to run mainnet node with `replay_transactions` set to true. This feature is still incomplete and may not be enabled on a mainnet node".into());
×
934
        }
1,822✔
935
        let initial_balances: Vec<InitialBalance> = match config_file.ustx_balance {
1,822✔
936
            Some(balances) => {
324✔
937
                if is_mainnet && !balances.is_empty() {
324✔
938
                    return Err(
×
939
                        "Attempted to run mainnet node with specified `initial_balances`".into(),
×
940
                    );
×
941
                }
324✔
942
                balances
324✔
943
                    .iter()
324✔
944
                    .map(|balance| {
1,296✔
945
                        let address: PrincipalData =
1,296✔
946
                            PrincipalData::parse_standard_principal(&balance.address)
1,296✔
947
                                .unwrap()
1,296✔
948
                                .into();
1,296✔
949
                        InitialBalance {
1,296✔
950
                            address,
1,296✔
951
                            amount: balance.amount,
1,296✔
952
                        }
1,296✔
953
                    })
1,296✔
954
                    .collect()
324✔
955
            }
956
            None => vec![],
1,498✔
957
        };
958

959
        let mut events_observers = match config_file.events_observer {
1,822✔
960
            Some(raw_observers) => {
×
961
                let mut observers = HashSet::new();
×
962
                for observer in raw_observers {
×
963
                    let events_keys: Vec<EventKeyType> = observer
×
964
                        .events_keys
×
965
                        .iter()
×
966
                        .map(|e| EventKeyType::from_string(e).unwrap())
×
967
                        .collect();
×
968

969
                    observers.insert(EventObserverConfig {
×
970
                        endpoint: observer.endpoint,
×
971
                        events_keys,
×
972
                        timeout_ms: observer.timeout_ms.unwrap_or(1_000),
×
973
                        disable_retries: observer.disable_retries.unwrap_or(false),
×
974
                    });
×
975
                }
976
                observers
×
977
            }
978
            None => HashSet::new(),
1,822✔
979
        };
980

981
        // check for observer config in env vars
982
        if let Ok(val) = std::env::var("STACKS_EVENT_OBSERVER") {
1,822✔
983
            events_observers.insert(EventObserverConfig {
×
984
                endpoint: val,
×
985
                events_keys: vec![EventKeyType::AnyEvent],
×
986
                timeout_ms: 1_000,
×
987
                disable_retries: false,
×
988
            });
×
989
        };
1,822✔
990

991
        let connection_options = match config_file.connection_options {
1,822✔
992
            Some(opts) => opts.into_config(is_mainnet)?,
1✔
993
            None => HELIUM_DEFAULT_CONNECTION_OPTIONS.clone(),
1,821✔
994
        };
995

996
        let estimation = match config_file.fee_estimation {
1,822✔
997
            Some(f) => FeeEstimationConfig::from(f),
×
998
            None => default_estimator,
1,822✔
999
        };
1000

1001
        let atlas = match config_file.atlas {
1,822✔
1002
            Some(f) => f.into_config(is_mainnet),
×
1003
            None => AtlasConfig::new(is_mainnet),
1,822✔
1004
        };
1005

1006
        atlas
1,822✔
1007
            .validate()
1,822✔
1008
            .map_err(|e| format!("Atlas config error: {e}"))?;
1,822✔
1009

1010
        if miner.mining_key.is_none() && miner.pre_nakamoto_mock_signing {
1,822✔
1011
            return Err("Cannot use pre_nakamoto_mock_signing without a mining_key".to_string());
×
1012
        }
1,822✔
1013

1014
        Ok(Config {
1,822✔
1015
            config_path: config_file.__path,
1,822✔
1016
            node,
1,822✔
1017
            burnchain,
1,822✔
1018
            initial_balances,
1,822✔
1019
            events_observers,
1,822✔
1020
            connection_options,
1,822✔
1021
            estimation,
1,822✔
1022
            miner,
1,822✔
1023
            atlas,
1,822✔
1024
        })
1,822✔
1025
    }
1,825✔
1026

1027
    /// Returns the path working directory path, and ensures it exists.
1028
    pub fn get_working_dir(&self) -> PathBuf {
2,655✔
1029
        let path = PathBuf::from(&self.node.working_dir);
2,655✔
1030
        fs::create_dir_all(&path).unwrap_or_else(|_| {
2,655✔
1031
            panic!(
×
1032
                "Failed to create working directory at {}",
1033
                path.to_string_lossy()
×
1034
            )
1035
        });
1036
        path
2,655✔
1037
    }
2,655✔
1038

1039
    fn get_burnchain_path(&self) -> PathBuf {
19,785,204✔
1040
        let mut path = PathBuf::from(&self.node.working_dir);
19,785,204✔
1041
        path.push(&self.burnchain.mode);
19,785,204✔
1042
        path.push("burnchain");
19,785,204✔
1043
        path
19,785,204✔
1044
    }
19,785,204✔
1045

1046
    pub fn get_chainstate_path(&self) -> PathBuf {
7,111,701✔
1047
        let mut path = PathBuf::from(&self.node.working_dir);
7,111,701✔
1048
        path.push(&self.burnchain.mode);
7,111,701✔
1049
        path.push("chainstate");
7,111,701✔
1050
        path
7,111,701✔
1051
    }
7,111,701✔
1052

1053
    /// Returns the path `{get_chainstate_path()}/estimates`, and ensures it exists.
1054
    pub fn get_estimates_path(&self) -> PathBuf {
2,312,865✔
1055
        let mut path = self.get_chainstate_path();
2,312,865✔
1056
        path.push("estimates");
2,312,865✔
1057
        fs::create_dir_all(&path).unwrap_or_else(|_| {
2,312,865✔
1058
            panic!(
×
1059
                "Failed to create `estimates` directory at {}",
1060
                path.to_string_lossy()
×
1061
            )
1062
        });
1063
        path
2,312,865✔
1064
    }
2,312,865✔
1065

1066
    pub fn get_chainstate_path_str(&self) -> String {
4,695,336✔
1067
        self.get_chainstate_path()
4,695,336✔
1068
            .to_str()
4,695,336✔
1069
            .expect("Unable to produce path")
4,695,336✔
1070
            .to_string()
4,695,336✔
1071
    }
4,695,336✔
1072

1073
    pub fn get_burnchain_path_str(&self) -> String {
9,747✔
1074
        self.get_burnchain_path()
9,747✔
1075
            .to_str()
9,747✔
1076
            .expect("Unable to produce path")
9,747✔
1077
            .to_string()
9,747✔
1078
    }
9,747✔
1079

1080
    pub fn get_burn_db_path(&self) -> String {
8,806,851✔
1081
        self.get_burnchain_path()
8,806,851✔
1082
            .to_str()
8,806,851✔
1083
            .expect("Unable to produce path")
8,806,851✔
1084
            .to_string()
8,806,851✔
1085
    }
8,806,851✔
1086

1087
    pub fn get_burn_db_file_path(&self) -> String {
4,850,352✔
1088
        let mut path = self.get_burnchain_path();
4,850,352✔
1089
        path.push("sortition");
4,850,352✔
1090
        path.to_str().expect("Unable to produce path").to_string()
4,850,352✔
1091
    }
4,850,352✔
1092

1093
    pub fn get_spv_headers_file_path(&self) -> String {
6,118,254✔
1094
        let mut path = self.get_burnchain_path();
6,118,254✔
1095
        path.set_file_name("headers.sqlite");
6,118,254✔
1096
        path.to_str().expect("Unable to produce path").to_string()
6,118,254✔
1097
    }
6,118,254✔
1098

1099
    pub fn get_peer_db_file_path(&self) -> String {
2,637✔
1100
        let mut path = self.get_chainstate_path();
2,637✔
1101
        path.set_file_name("peer.sqlite");
2,637✔
1102
        path.to_str().expect("Unable to produce path").to_string()
2,637✔
1103
    }
2,637✔
1104

1105
    pub fn get_atlas_db_file_path(&self) -> String {
7,731✔
1106
        let mut path = self.get_chainstate_path();
7,731✔
1107
        path.set_file_name("atlas.sqlite");
7,731✔
1108
        path.to_str().expect("Unable to produce path").to_string()
7,731✔
1109
    }
7,731✔
1110

1111
    pub fn get_stacker_db_file_path(&self) -> String {
80,091✔
1112
        let mut path = self.get_chainstate_path();
80,091✔
1113
        path.set_file_name("stacker_db.sqlite");
80,091✔
1114
        path.to_str().expect("Unable to produce path").to_string()
80,091✔
1115
    }
80,091✔
1116

1117
    pub fn add_initial_balance(&mut self, address: String, amount: u64) {
12,906✔
1118
        let new_balance = InitialBalance {
12,906✔
1119
            address: PrincipalData::parse(&address).unwrap().into(),
12,906✔
1120
            amount,
12,906✔
1121
        };
12,906✔
1122
        self.initial_balances.push(new_balance);
12,906✔
1123
    }
12,906✔
1124

1125
    pub fn get_initial_liquid_ustx(&self) -> u128 {
×
1126
        let mut total = 0;
×
1127
        for ib in self.initial_balances.iter() {
×
1128
            total += ib.amount as u128
×
1129
        }
1130
        total
×
1131
    }
×
1132

1133
    pub fn is_mainnet(&self) -> bool {
7,385,004✔
1134
        matches!(self.burnchain.mode.as_str(), "mainnet")
7,385,004✔
1135
    }
7,385,004✔
1136

1137
    pub fn is_node_event_driven(&self) -> bool {
×
1138
        !self.events_observers.is_empty()
×
1139
    }
×
1140

1141
    pub fn make_nakamoto_block_builder_settings(
65,349✔
1142
        &self,
65,349✔
1143
        miner_status: Arc<Mutex<MinerStatus>>,
65,349✔
1144
    ) -> BlockBuilderSettings {
65,349✔
1145
        let miner_config = self.get_miner_config();
65,349✔
1146
        BlockBuilderSettings {
65,349✔
1147
            max_miner_time_ms: miner_config.nakamoto_attempt_time_ms,
65,349✔
1148
            mempool_settings: MemPoolWalkSettings {
65,349✔
1149
                strategy: miner_config.mempool_walk_strategy,
65,349✔
1150
                max_walk_time_ms: miner_config.nakamoto_attempt_time_ms,
65,349✔
1151
                consider_no_estimate_tx_prob: miner_config.probability_pick_no_estimate_tx,
65,349✔
1152
                nonce_cache_size: miner_config.nonce_cache_size,
65,349✔
1153
                candidate_retry_cache_size: miner_config.candidate_retry_cache_size,
65,349✔
1154
                txs_to_consider: miner_config.txs_to_consider,
65,349✔
1155
                filter_origins: miner_config.filter_origins,
65,349✔
1156
                tenure_cost_limit_per_block_percentage: miner_config
65,349✔
1157
                    .tenure_cost_limit_per_block_percentage,
65,349✔
1158
                contract_cost_limit_percentage: miner_config.contract_cost_limit_percentage,
65,349✔
1159
                log_skipped_transactions: miner_config.log_skipped_transactions,
65,349✔
1160
            },
65,349✔
1161
            miner_status,
65,349✔
1162
            confirm_microblocks: false,
65,349✔
1163
            max_execution_time: Some(Duration::from_secs(miner_config.max_execution_time_secs)),
65,349✔
1164
            max_tenure_bytes: miner_config.max_tenure_bytes,
65,349✔
1165
            temporarily_excluded_txids: HashSet::new(),
65,349✔
1166
            max_assembly_mem_bytes: miner_config.max_assembly_mem_bytes,
65,349✔
1167
        }
65,349✔
1168
    }
65,349✔
1169

1170
    // TODO: add tests from mutation testing results #4867
1171
    #[cfg_attr(test, mutants::skip)]
1172
    pub fn make_block_builder_settings(
332,001✔
1173
        &self,
332,001✔
1174
        attempt: u64,
332,001✔
1175
        microblocks: bool,
332,001✔
1176
        miner_status: Arc<Mutex<MinerStatus>>,
332,001✔
1177
    ) -> BlockBuilderSettings {
332,001✔
1178
        let miner_config = self.get_miner_config();
332,001✔
1179
        BlockBuilderSettings {
1180
            max_miner_time_ms: if microblocks {
332,001✔
1181
                miner_config.microblock_attempt_time_ms
97,272✔
1182
            } else if attempt <= 1 {
234,729✔
1183
                // first attempt to mine a block -- do so right away
1184
                miner_config.first_attempt_time_ms
89,532✔
1185
            } else {
1186
                // second or later attempt to mine a block -- give it some time
1187
                miner_config.subsequent_attempt_time_ms
145,197✔
1188
            },
1189
            mempool_settings: MemPoolWalkSettings {
1190
                max_walk_time_ms: if microblocks {
332,001✔
1191
                    miner_config.microblock_attempt_time_ms
97,272✔
1192
                } else if attempt <= 1 {
234,729✔
1193
                    // first attempt to mine a block -- do so right away
1194
                    miner_config.first_attempt_time_ms
89,532✔
1195
                } else {
1196
                    // second or later attempt to mine a block -- give it some time
1197
                    miner_config.subsequent_attempt_time_ms
145,197✔
1198
                },
1199
                strategy: miner_config.mempool_walk_strategy,
332,001✔
1200
                consider_no_estimate_tx_prob: miner_config.probability_pick_no_estimate_tx,
332,001✔
1201
                nonce_cache_size: miner_config.nonce_cache_size,
332,001✔
1202
                candidate_retry_cache_size: miner_config.candidate_retry_cache_size,
332,001✔
1203
                txs_to_consider: miner_config.txs_to_consider,
332,001✔
1204
                filter_origins: miner_config.filter_origins,
332,001✔
1205
                tenure_cost_limit_per_block_percentage: miner_config
332,001✔
1206
                    .tenure_cost_limit_per_block_percentage,
332,001✔
1207
                contract_cost_limit_percentage: miner_config.contract_cost_limit_percentage,
332,001✔
1208
                log_skipped_transactions: miner_config.log_skipped_transactions,
332,001✔
1209
            },
1210
            miner_status,
332,001✔
1211
            confirm_microblocks: true,
1212
            max_execution_time: Some(Duration::from_secs(miner_config.max_execution_time_secs)),
332,001✔
1213
            max_tenure_bytes: miner_config.max_tenure_bytes,
332,001✔
1214
            temporarily_excluded_txids: HashSet::new(),
332,001✔
1215
            max_assembly_mem_bytes: miner_config.max_assembly_mem_bytes,
332,001✔
1216
        }
1217
    }
332,001✔
1218

1219
    pub fn get_miner_stats(&self) -> Option<MinerStats> {
×
1220
        let miner_config = self.get_miner_config();
×
1221
        if let Some(unconfirmed_commits_helper) = miner_config.unconfirmed_commits_helper.as_ref() {
×
1222
            let miner_stats = MinerStats {
×
1223
                unconfirmed_commits_helper: unconfirmed_commits_helper.clone(),
×
1224
            };
×
1225
            return Some(miner_stats);
×
1226
        }
×
1227
        None
×
1228
    }
×
1229

1230
    /// Determine how long the p2p state machine should poll for.
1231
    /// If the node is not mining, then use a default value.
1232
    /// If the node is mining, however, then at the time of this writing, the miner's latency is in
1233
    /// part dependent on the state machine getting block data back to the miner quickly, and thus
1234
    /// the poll time is dependent on the first attempt time.
1235
    pub fn get_poll_time(&self) -> u64 {
4,698✔
1236
        if self.node.miner {
4,698✔
1237
            cmp::min(1000, self.miner.first_attempt_time_ms / 2)
4,617✔
1238
        } else {
1239
            1000
81✔
1240
        }
1241
    }
4,698✔
1242
}
1243

1244
impl std::default::Default for Config {
1245
    fn default() -> Config {
4,453✔
1246
        let node = NodeConfig::default();
4,453✔
1247
        let burnchain = BurnchainConfig::default();
4,453✔
1248

1249
        let connection_options = HELIUM_DEFAULT_CONNECTION_OPTIONS.clone();
4,453✔
1250
        let estimation = FeeEstimationConfig::default();
4,453✔
1251
        let mainnet = burnchain.mode == "mainnet";
4,453✔
1252

1253
        Config {
4,453✔
1254
            config_path: None,
4,453✔
1255
            burnchain,
4,453✔
1256
            node,
4,453✔
1257
            initial_balances: vec![],
4,453✔
1258
            events_observers: HashSet::new(),
4,453✔
1259
            connection_options,
4,453✔
1260
            estimation,
4,453✔
1261
            miner: MinerConfig::default(),
4,453✔
1262
            atlas: AtlasConfig::new(mainnet),
4,453✔
1263
        }
4,453✔
1264
    }
4,453✔
1265
}
1266

1267
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
1268
pub struct BurnchainConfig {
1269
    /// The underlying blockchain used for Proof-of-Transfer.
1270
    /// ---
1271
    /// @default: `"bitcoin"`
1272
    /// @notes:
1273
    ///   - Currently, only `"bitcoin"` is supported.
1274
    pub chain: String,
1275
    /// The operational mode or network profile for the Stacks node.
1276
    /// This setting determines network parameters (like chain ID, peer version),
1277
    /// default configurations, genesis block definitions, and overall node behavior.
1278
    ///
1279
    /// Supported values:
1280
    /// - `"mainnet"`: mainnet
1281
    /// - `"xenon"`: testnet
1282
    /// - `"mocknet"`: regtest
1283
    /// - `"helium"`: regtest
1284
    /// - `"neon"`: regtest
1285
    /// - `"argon"`: regtest
1286
    /// - `"krypton"`: regtest
1287
    /// - `"nakamoto-neon"`: regtest
1288
    /// ---
1289
    /// @default: `"mocknet"`
1290
    pub mode: String,
1291
    /// The network-specific identifier used in P2P communication and database initialization.
1292
    /// ---
1293
    /// @default: |
1294
    ///   - if [`BurnchainConfig::mode`] is `"mainnet"`: [`CHAIN_ID_MAINNET`]
1295
    ///   - else: [`CHAIN_ID_TESTNET`]
1296
    /// @notes:
1297
    ///   - **Warning:** Do not modify this unless you really know what you're doing.
1298
    ///   - This is intended strictly for testing purposes.
1299
    pub chain_id: u32,
1300
    /// The peer protocol version number used in P2P communication.
1301
    /// This parameter cannot be set via the configuration file.
1302
    /// ---
1303
    /// @default: |
1304
    ///   - if [`BurnchainConfig::mode`] is `"mainnet"`: [`PEER_VERSION_MAINNET`]
1305
    ///   - else: [`PEER_VERSION_TESTNET`]
1306
    /// @notes:
1307
    ///   - **Warning:** Do not modify this unless you really know what you're doing.
1308
    pub peer_version: u32,
1309
    /// Specifies a mandatory wait period (in milliseconds) after receiving a burnchain tip
1310
    /// before the node attempts to build the anchored block for the new tenure.
1311
    /// This duration effectively schedules the start of the block-building process
1312
    /// relative to the tip's arrival time.
1313
    /// ---
1314
    /// @default: `5_000`
1315
    /// @units: milliseconds
1316
    /// @notes:
1317
    ///   - This is intended strictly for testing purposes.
1318
    pub commit_anchor_block_within: u64,
1319
    /// The maximum amount (in sats) of "burn commitment" to broadcast for the next
1320
    /// block's leader election. Acts as a safety cap to limit the maximum amount
1321
    /// spent on mining. It serves as both the target fee and a fallback if dynamic
1322
    /// fee calculations fail or cannot be performed.
1323
    ///
1324
    /// This setting can be hot-reloaded from the config file, allowing adjustment
1325
    /// without restarting.
1326
    /// ---
1327
    /// @default: `20_000`
1328
    /// @units: satoshis
1329
    /// @notes:
1330
    ///   - Only relevant if [`NodeConfig::miner`] is `true`.
1331
    pub burn_fee_cap: u64,
1332
    /// The hostname or IP address of the bitcoin node peer.
1333
    ///
1334
    /// This field is required for all node configurations as it specifies where to
1335
    /// find the underlying bitcoin node to interact with for PoX operations,
1336
    /// block validation, and mining.
1337
    /// ---
1338
    /// @default: `"0.0.0.0"`
1339
    pub peer_host: String,
1340
    /// The P2P network port of the bitcoin node specified by [`BurnchainConfig::peer_host`].
1341
    /// ---
1342
    /// @default: `8333`
1343
    pub peer_port: u16,
1344
    /// The RPC port of the bitcoin node specified by [`BurnchainConfig::peer_host`].
1345
    /// ---
1346
    /// @default: `8332`
1347
    pub rpc_port: u16,
1348
    /// Flag indicating whether to use SSL/TLS when connecting to the bitcoin node's
1349
    /// RPC interface.
1350
    /// ---
1351
    /// @default: `false`
1352
    pub rpc_ssl: bool,
1353
    /// The username for authenticating with the bitcoin node's RPC interface.
1354
    /// Required if the bitcoin node requires RPC authentication.
1355
    /// ---
1356
    /// @default: `None`
1357
    /// @notes:
1358
    ///   - Only relevant if [`NodeConfig::miner`] is `true`.
1359
    pub username: Option<String>,
1360
    /// The password for authenticating with the bitcoin node's RPC interface.
1361
    /// Required if the bitcoin node requires RPC authentication.
1362
    /// ---
1363
    /// @default: `None`
1364
    /// @notes:
1365
    ///   - Only relevant if [`NodeConfig::miner`] is `true`.
1366
    pub password: Option<String>,
1367
    /// Timeout duration, in seconds, for RPC calls made to the bitcoin node.
1368
    /// Configures the timeout on the underlying HTTP client.
1369
    /// ---
1370
    /// @default: `300`
1371
    /// @units: seconds
1372
    pub timeout: u64,
1373
    /// Timeout duration, in seconds, for socket operations (read/write) with the bitcoin node.
1374
    /// Controls how long the node will wait for socket operations to complete before timing out.
1375
    /// ---
1376
    /// @default: `30`
1377
    /// @units: seconds
1378
    pub socket_timeout: u64,
1379
    /// The network "magic bytes" used to identify packets for the specific bitcoin
1380
    /// network instance (e.g., mainnet, testnet, regtest). Must match the magic
1381
    /// bytes of the connected bitcoin node.
1382
    ///
1383
    /// These two-byte identifiers help ensure that nodes only connect to peers on the
1384
    /// same network type. Common values include:
1385
    /// - "X2" for mainnet
1386
    /// - "T2" for testnet (xenon)
1387
    /// - Other values for specific test networks
1388
    ///
1389
    /// Configured as a 2-character ASCII string (e.g., "X2" for mainnet).
1390
    /// ---
1391
    /// @default: |
1392
    ///   - if [`BurnchainConfig::mode`] is `"xenon"`: `"T2"`
1393
    ///   - else: `"X2"`
1394
    pub magic_bytes: MagicBytes,
1395
    /// The public key associated with the local mining address for the underlying
1396
    /// Bitcoin regtest node. Provided as a hex string representing an uncompressed
1397
    /// public key.
1398
    ///
1399
    /// It is primarily used in modes that rely on a controlled Bitcoin regtest
1400
    /// backend (e.g., "helium", "mocknet", "neon") where the Stacks node itself
1401
    /// needs to instruct the Bitcoin node to generate blocks.
1402
    ///
1403
    /// The key is used to derive the Bitcoin address that receives the coinbase
1404
    /// rewards when generating blocks on the regtest network.
1405
    /// ---
1406
    /// @default: `None`
1407
    /// @notes:
1408
    ///   - Mandatory if [`BurnchainConfig::mode`] is "helium".
1409
    ///   - This is intended strictly for testing purposes.
1410
    pub local_mining_public_key: Option<String>,
1411
    /// Optional bitcoin block height at which the Stacks node process should
1412
    /// gracefully exit. When bitcoin reaches this height, the node logs a message
1413
    /// and initiates a graceful shutdown.
1414
    /// ---
1415
    /// @default: `None`
1416
    /// @notes:
1417
    ///   - Applied only if [`BurnchainConfig::mode`] is not "mainnet".
1418
    ///   - This is intended strictly for testing purposes.
1419
    pub process_exit_at_block_height: Option<u64>,
1420
    /// The interval, in seconds, at which the node polls the bitcoin node for new
1421
    /// blocks and state updates.
1422
    ///
1423
    /// The default value of 10 seconds is mainly intended for testing purposes.
1424
    /// It's suggested to set this to a higher value for mainnet, e.g., 300 seconds
1425
    /// (5 minutes).
1426
    /// ---
1427
    /// @default: `10`
1428
    /// @units: seconds
1429
    pub poll_time_secs: u64,
1430
    /// The default fee rate in sats/vByte to use when estimating fees for miners
1431
    /// to submit bitcoin transactions (like block commits or leader key registrations).
1432
    /// ---
1433
    /// @default: [`DEFAULT_SATS_PER_VB`]
1434
    /// @units: sats/vByte
1435
    /// @notes:
1436
    ///   - Only relevant if [`NodeConfig::miner`] is `true`.
1437
    pub satoshis_per_byte: u64,
1438
    /// Maximum fee rate multiplier allowed when using Replace-By-Fee (RBF) for
1439
    /// bitcoin transactions. Expressed as a percentage of the original
1440
    /// [`BurnchainConfig::satoshis_per_byte`] rate (e.g., 150 means the fee rate
1441
    /// can be increased up to 1.5x). Used in mining logic for RBF decisions to
1442
    /// cap the replacement fee rate.
1443
    /// ---
1444
    /// @default: [`DEFAULT_MAX_RBF_RATE`]
1445
    /// @units: percent
1446
    /// @notes:
1447
    ///   - Only relevant if [`NodeConfig::miner`] is `true`.
1448
    pub max_rbf: u64,
1449
    /// Estimated size (in virtual bytes) of a leader key registration transaction
1450
    /// on bitcoin. Used for fee calculation in mining logic by multiplying with the
1451
    /// fee rate [`BurnchainConfig::satoshis_per_byte`].
1452
    /// ---
1453
    /// @default: [`OP_TX_LEADER_KEY_ESTIM_SIZE`]
1454
    /// @units: virtual bytes
1455
    /// @notes:
1456
    ///   - Only relevant if [`NodeConfig::miner`] is `true`.
1457
    pub leader_key_tx_estimated_size: u64,
1458
    /// Estimated size (in virtual bytes) of a block commit transaction on bitcoin.
1459
    /// Used for fee calculation in mining logic by multiplying with the fee rate
1460
    /// [`BurnchainConfig::satoshis_per_byte`].
1461
    /// ---
1462
    /// @default: [`OP_TX_BLOCK_COMMIT_ESTIM_SIZE`]
1463
    /// @units: virtual bytes
1464
    /// @notes:
1465
    ///   - Only relevant if [`NodeConfig::miner`] is `true`.
1466
    pub block_commit_tx_estimated_size: u64,
1467
    /// The incremental amount (in sats/vByte) to add to the previous transaction's
1468
    /// fee rate for RBF bitcoin transactions.
1469
    /// ---
1470
    /// @default: [`DEFAULT_RBF_FEE_RATE_INCREMENT`]
1471
    /// @units: sats/vByte
1472
    /// @notes:
1473
    ///   - Only relevant if [`NodeConfig::miner`] is `true`.
1474
    pub rbf_fee_increment: u64,
1475
    /// Overrides the default starting bitcoin block height for the node.
1476
    /// Allows starting synchronization from a specific historical point in test environments.
1477
    /// ---
1478
    /// @default: `None` (uses the burnchain's default starting height for the mode)
1479
    /// @notes:
1480
    ///   - Applied only if [`BurnchainConfig::mode`] is not "mainnet".
1481
    ///   - This is intended strictly for testing purposes.
1482
    ///   - Should be used together with [`BurnchainConfig::first_burn_block_timestamp`] and
1483
    ///     [`BurnchainConfig::first_burn_block_hash`] for proper operation.
1484
    pub first_burn_block_height: Option<u64>,
1485
    /// Overrides the default starting block timestamp of the burnchain.
1486
    /// ---
1487
    /// @default: `None` (uses the burnchain's default starting timestamp)
1488
    /// @notes:
1489
    ///   - Applied only if [`BurnchainConfig::mode`] is not "mainnet".
1490
    ///   - This is intended strictly for testing purposes.
1491
    ///   - Should be used together with [`BurnchainConfig::first_burn_block_height`] and
1492
    ///     [`BurnchainConfig::first_burn_block_hash`] for proper operation.
1493
    pub first_burn_block_timestamp: Option<u32>,
1494
    /// Overrides the default starting block hash of the burnchain.
1495
    /// ---
1496
    /// @default: `None` (uses the burnchain's default starting block hash)
1497
    /// @notes:
1498
    ///   - Applied only if [`BurnchainConfig::mode`] is not "mainnet".
1499
    ///   - This is intended strictly for testing purposes.
1500
    ///   - Should be used together with [`BurnchainConfig::first_burn_block_height`] and
1501
    ///     [`BurnchainConfig::first_burn_block_timestamp`] for proper operation.
1502
    pub first_burn_block_hash: Option<String>,
1503
    /// Custom override for the definitions of Stacks epochs (start/end burnchain
1504
    /// heights, consensus rules). This setting allows testing specific epoch
1505
    /// transitions or custom consensus rules by defining exactly when each epoch
1506
    /// starts on bitcoin.
1507
    ///
1508
    /// Epochs define distinct protocol rule sets (consensus rules, execution costs,
1509
    /// capabilities). When configured, the list must include all epochs
1510
    /// sequentially from "1.0" up to the highest desired epoch, without skipping
1511
    /// any intermediate ones. Valid `epoch_name` values currently include:
1512
    /// `"1.0"`, `"2.0"`, `"2.05"`, `"2.1"`, `"2.2"`, `"2.3"`, `"2.4"`, `"2.5"`, `"3.0"`, `"3.1"`.
1513
    ///
1514
    /// **Validation Rules:**
1515
    /// - Epochs must be provided in strict chronological order (`1.0`, `2.0`, `2.05`...).
1516
    /// - `start_height` values must be non-decreasing across the list.
1517
    /// - Epoch `"1.0"` must have `start_height = 0`.
1518
    /// - The number of defined epochs cannot exceed the maximum supported by the node software.
1519
    /// ---
1520
    /// @default: `None` (uses the standard epoch definitions for the selected [`BurnchainConfig::mode`])
1521
    /// @notes:
1522
    ///   - Applied only if [`BurnchainConfig::mode`] is not "mainnet".
1523
    ///   - This is intended strictly for testing purposes.
1524
    ///   - Configured as a list `[[burnchain.epochs]]` in TOML, each with `epoch_name` (string)
1525
    ///     and `start_height` (integer Bitcoin block height).
1526
    /// @toml_example: |
1527
    ///   [[burnchain.epochs]]
1528
    ///   epoch_name = "2.1"
1529
    ///   start_height = 150
1530
    ///
1531
    ///   [[burnchain.epochs]]
1532
    ///   epoch_name = "2.2"
1533
    ///   start_height = 200
1534
    pub epochs: Option<EpochList<ExecutionCost>>,
1535
    /// Sets a custom burnchain height for PoX-2 activation (for testing).
1536
    ///
1537
    /// This affects two key transitions:
1538
    /// 1. The block height at which PoX v1 lockups are automatically unlocked.
1539
    /// 2. The block height from which PoX reward set calculations switch to PoX v2 rules.
1540
    ///
1541
    /// **Behavior:**
1542
    /// - This value directly sets the auto unlock height for PoX v1 lockups before
1543
    ///   transition to PoX v2. This also defines the burn height at which PoX reward
1544
    ///   sets are calculated using PoX v2 rather than v1.
1545
    /// - If custom [`BurnchainConfig::epochs`] are provided:
1546
    ///   - This value is used to validate that Epoch 2.1's start height is ≤ this value.
1547
    ///   - However, the height specified in `epochs` for Epoch 2.1 takes precedence.
1548
    /// ---
1549
    /// @default: `None`
1550
    /// @notes:
1551
    ///   - Applied only if [`BurnchainConfig::mode`] is not "mainnet".
1552
    ///   - This is intended strictly for testing purposes.
1553
    pub pox_2_activation: Option<u32>,
1554
    /// Overrides the length (in bitcoin blocks) of the PoX reward cycle.
1555
    /// ---
1556
    /// @default: `None` (uses the standard reward cycle length for the mode)
1557
    /// @units: bitcoin blocks
1558
    /// @notes:
1559
    ///   - Applied only if [`BurnchainConfig::mode`] is not "mainnet".
1560
    ///   - This is intended strictly for testing purposes.
1561
    pub pox_reward_length: Option<u32>,
1562
    /// Overrides the length (in bitcoin blocks) of the PoX prepare phase.
1563
    /// ---
1564
    /// @default: `None` (uses the standard prepare phase length for the mode)
1565
    /// @units: bitcoin blocks
1566
    /// @notes:
1567
    ///   - Applied only if [`BurnchainConfig::mode`] is not "mainnet".
1568
    ///   - This is intended strictly for testing purposes.
1569
    pub pox_prepare_length: Option<u32>,
1570
    /// Overrides the bitcoin height at which the PoX sunset period begins in epochs
1571
    /// before 2.1. The sunset period represents a planned phase-out of the PoX
1572
    /// mechanism. During this period, stacking rewards gradually decrease,
1573
    /// eventually ceasing entirely. This parameter allows testing the PoX sunset
1574
    /// transition by explicitly setting its start height.
1575
    /// ---
1576
    /// @default: `None` (uses the standard sunset start height for the mode)
1577
    /// @deprecated: The sunset phase was removed in Epoch 2.1.
1578
    /// @notes:
1579
    ///   - Applied only if [`BurnchainConfig::mode`] is not "mainnet".
1580
    ///   - This is intended strictly for testing purposes for epochs before 2.1.
1581
    pub sunset_start: Option<u32>,
1582
    /// Overrides the bitcoin height, non-inclusive, at which the PoX sunset period
1583
    /// ends in epochs before 2.1. After this height, Stacking rewards are disabled
1584
    /// completely. This parameter works together with `sunset_start` to define the
1585
    /// full sunset transition period for PoX.
1586
    /// ---
1587
    /// @default: `None` (uses the standard sunset end height for the mode)
1588
    /// @deprecated: The sunset phase was removed in Epoch 2.1.
1589
    /// @notes:
1590
    ///   - Applied only if [`BurnchainConfig::mode`] is not "mainnet".
1591
    ///   - This is intended strictly for testing purposes for epochs before 2.1.
1592
    pub sunset_end: Option<u32>,
1593
    /// Specifies the name of the Bitcoin wallet to use within the connected bitcoin
1594
    /// node. Used to interact with a specific named wallet if the bitcoin node
1595
    /// manages multiple wallets.
1596
    ///
1597
    /// If the specified wallet doesn't exist, the node will attempt to create it via
1598
    /// the `createwallet` RPC call. This is particularly useful for miners who need
1599
    /// to manage separate wallets.
1600
    /// ---
1601
    /// @default: `""` (empty string, implying the default wallet or no specific wallet needed)
1602
    /// @notes:
1603
    ///   - Primarily relevant for miners interacting with multi-wallet Bitcoin nodes.
1604
    pub wallet_name: String,
1605
    /// Fault injection setting for testing. Introduces an artificial delay (in
1606
    /// milliseconds) before processing each burnchain block download. Simulates a
1607
    /// slow burnchain connection.
1608
    /// ---
1609
    /// @default: `0` (no delay)
1610
    /// @units: milliseconds
1611
    /// @notes:
1612
    ///   - This is intended strictly for testing purposes.
1613
    pub fault_injection_burnchain_block_delay: u64,
1614
    /// The maximum number of unspent transaction outputs (UTXOs) to request from
1615
    /// the bitcoin node.
1616
    ///
1617
    /// This value is passed as the `maximumCount` parameter to the bitcoin node.
1618
    /// It helps manage response size and processing load, particularly relevant
1619
    /// for miners querying for available UTXOs to fund operations like block
1620
    /// commits or leader key registrations.
1621
    ///
1622
    /// Setting this limit too high might lead to performance issues or timeouts when
1623
    /// querying nodes with a very large number of UTXOs. Conversely, setting it too
1624
    /// low might prevent the miner from finding enough UTXOs in a single query to
1625
    /// meet the required funding amount for a transaction, even if sufficient funds
1626
    /// exist across more UTXOs not returned by the limited query.
1627
    /// ---
1628
    /// @default: `1024`
1629
    /// @notes:
1630
    ///   - This value must be `<= 1024`.
1631
    ///   - Only relevant if [`NodeConfig::miner`] is `true`.
1632
    pub max_unspent_utxos: Option<u64>,
1633
}
1634

1635
impl BurnchainConfig {
1636
    fn default() -> BurnchainConfig {
4,454✔
1637
        BurnchainConfig {
4,454✔
1638
            chain: "bitcoin".to_string(),
4,454✔
1639
            mode: "mocknet".to_string(),
4,454✔
1640
            chain_id: CHAIN_ID_TESTNET,
4,454✔
1641
            peer_version: PEER_VERSION_TESTNET,
4,454✔
1642
            burn_fee_cap: 20000,
4,454✔
1643
            commit_anchor_block_within: 5000,
4,454✔
1644
            peer_host: "0.0.0.0".to_string(),
4,454✔
1645
            peer_port: 8333,
4,454✔
1646
            rpc_port: 8332,
4,454✔
1647
            rpc_ssl: false,
4,454✔
1648
            username: None,
4,454✔
1649
            password: None,
4,454✔
1650
            timeout: 300,
4,454✔
1651
            socket_timeout: 30,
4,454✔
1652
            magic_bytes: BLOCKSTACK_MAGIC_MAINNET,
4,454✔
1653
            local_mining_public_key: None,
4,454✔
1654
            process_exit_at_block_height: None,
4,454✔
1655
            poll_time_secs: 10, // TODO: this is a testnet specific value.
4,454✔
1656
            satoshis_per_byte: DEFAULT_SATS_PER_VB,
4,454✔
1657
            max_rbf: DEFAULT_MAX_RBF_RATE,
4,454✔
1658
            leader_key_tx_estimated_size: OP_TX_LEADER_KEY_ESTIM_SIZE,
4,454✔
1659
            block_commit_tx_estimated_size: OP_TX_BLOCK_COMMIT_ESTIM_SIZE,
4,454✔
1660
            rbf_fee_increment: DEFAULT_RBF_FEE_RATE_INCREMENT,
4,454✔
1661
            first_burn_block_height: None,
4,454✔
1662
            first_burn_block_timestamp: None,
4,454✔
1663
            first_burn_block_hash: None,
4,454✔
1664
            epochs: None,
4,454✔
1665
            pox_2_activation: None,
4,454✔
1666
            pox_prepare_length: None,
4,454✔
1667
            pox_reward_length: None,
4,454✔
1668
            sunset_start: None,
4,454✔
1669
            sunset_end: None,
4,454✔
1670
            wallet_name: "".to_string(),
4,454✔
1671
            fault_injection_burnchain_block_delay: 0,
4,454✔
1672
            max_unspent_utxos: Some(1024),
4,454✔
1673
        }
4,454✔
1674
    }
4,454✔
1675
    pub fn get_rpc_url(&self, wallet: Option<String>) -> String {
×
1676
        let scheme = match self.rpc_ssl {
×
1677
            true => "https://",
×
1678
            false => "http://",
×
1679
        };
1680
        let wallet_path = if let Some(wallet_id) = wallet.as_ref() {
×
1681
            format!("/wallet/{wallet_id}")
×
1682
        } else {
1683
            "".to_string()
×
1684
        };
1685
        format!("{scheme}{}:{}{wallet_path}", self.peer_host, self.rpc_port)
×
1686
    }
×
1687

1688
    pub fn get_rpc_socket_addr(&self) -> SocketAddr {
×
1689
        let mut addrs_iter = format!("{}:{}", self.peer_host, self.rpc_port)
×
1690
            .to_socket_addrs()
×
1691
            .unwrap();
×
1692
        addrs_iter.next().unwrap()
×
1693
    }
×
1694

1695
    pub fn get_bitcoin_network(&self) -> (String, BitcoinNetworkType) {
35,160,439✔
1696
        match self.mode.as_str() {
35,160,439✔
1697
            "mainnet" => ("mainnet".to_string(), BitcoinNetworkType::Mainnet),
35,160,439✔
1698
            "xenon" => ("testnet".to_string(), BitcoinNetworkType::Testnet),
35,160,428✔
1699
            "helium" | "neon" | "argon" | "krypton" | "mocknet" | "nakamoto-neon" => {
35,160,104✔
1700
                ("regtest".to_string(), BitcoinNetworkType::Regtest)
35,160,104✔
1701
            }
1702
            other => panic!("Invalid stacks-node mode: {other}"),
×
1703
        }
1704
    }
35,160,439✔
1705

1706
    pub fn get_epoch_list(&self) -> EpochList<ExecutionCost> {
9,052,974✔
1707
        StacksEpoch::get_epochs(self.get_bitcoin_network().1, self.epochs.as_ref())
9,052,974✔
1708
    }
9,052,974✔
1709
}
1710

1711
#[derive(Clone, Deserialize, Default, Debug)]
1712
pub struct StacksEpochConfigFile {
1713
    epoch_name: String,
1714
    start_height: i64,
1715
}
1716

1717
pub const EPOCH_CONFIG_1_0_0: &str = "1.0";
1718
pub const EPOCH_CONFIG_2_0_0: &str = "2.0";
1719
pub const EPOCH_CONFIG_2_0_5: &str = "2.05";
1720
pub const EPOCH_CONFIG_2_1_0: &str = "2.1";
1721
pub const EPOCH_CONFIG_2_2_0: &str = "2.2";
1722
pub const EPOCH_CONFIG_2_3_0: &str = "2.3";
1723
pub const EPOCH_CONFIG_2_4_0: &str = "2.4";
1724
pub const EPOCH_CONFIG_2_5_0: &str = "2.5";
1725
pub const EPOCH_CONFIG_3_0_0: &str = "3.0";
1726
pub const EPOCH_CONFIG_3_1_0: &str = "3.1";
1727
pub const EPOCH_CONFIG_3_2_0: &str = "3.2";
1728
pub const EPOCH_CONFIG_3_3_0: &str = "3.3";
1729
pub const EPOCH_CONFIG_3_4_0: &str = "3.4";
1730

1731
#[derive(Clone, Deserialize, Default, Debug)]
1732
#[serde(deny_unknown_fields)]
1733
pub struct BurnchainConfigFile {
1734
    pub chain: Option<String>,
1735
    pub mode: Option<String>,
1736
    pub chain_id: Option<u32>,
1737
    pub burn_fee_cap: Option<u64>,
1738
    pub commit_anchor_block_within: Option<u64>,
1739
    pub peer_host: Option<String>,
1740
    pub peer_port: Option<u16>,
1741
    pub rpc_port: Option<u16>,
1742
    pub rpc_ssl: Option<bool>,
1743
    pub username: Option<String>,
1744
    pub password: Option<String>,
1745
    /// Timeout, in seconds, for communication with bitcoind
1746
    pub timeout: Option<u64>,
1747
    /// Socket timeout, in seconds, for socket operations with bitcoind
1748
    pub socket_timeout: Option<u64>,
1749
    pub magic_bytes: Option<String>,
1750
    pub local_mining_public_key: Option<String>,
1751
    pub process_exit_at_block_height: Option<u64>,
1752
    pub poll_time_secs: Option<u64>,
1753
    pub satoshis_per_byte: Option<u64>,
1754
    pub leader_key_tx_estimated_size: Option<u64>,
1755
    pub block_commit_tx_estimated_size: Option<u64>,
1756
    pub rbf_fee_increment: Option<u64>,
1757
    pub max_rbf: Option<u64>,
1758
    pub first_burn_block_height: Option<u64>,
1759
    pub first_burn_block_timestamp: Option<u32>,
1760
    pub first_burn_block_hash: Option<String>,
1761
    pub epochs: Option<Vec<StacksEpochConfigFile>>,
1762
    pub pox_prepare_length: Option<u32>,
1763
    pub pox_reward_length: Option<u32>,
1764
    pub pox_2_activation: Option<u32>,
1765
    pub sunset_start: Option<u32>,
1766
    pub sunset_end: Option<u32>,
1767
    pub wallet_name: Option<String>,
1768
    pub fault_injection_burnchain_block_delay: Option<u64>,
1769
    pub max_unspent_utxos: Option<u64>,
1770
}
1771

1772
impl BurnchainConfigFile {
1773
    fn into_config_default(
1,824✔
1774
        mut self,
1,824✔
1775
        default_burnchain_config: BurnchainConfig,
1,824✔
1776
    ) -> Result<BurnchainConfig, String> {
1,824✔
1777
        if self.mode.as_deref() == Some("xenon") {
1,824✔
1778
            if self.magic_bytes.is_none() {
324✔
1779
                self.magic_bytes = ConfigFile::xenon().burnchain.unwrap().magic_bytes;
×
1780
            }
324✔
1781
        }
1,500✔
1782

1783
        let mode = self.mode.unwrap_or(default_burnchain_config.mode);
1,824✔
1784
        let is_mainnet = mode == "mainnet";
1,824✔
1785
        if is_mainnet {
1,824✔
1786
            // check magic bytes and set if not defined
1787
            let mainnet_magic = ConfigFile::mainnet().burnchain.unwrap().magic_bytes;
12✔
1788
            if self.magic_bytes.is_none() {
12✔
1789
                self.magic_bytes.clone_from(&mainnet_magic);
3✔
1790
            }
12✔
1791
            if self.magic_bytes != mainnet_magic {
12✔
1792
                return Err(format!(
×
1793
                    "Attempted to run mainnet node with bad magic bytes '{}'",
×
1794
                    self.magic_bytes.as_ref().unwrap()
×
1795
                ));
×
1796
            }
12✔
1797
        }
1,812✔
1798

1799
        let mut config = BurnchainConfig {
1,822✔
1800
            chain: self.chain.unwrap_or(default_burnchain_config.chain),
1,824✔
1801
            chain_id: match self.chain_id {
1,824✔
1802
                Some(chain_id) => {
3✔
1803
                    if is_mainnet && chain_id != CHAIN_ID_MAINNET {
3✔
1804
                        return Err(format!(
1✔
1805
                            "Attempted to run mainnet node with chain_id {chain_id}",
1✔
1806
                        ));
1✔
1807
                    }
2✔
1808
                    chain_id
2✔
1809
                }
1810
                None => {
1811
                    if is_mainnet {
1,821✔
1812
                        CHAIN_ID_MAINNET
10✔
1813
                    } else {
1814
                        CHAIN_ID_TESTNET
1,811✔
1815
                    }
1816
                }
1817
            },
1818
            peer_version: if is_mainnet {
1,823✔
1819
                PEER_VERSION_MAINNET
11✔
1820
            } else {
1821
                PEER_VERSION_TESTNET
1,812✔
1822
            },
1823
            mode,
1,823✔
1824
            burn_fee_cap: self
1,823✔
1825
                .burn_fee_cap
1,823✔
1826
                .unwrap_or(default_burnchain_config.burn_fee_cap),
1,823✔
1827
            commit_anchor_block_within: self
1,823✔
1828
                .commit_anchor_block_within
1,823✔
1829
                .unwrap_or(default_burnchain_config.commit_anchor_block_within),
1,823✔
1830
            peer_host: match self.peer_host.as_ref() {
1,823✔
1831
                Some(peer_host) => {
334✔
1832
                    format!("{}:1", &peer_host)
334✔
1833
                        .to_socket_addrs()
334✔
1834
                        .map_err(|e| format!("Invalid burnchain.peer_host: {}", &e))?
334✔
1835
                        .next()
333✔
1836
                        .is_none()
333✔
1837
                        .then(|| {
333✔
1838
                            return format!("No IP address could be queried for '{}'", &peer_host);
×
1839
                        });
×
1840
                    peer_host.clone()
333✔
1841
                }
1842
                None => default_burnchain_config.peer_host,
1,489✔
1843
            },
1844
            peer_port: self.peer_port.unwrap_or(default_burnchain_config.peer_port),
1,822✔
1845
            rpc_port: self.rpc_port.unwrap_or(default_burnchain_config.rpc_port),
1,822✔
1846
            rpc_ssl: self.rpc_ssl.unwrap_or(default_burnchain_config.rpc_ssl),
1,822✔
1847
            username: self.username,
1,822✔
1848
            password: self.password,
1,822✔
1849
            timeout: self.timeout.unwrap_or(default_burnchain_config.timeout),
1,822✔
1850
            socket_timeout: self
1,822✔
1851
                .socket_timeout
1,822✔
1852
                .unwrap_or(default_burnchain_config.socket_timeout),
1,822✔
1853
            magic_bytes: self
1,822✔
1854
                .magic_bytes
1,822✔
1855
                .map(|magic_ascii| {
1,822✔
1856
                    assert_eq!(magic_ascii.len(), 2, "Magic bytes must be length-2");
335✔
1857
                    assert!(magic_ascii.is_ascii(), "Magic bytes must be ASCII");
335✔
1858
                    MagicBytes::from(magic_ascii.as_bytes())
335✔
1859
                })
335✔
1860
                .unwrap_or(default_burnchain_config.magic_bytes),
1,822✔
1861
            local_mining_public_key: self.local_mining_public_key,
1,822✔
1862
            process_exit_at_block_height: self.process_exit_at_block_height,
1,822✔
1863
            poll_time_secs: self
1,822✔
1864
                .poll_time_secs
1,822✔
1865
                .unwrap_or(default_burnchain_config.poll_time_secs),
1,822✔
1866
            satoshis_per_byte: self
1,822✔
1867
                .satoshis_per_byte
1,822✔
1868
                .unwrap_or(default_burnchain_config.satoshis_per_byte),
1,822✔
1869
            max_rbf: self.max_rbf.unwrap_or(default_burnchain_config.max_rbf),
1,822✔
1870
            leader_key_tx_estimated_size: self
1,822✔
1871
                .leader_key_tx_estimated_size
1,822✔
1872
                .unwrap_or(default_burnchain_config.leader_key_tx_estimated_size),
1,822✔
1873
            block_commit_tx_estimated_size: self
1,822✔
1874
                .block_commit_tx_estimated_size
1,822✔
1875
                .unwrap_or(default_burnchain_config.block_commit_tx_estimated_size),
1,822✔
1876
            rbf_fee_increment: self
1,822✔
1877
                .rbf_fee_increment
1,822✔
1878
                .unwrap_or(default_burnchain_config.rbf_fee_increment),
1,822✔
1879
            first_burn_block_height: self
1,822✔
1880
                .first_burn_block_height
1,822✔
1881
                .or(default_burnchain_config.first_burn_block_height),
1,822✔
1882
            first_burn_block_timestamp: self
1,822✔
1883
                .first_burn_block_timestamp
1,822✔
1884
                .or(default_burnchain_config.first_burn_block_timestamp),
1,822✔
1885
            first_burn_block_hash: self
1,822✔
1886
                .first_burn_block_hash
1,822✔
1887
                .clone()
1,822✔
1888
                .or(default_burnchain_config.first_burn_block_hash.clone()),
1,822✔
1889
            // will be overwritten below
1890
            epochs: default_burnchain_config.epochs,
1,822✔
1891
            pox_2_activation: self
1,822✔
1892
                .pox_2_activation
1,822✔
1893
                .or(default_burnchain_config.pox_2_activation),
1,822✔
1894
            sunset_start: self.sunset_start.or(default_burnchain_config.sunset_start),
1,822✔
1895
            sunset_end: self.sunset_end.or(default_burnchain_config.sunset_end),
1,822✔
1896
            wallet_name: self
1,822✔
1897
                .wallet_name
1,822✔
1898
                .unwrap_or(default_burnchain_config.wallet_name.clone()),
1,822✔
1899
            pox_reward_length: self
1,822✔
1900
                .pox_reward_length
1,822✔
1901
                .or(default_burnchain_config.pox_reward_length),
1,822✔
1902
            pox_prepare_length: self
1,822✔
1903
                .pox_prepare_length
1,822✔
1904
                .or(default_burnchain_config.pox_prepare_length),
1,822✔
1905
            fault_injection_burnchain_block_delay: self
1,822✔
1906
                .fault_injection_burnchain_block_delay
1,822✔
1907
                .unwrap_or(default_burnchain_config.fault_injection_burnchain_block_delay),
1,822✔
1908
            max_unspent_utxos: self
1,822✔
1909
                .max_unspent_utxos
1,822✔
1910
                .inspect(|&val| {
1,822✔
1911
                    assert!(val <= 1024, "Value for max_unspent_utxos should be <= 1024");
×
1912
                })
×
1913
                .or(default_burnchain_config.max_unspent_utxos),
1,822✔
1914
        };
1915

1916
        if let BitcoinNetworkType::Mainnet = config.get_bitcoin_network().1 {
1,822✔
1917
            // check that pox_2_activation hasn't been set in mainnet
1918
            if config.pox_2_activation.is_some()
11✔
1919
                || config.sunset_start.is_some()
11✔
1920
                || config.sunset_end.is_some()
11✔
1921
            {
1922
                return Err("PoX-2 parameters are not configurable in mainnet".into());
×
1923
            }
11✔
1924
            // Check that the first burn block options are not set in mainnet
1925
            if config.first_burn_block_height.is_some()
11✔
1926
                || config.first_burn_block_timestamp.is_some()
11✔
1927
                || config.first_burn_block_hash.is_some()
11✔
1928
            {
1929
                return Err("First burn block parameters are not configurable in mainnet".into());
×
1930
            }
11✔
1931
        }
1,811✔
1932

1933
        if let Some(ref conf_epochs) = self.epochs {
1,822✔
1934
            config.epochs = Some(Config::make_epochs(
×
1935
                conf_epochs,
×
1936
                &config.mode,
×
1937
                config.get_bitcoin_network().1,
×
1938
                self.pox_2_activation,
×
1939
            )?);
×
1940
        }
1,822✔
1941

1942
        Ok(config)
1,822✔
1943
    }
1,824✔
1944
}
1945

1946
#[derive(Clone, Debug)]
1947
pub struct NodeConfig {
1948
    /// Human-readable name for the node. Primarily used for identification in testing
1949
    /// environments (e.g., deriving log file names, temporary directory names).
1950
    /// ---
1951
    /// @default: `"helium-node"`
1952
    pub name: String,
1953
    /// The node's Bitcoin wallet private key, provided as a hex string in the config file.
1954
    /// Used to initialize the node's keychain for signing operations.
1955
    /// If [`MinerConfig::mining_key`] is not set, this seed may also be used for
1956
    /// mining-related signing.
1957
    /// ---
1958
    /// @default: Randomly generated 32 bytes
1959
    /// @notes:
1960
    ///   - Required if [`NodeConfig::miner`] is `true` and [`MinerConfig::mining_key`] is absent.
1961
    pub seed: Vec<u8>,
1962
    /// The file system absolute path to the node's working directory.
1963
    /// All persistent data, including chainstate, burnchain databases, and potentially
1964
    /// other stores, will be located within this directory. This path can be
1965
    /// overridden by setting the `STACKS_WORKING_DIR` environment variable.
1966
    /// ---
1967
    /// @default: `/tmp/stacks-node-{current_timestamp}`
1968
    /// @notes:
1969
    ///   - For persistent mainnet or testnet nodes, this path must be explicitly
1970
    ///     configured to a non-temporary location.
1971
    pub working_dir: String,
1972
    /// The IPv4 address and port (e.g., "0.0.0.0:20443") on which the node's HTTP RPC
1973
    /// server should bind and listen for incoming API requests.
1974
    /// ---
1975
    /// @default: `"0.0.0.0:20443"`
1976
    pub rpc_bind: String,
1977
    /// The IPv4 address and port (e.g., "0.0.0.0:20444") on which the node's P2P
1978
    /// networking service should bind and listen for incoming connections from other peers.
1979
    /// ---
1980
    /// @default: `"0.0.0.0:20444"`
1981
    pub p2p_bind: String,
1982
    /// The publicly accessible URL that this node advertises to peers during the P2P
1983
    /// handshake as its HTTP RPC endpoint. Other nodes or services might use this URL
1984
    /// to query the node's API.
1985
    /// ---
1986
    /// @default: Derived by adding "http://" prefix to [`NodeConfig::rpc_bind`] value.
1987
    /// @notes:
1988
    ///   - Example: For rpc_bind="0.0.0.0:20443", data_url becomes "http://0.0.0.0:20443".
1989
    pub data_url: String,
1990
    /// The publicly accessible IPv4 address and port that this node advertises to peers
1991
    /// for P2P connections. This might differ from [`NodeConfig::p2p_bind`] if the
1992
    /// node is behind NAT or a proxy.
1993
    /// ---
1994
    /// @default: Derived directly from [`NodeConfig::rpc_bind`] value.
1995
    /// @notes:
1996
    ///   - Example: For rpc_bind="0.0.0.0:20443", p2p_address becomes "0.0.0.0:20443".
1997
    ///   - The default value derivation might be unexpected, potentially using the
1998
    ///     [`NodeConfig::rpc_bind`] address; explicit configuration is recommended if needed.
1999
    pub p2p_address: String,
2000
    /// The private key seed, provided as a hex string in the config file, used
2001
    /// specifically for the node's identity and message signing within the P2P
2002
    /// networking layer. This is separate from the main [`NodeConfig::seed`].
2003
    /// ---
2004
    /// @default: Randomly generated 32 bytes
2005
    pub local_peer_seed: Vec<u8>,
2006
    /// A list of initial peer nodes used to bootstrap connections into the Stacks P2P
2007
    /// network. Peers are specified in a configuration file as comma-separated
2008
    /// strings in the format `"PUBKEY@IP:PORT"` or `"PUBKEY@HOSTNAME:PORT"`. DNS
2009
    /// hostnames are resolved during configuration loading.
2010
    /// ---
2011
    /// @default: `[]` (empty vector)
2012
    /// @toml_example: |
2013
    ///   bootstrap_node = "pubkey1@example.com:30444,pubkey2@192.168.1.100:20444"
2014
    pub bootstrap_node: Vec<Neighbor>,
2015
    /// A list of peer addresses that this node should explicitly deny connections from.
2016
    /// Peers are specified as comma-separated strings in the format "IP:PORT" or
2017
    /// "HOSTNAME:PORT" in the configuration file. DNS hostnames are resolved during
2018
    /// configuration loading.
2019
    /// ---
2020
    /// @default: `[]` (empty vector)
2021
    /// @toml_example: |
2022
    ///   deny_nodes = "192.168.1.100:20444,badhost.example.com:20444"
2023
    pub deny_nodes: Vec<Neighbor>,
2024
    /// Flag indicating whether this node should activate its mining logic and attempt to
2025
    /// produce Stacks blocks. Setting this to `true` typically requires providing
2026
    /// necessary private keys (either [`NodeConfig::seed`] or [`MinerConfig::mining_key`]).
2027
    /// ---
2028
    /// @default: `false`
2029
    pub miner: bool,
2030
    /// Setting this to `true` enables the node to replicate the miner and signer
2031
    /// Stacker DBs required for signing, and is required if the node is connected to a
2032
    /// signer.
2033
    /// ---
2034
    /// @default: `false`
2035
    pub stacker: bool,
2036
    /// Enables a simulated mining mode, primarily for local testing and development.
2037
    /// When `true`, the node may generate blocks locally without participating in the
2038
    /// real bitcoin consensus or P2P block production process.
2039
    /// ---
2040
    /// @default: `false`
2041
    /// @notes:
2042
    ///   - Only relevant if [`NodeConfig::miner`] is `true`.
2043
    pub mock_mining: bool,
2044
    /// If [`NodeConfig::mock_mining`] is enabled, this specifies an optional directory
2045
    /// path where the generated mock Stacks blocks will be saved. (pre-Nakamoto)
2046
    /// The path is canonicalized on load.
2047
    /// ---
2048
    /// @default: `None`
2049
    /// @deprecated: This setting was only used in the neon node and is ignored in Epoch 3.0+.
2050
    pub mock_mining_output_dir: Option<PathBuf>,
2051
    /// Enable microblock mining.
2052
    /// ---
2053
    /// @default: `true`
2054
    /// @deprecated: This setting is ignored in Epoch 2.5+.
2055
    pub mine_microblocks: bool,
2056
    /// How often to attempt producing microblocks, in milliseconds.
2057
    /// ---
2058
    /// @default: `30_000` (30 seconds)
2059
    /// @deprecated: This setting is ignored in Epoch 2.5+.
2060
    /// @notes:
2061
    ///   - Only applies when [`NodeConfig::mine_microblocks`] is true and before Epoch 2.5.
2062
    /// @units: milliseconds
2063
    pub microblock_frequency: u64,
2064
    /// The maximum number of microblocks allowed per Stacks block.
2065
    /// ---
2066
    /// @default: `65535` (u16::MAX)
2067
    /// @deprecated: This setting is ignored in Epoch 2.5+.
2068
    pub max_microblocks: u64,
2069
    /// Cooldown period after a microblock is produced, in milliseconds.
2070
    /// ---
2071
    /// @default: `30_000` (30 seconds)
2072
    /// @deprecated: This setting is ignored in Epoch 2.5+.
2073
    /// @notes:
2074
    ///   - Only applies when [`NodeConfig::mine_microblocks`] is true and before Epoch 2.5.
2075
    /// @units: milliseconds
2076
    pub wait_time_for_microblocks: u64,
2077
    /// When operating as a miner, this specifies the maximum time (in milliseconds)
2078
    /// the node waits after detecting a new burnchain block to synchronize corresponding
2079
    /// Stacks block data from the network before resuming mining attempts.
2080
    /// If synchronization doesn't complete within this duration, mining resumes anyway
2081
    /// to prevent stalling. This setting is loaded by all nodes but primarily affects
2082
    /// miner behavior within the relayer thread.
2083
    /// ---
2084
    /// @default: `30_000` (30 seconds)
2085
    /// @units: milliseconds
2086
    pub wait_time_for_blocks: u64,
2087
    /// Controls how frequently, in milliseconds, the Nakamoto miner's relay thread
2088
    /// polls for work or takes periodic actions when idle (e.g., checking for new
2089
    /// burnchain blocks). A default value of 10 seconds is reasonable on mainnet
2090
    /// (where bitcoin blocks are ~10 minutes). A lower value might be useful in
2091
    /// other environments with faster burn blocks.
2092
    /// ---
2093
    /// @default: `10_000` (10 seconds)
2094
    /// @units: milliseconds
2095
    pub next_initiative_delay: u64,
2096
    /// Optional network address and port (e.g., "127.0.0.1:9153") for binding the
2097
    /// Prometheus metrics server. If set, the node will start an HTTP server on this
2098
    /// address to expose internal metrics for scraping by a Prometheus instance.
2099
    /// ---
2100
    /// @default: `None` (Prometheus server disabled)
2101
    pub prometheus_bind: Option<String>,
2102
    /// The strategy to use for MARF trie node caching in memory.
2103
    /// Controls the trade-off between memory usage and performance for state access.
2104
    ///
2105
    /// Possible values:
2106
    /// - `"noop"`: No caching (least memory).
2107
    /// - `"everything"`: Cache all nodes (most memory, potentially fastest).
2108
    /// - `"node256"`: Cache only larger `TrieNode256` nodes.
2109
    ///
2110
    /// If the value is `None` or an unrecognized string, it defaults to `"noop"`.
2111
    /// ---
2112
    /// @default: `None` (effectively `"noop"`)
2113
    pub marf_cache_strategy: Option<String>,
2114
    /// Controls the timing of hash calculations for MARF trie nodes.
2115
    /// - If `true`, hashes are calculated only when the MARF is flushed to disk
2116
    ///   (deferred hashing).
2117
    /// - If `false`, hashes are calculated immediately as leaf nodes are inserted or
2118
    ///   updated (immediate hashing).
2119
    /// Deferred hashing might improve write performance.
2120
    /// ---
2121
    /// @default: `true`
2122
    pub marf_defer_hashing: bool,
2123
    /// Enables on-disk compression for MARF data structures to reduce disk space usage
2124
    /// for chainstate storage.
2125
    ///
2126
    /// When set to `true`, MARF trie nodes may be compressed
2127
    /// before being written to disk, trading slightly increased CPU overhead during
2128
    /// reads and writes for reduced storage requirements.
2129
    /// ---
2130
    /// @default: `true`
2131
    /// @notes:
2132
    ///   - Compression affects only the on-disk MARF representation; in-memory behavior
2133
    ///     remains unchanged.
2134
    pub marf_compress: bool,
2135
    /// Sampling interval in seconds for the PoX synchronization watchdog thread
2136
    /// (pre-Nakamoto). Determines how often the watchdog checked PoX state
2137
    /// consistency in the Neon run loop.
2138
    /// ---
2139
    /// @default: `30`
2140
    /// @units: seconds
2141
    /// @deprecated: Unused after the Nakamoto upgrade. This setting is ignored in Epoch 3.0+.
2142
    pub pox_sync_sample_secs: u64,
2143
    /// If set to `true`, the node initializes its state using an alternative test
2144
    /// genesis block definition, loading different initial balances, names, and
2145
    /// lockups than the standard network genesis.
2146
    /// ---
2147
    /// @default: `None` (uses standard network genesis)
2148
    /// @notes:
2149
    ///   - This is intended strictly for testing purposes and is disallowed on mainnet.
2150
    pub use_test_genesis_chainstate: Option<bool>,
2151
    /// Fault injection setting for testing purposes. If set to `Some(p)`, where `p` is
2152
    /// between 0 and 100, the node will have a `p` percent chance of intentionally
2153
    /// *not* pushing a newly processed block to its peers.
2154
    /// ---
2155
    /// @default: `None` (no fault injection)
2156
    /// @notes:
2157
    ///   - Values: 0-100 (percentage).
2158
    pub fault_injection_block_push_fail_probability: Option<u8>,
2159
    /// Fault injection setting for testing purposes. If `true`, the node's chainstate
2160
    /// database access layer may intentionally fail to retrieve block data, even if it
2161
    /// exists, simulating block hiding or data unavailability.
2162
    /// ---
2163
    /// @default: `false`
2164
    /// @notes:
2165
    ///   - This parameter cannot be set via the configuration file; it must be modified
2166
    ///     programmatically.
2167
    pub fault_injection_hide_blocks: bool,
2168
    /// The polling interval, in seconds, for the background thread that monitors
2169
    /// chain liveness. This thread periodically wakes up the main coordinator to
2170
    /// check for chain progress or other conditions requiring action.
2171
    /// ---
2172
    /// @default: `300` (5 minutes)
2173
    /// @units: seconds
2174
    pub chain_liveness_poll_time_secs: u64,
2175
    /// By default, HTTP requests to event observers block the operation of the node
2176
    /// until a successful response is received from the observer. This creates a
2177
    /// predictable order of operations, but it also means that an event observer that
2178
    /// is slow to respond might stall the node. This can be prevented by changing this
2179
    /// setting to `false`, in which case those requests are enqueued to be delivered
2180
    /// on a background thread. Only if the queue is full will new requests cause
2181
    /// blocking again. The size of that queue can be controlled with the
2182
    /// `event_dispatcher_queue_size` setting.
2183
    ///
2184
    /// Pending requests are persisted across restarts of the node. On restart, the
2185
    /// node will deliver all remaining event payloads before resuming normal operations.
2186
    /// ---
2187
    /// @default: `true`
2188
    pub event_dispatcher_blocking: bool,
2189
    /// This setting does nothing if the `event_dispatcher_blocking` has its default
2190
    /// value of `true`. But if the event dispatcher is set to be non-blocking, this
2191
    /// queue size controls how many events can be in-flight (not yet delivered) before
2192
    /// the event dispatcher becomes blocking again until space becomes available.
2193
    ///
2194
    /// Setting this value to `0` is equivalent to setting `event_dispatcher_blocking`
2195
    /// to `true`, as no in-flight requests are allowed.
2196
    /// ---
2197
    /// @default: `1_000`
2198
    pub event_dispatcher_queue_size: usize,
2199
    /// A list of specific StackerDB contracts (identified by their qualified contract
2200
    /// identifiers, e.g., "SP000000000000000000002Q6VF78.pox-3") that this node
2201
    /// should actively replicate.
2202
    /// ---
2203
    /// @default: |
2204
    ///   - if [`NodeConfig::miner`] is `true` or [`NodeConfig::stacker`] is `true`:
2205
    ///     relevant system contracts (e.g., `.miners`, `.signers-*`) are
2206
    ///     automatically added in addition to any contracts specified in the
2207
    ///     configuration file.
2208
    ///   - else: defaults to an empty list `[]`.
2209
    /// @notes:
2210
    ///   - Values are strings representing qualified contract identifiers.
2211
    /// @toml_example: |
2212
    ///   stacker_dbs = [
2213
    ///     "SP000000000000000000002Q6VF78.pox-3",
2214
    ///     "SP2C2YFP12AJZB4M4KUPSTMZQR0SNHNPH204SCQJM.stx-oracle-v1"
2215
    ///   ]
2216
    pub stacker_dbs: Vec<QualifiedContractIdentifier>,
2217
    /// Enables the transaction index, which maps transaction IDs to the blocks
2218
    /// containing them. Setting this to `true` allows the use of RPC endpoints
2219
    /// that look up transactions by ID (e.g., `/extended/v1/tx/{txid}`), but
2220
    /// requires substantial additional disk space for the index database.
2221
    /// ---
2222
    /// @default: `false`
2223
    pub txindex: bool,
2224
}
2225

2226
#[derive(Clone, Debug, Default)]
2227
pub enum CostEstimatorName {
2228
    #[default]
2229
    NaivePessimistic,
2230
}
2231

2232
#[derive(Clone, Debug, Default)]
2233
pub enum FeeEstimatorName {
2234
    #[default]
2235
    ScalarFeeRate,
2236
    FuzzedWeightedMedianFeeRate,
2237
}
2238

2239
#[derive(Clone, Debug, Default)]
2240
pub enum CostMetricName {
2241
    #[default]
2242
    ProportionDotProduct,
2243
}
2244

2245
impl CostEstimatorName {
2246
    fn panic_parse(s: String) -> CostEstimatorName {
×
2247
        if &s.to_lowercase() == "naive_pessimistic" {
×
2248
            CostEstimatorName::NaivePessimistic
×
2249
        } else {
2250
            panic!("Bad cost estimator name supplied in configuration file: {s}");
×
2251
        }
2252
    }
×
2253
}
2254

2255
impl FeeEstimatorName {
2256
    fn panic_parse(s: String) -> FeeEstimatorName {
×
2257
        if &s.to_lowercase() == "scalar_fee_rate" {
×
2258
            FeeEstimatorName::ScalarFeeRate
×
2259
        } else if &s.to_lowercase() == "fuzzed_weighted_median_fee_rate" {
×
2260
            FeeEstimatorName::FuzzedWeightedMedianFeeRate
×
2261
        } else {
2262
            panic!("Bad fee estimator name supplied in configuration file: {s}");
×
2263
        }
2264
    }
×
2265
}
2266

2267
impl CostMetricName {
2268
    fn panic_parse(s: String) -> CostMetricName {
×
2269
        if &s.to_lowercase() == "proportion_dot_product" {
×
2270
            CostMetricName::ProportionDotProduct
×
2271
        } else {
2272
            panic!("Bad cost metric name supplied in configuration file: {s}");
×
2273
        }
2274
    }
×
2275
}
2276

2277
#[derive(Clone, Debug)]
2278
pub struct FeeEstimationConfig {
2279
    pub cost_estimator: Option<CostEstimatorName>,
2280
    pub fee_estimator: Option<FeeEstimatorName>,
2281
    pub cost_metric: Option<CostMetricName>,
2282
    pub log_error: bool,
2283
    /// If using FeeRateFuzzer, the amount of random noise, as a percentage of the base value (in
2284
    /// [0, 1]) to add for fuzz. See comments on FeeRateFuzzer.
2285
    pub fee_rate_fuzzer_fraction: f64,
2286
    /// If using WeightedMedianFeeRateEstimator, the window size to use. See comments on
2287
    /// WeightedMedianFeeRateEstimator.
2288
    pub fee_rate_window_size: u64,
2289
}
2290

2291
impl Default for FeeEstimationConfig {
2292
    fn default() -> Self {
4,453✔
2293
        Self {
4,453✔
2294
            cost_estimator: Some(CostEstimatorName::default()),
4,453✔
2295
            fee_estimator: Some(FeeEstimatorName::default()),
4,453✔
2296
            cost_metric: Some(CostMetricName::default()),
4,453✔
2297
            log_error: false,
4,453✔
2298
            fee_rate_fuzzer_fraction: 0.1f64,
4,453✔
2299
            fee_rate_window_size: 5u64,
4,453✔
2300
        }
4,453✔
2301
    }
4,453✔
2302
}
2303

2304
impl From<FeeEstimationConfigFile> for FeeEstimationConfig {
2305
    fn from(f: FeeEstimationConfigFile) -> Self {
×
2306
        if let Some(true) = f.disabled {
×
2307
            return Self {
×
2308
                cost_estimator: None,
×
2309
                fee_estimator: None,
×
2310
                cost_metric: None,
×
2311
                log_error: false,
×
2312
                fee_rate_fuzzer_fraction: 0f64,
×
2313
                fee_rate_window_size: 0u64,
×
2314
            };
×
2315
        }
×
2316
        let cost_estimator = f
×
2317
            .cost_estimator
×
2318
            .map(CostEstimatorName::panic_parse)
×
2319
            .unwrap_or_default();
×
2320
        let fee_estimator = f
×
2321
            .fee_estimator
×
2322
            .map(FeeEstimatorName::panic_parse)
×
2323
            .unwrap_or_default();
×
2324
        let cost_metric = f
×
2325
            .cost_metric
×
2326
            .map(CostMetricName::panic_parse)
×
2327
            .unwrap_or_default();
×
2328
        let log_error = f.log_error.unwrap_or(false);
×
2329
        Self {
×
2330
            cost_estimator: Some(cost_estimator),
×
2331
            fee_estimator: Some(fee_estimator),
×
2332
            cost_metric: Some(cost_metric),
×
2333
            log_error,
×
2334
            fee_rate_fuzzer_fraction: f.fee_rate_fuzzer_fraction.unwrap_or(0.1f64),
×
2335
            fee_rate_window_size: f.fee_rate_window_size.unwrap_or(5u64),
×
2336
        }
×
2337
    }
×
2338
}
2339

2340
impl Config {
2341
    pub fn make_cost_estimator(&self) -> Option<Box<dyn CostEstimator>> {
2,302,983✔
2342
        let cost_estimator: Box<dyn CostEstimator> =
2,302,983✔
2343
            match self.estimation.cost_estimator.as_ref()? {
2,302,983✔
2344
                CostEstimatorName::NaivePessimistic => Box::new(
2,302,983✔
2345
                    self.estimation
2,302,983✔
2346
                        .make_pessimistic_cost_estimator(self.get_estimates_path()),
2,302,983✔
2347
                ),
2,302,983✔
2348
            };
2349

2350
        Some(cost_estimator)
2,302,983✔
2351
    }
2,302,983✔
2352

2353
    pub fn make_cost_metric(&self) -> Option<Box<dyn CostMetric>> {
2,307,771✔
2354
        let metric: Box<dyn CostMetric> = match self.estimation.cost_metric.as_ref()? {
2,307,771✔
2355
            CostMetricName::ProportionDotProduct => {
2356
                Box::new(ProportionalDotProduct::new(MAX_BLOCK_LEN as u64))
2,307,771✔
2357
            }
2358
        };
2359

2360
        Some(metric)
2,307,771✔
2361
    }
2,307,771✔
2362

2363
    pub fn make_fee_estimator(&self) -> Option<Box<dyn FeeEstimator>> {
9,882✔
2364
        let metric = self.make_cost_metric()?;
9,882✔
2365
        let fee_estimator: Box<dyn FeeEstimator> = match self.estimation.fee_estimator.as_ref()? {
9,882✔
2366
            FeeEstimatorName::ScalarFeeRate => self
9,846✔
2367
                .estimation
9,846✔
2368
                .make_scalar_fee_estimator(self.get_estimates_path(), metric),
9,846✔
2369
            FeeEstimatorName::FuzzedWeightedMedianFeeRate => self
36✔
2370
                .estimation
36✔
2371
                .make_fuzzed_weighted_median_fee_estimator(self.get_estimates_path(), metric),
36✔
2372
        };
2373

2374
        Some(fee_estimator)
9,882✔
2375
    }
9,882✔
2376
}
2377

2378
impl FeeEstimationConfig {
2379
    pub fn make_pessimistic_cost_estimator(
2,302,983✔
2380
        &self,
2,302,983✔
2381
        mut estimates_path: PathBuf,
2,302,983✔
2382
    ) -> PessimisticEstimator {
2,302,983✔
2383
        if let Some(CostEstimatorName::NaivePessimistic) = self.cost_estimator.as_ref() {
2,302,983✔
2384
            estimates_path.push("cost_estimator_pessimistic.sqlite");
2,302,983✔
2385
            PessimisticEstimator::open(&estimates_path, self.log_error)
2,302,983✔
2386
                .expect("Error opening cost estimator")
2,302,983✔
2387
        } else {
2388
            panic!("BUG: Expected to configure a naive pessimistic cost estimator");
×
2389
        }
2390
    }
2,302,983✔
2391

2392
    pub fn make_scalar_fee_estimator<CM: CostMetric + 'static>(
9,846✔
2393
        &self,
9,846✔
2394
        mut estimates_path: PathBuf,
9,846✔
2395
        metric: CM,
9,846✔
2396
    ) -> Box<dyn FeeEstimator> {
9,846✔
2397
        if let Some(FeeEstimatorName::ScalarFeeRate) = self.fee_estimator.as_ref() {
9,846✔
2398
            estimates_path.push("fee_estimator_scalar_rate.sqlite");
9,846✔
2399
            Box::new(
9,846✔
2400
                ScalarFeeRateEstimator::open(&estimates_path, metric)
9,846✔
2401
                    .expect("Error opening fee estimator"),
9,846✔
2402
            )
9,846✔
2403
        } else {
2404
            panic!("BUG: Expected to configure a scalar fee estimator");
×
2405
        }
2406
    }
9,846✔
2407

2408
    // Creates a fuzzed WeightedMedianFeeRateEstimator with window_size 5. The fuzz
2409
    // is uniform with bounds [+/- 0.5].
2410
    pub fn make_fuzzed_weighted_median_fee_estimator<CM: CostMetric + 'static>(
36✔
2411
        &self,
36✔
2412
        mut estimates_path: PathBuf,
36✔
2413
        metric: CM,
36✔
2414
    ) -> Box<dyn FeeEstimator> {
36✔
2415
        if let Some(FeeEstimatorName::FuzzedWeightedMedianFeeRate) = self.fee_estimator.as_ref() {
36✔
2416
            estimates_path.push("fee_fuzzed_weighted_median.sqlite");
36✔
2417
            let underlying_estimator = WeightedMedianFeeRateEstimator::open(
36✔
2418
                &estimates_path,
36✔
2419
                metric,
36✔
2420
                self.fee_rate_window_size
36✔
2421
                    .try_into()
36✔
2422
                    .expect("Configured fee rate window size out of bounds."),
36✔
2423
            )
2424
            .expect("Error opening fee estimator");
36✔
2425
            Box::new(FeeRateFuzzer::new(
36✔
2426
                underlying_estimator,
36✔
2427
                self.fee_rate_fuzzer_fraction,
36✔
2428
            ))
36✔
2429
        } else {
2430
            panic!("BUG: Expected to configure a weighted median fee estimator");
×
2431
        }
2432
    }
36✔
2433
}
2434

2435
impl Default for NodeConfig {
2436
    fn default() -> Self {
4,453✔
2437
        let mut rng = rand::thread_rng();
4,453✔
2438
        let mut buf = [0u8; 8];
4,453✔
2439
        rng.fill_bytes(&mut buf);
4,453✔
2440

2441
        let now = get_epoch_time_ms();
4,453✔
2442
        let testnet_id = format!("stacks-node-{now}");
4,453✔
2443

2444
        let rpc_port = 20443;
4,453✔
2445
        let p2p_port = 20444;
4,453✔
2446

2447
        let mut local_peer_seed = [0u8; 32];
4,453✔
2448
        rng.fill_bytes(&mut local_peer_seed);
4,453✔
2449

2450
        let mut seed = [0u8; 32];
4,453✔
2451
        rng.fill_bytes(&mut seed);
4,453✔
2452

2453
        let name = "helium-node";
4,453✔
2454
        NodeConfig {
4,453✔
2455
            name: name.to_string(),
4,453✔
2456
            seed: seed.to_vec(),
4,453✔
2457
            working_dir: format!("/tmp/{testnet_id}"),
4,453✔
2458
            rpc_bind: format!("0.0.0.0:{rpc_port}"),
4,453✔
2459
            p2p_bind: format!("0.0.0.0:{p2p_port}"),
4,453✔
2460
            data_url: format!("http://127.0.0.1:{rpc_port}"),
4,453✔
2461
            p2p_address: format!("127.0.0.1:{rpc_port}"),
4,453✔
2462
            bootstrap_node: vec![],
4,453✔
2463
            deny_nodes: vec![],
4,453✔
2464
            local_peer_seed: local_peer_seed.to_vec(),
4,453✔
2465
            miner: false,
4,453✔
2466
            stacker: false,
4,453✔
2467
            mock_mining: false,
4,453✔
2468
            mock_mining_output_dir: None,
4,453✔
2469
            mine_microblocks: true,
4,453✔
2470
            microblock_frequency: 30_000,
4,453✔
2471
            max_microblocks: u16::MAX as u64,
4,453✔
2472
            wait_time_for_microblocks: 30_000,
4,453✔
2473
            wait_time_for_blocks: 30_000,
4,453✔
2474
            next_initiative_delay: 10_000,
4,453✔
2475
            prometheus_bind: None,
4,453✔
2476
            marf_cache_strategy: None,
4,453✔
2477
            marf_defer_hashing: true,
4,453✔
2478
            marf_compress: true,
4,453✔
2479
            pox_sync_sample_secs: 30,
4,453✔
2480
            use_test_genesis_chainstate: None,
4,453✔
2481
            fault_injection_block_push_fail_probability: None,
4,453✔
2482
            fault_injection_hide_blocks: false,
4,453✔
2483
            chain_liveness_poll_time_secs: 300,
4,453✔
2484
            event_dispatcher_blocking: true,
4,453✔
2485
            event_dispatcher_queue_size: 1000,
4,453✔
2486
            stacker_dbs: vec![],
4,453✔
2487
            txindex: false,
4,453✔
2488
        }
4,453✔
2489
    }
4,453✔
2490
}
2491

2492
impl NodeConfig {
2493
    /// Get a SocketAddr for this node's RPC endpoint which uses the loopback address
2494
    pub fn get_rpc_loopback(&self) -> Option<SocketAddr> {
44,001✔
2495
        let rpc_port = self.rpc_bind_addr()?.port();
44,001✔
2496
        Some(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), rpc_port))
44,001✔
2497
    }
44,001✔
2498

2499
    pub fn rpc_bind_addr(&self) -> Option<SocketAddr> {
48,357✔
2500
        SocketAddr::from_str(&self.rpc_bind)
48,357✔
2501
            .inspect_err(|e| {
48,357✔
2502
                error!("Could not parse node.rpc_bind configuration setting as SocketAddr: {e}");
×
2503
            })
×
2504
            .ok()
48,357✔
2505
    }
48,357✔
2506

2507
    pub fn p2p_bind_addr(&self) -> Option<SocketAddr> {
2,178✔
2508
        SocketAddr::from_str(&self.p2p_bind)
2,178✔
2509
            .inspect_err(|e| {
2,178✔
2510
                error!("Could not parse node.rpc_bind configuration setting as SocketAddr: {e}");
×
2511
            })
×
2512
            .ok()
2,178✔
2513
    }
2,178✔
2514

2515
    pub fn add_signers_stackerdbs(&mut self, is_mainnet: bool) {
1,782✔
2516
        for signer_set in 0..2 {
3,564✔
2517
            for message_id in 0..SIGNER_SLOTS_PER_USER {
46,332✔
2518
                let contract_name = NakamotoSigners::make_signers_db_name(signer_set, message_id);
46,332✔
2519
                let contract_id = boot_code_id(contract_name.as_str(), is_mainnet);
46,332✔
2520
                if !self.stacker_dbs.contains(&contract_id) {
46,332✔
2521
                    self.stacker_dbs.push(contract_id);
46,332✔
2522
                }
46,332✔
2523
            }
2524
        }
2525
    }
1,782✔
2526

2527
    pub fn add_miner_stackerdb(&mut self, is_mainnet: bool) {
1,782✔
2528
        let miners_contract_id = boot_code_id(MINERS_NAME, is_mainnet);
1,782✔
2529
        if !self.stacker_dbs.contains(&miners_contract_id) {
1,782✔
2530
            self.stacker_dbs.push(miners_contract_id);
1,782✔
2531
        }
1,782✔
2532
    }
1,782✔
2533

2534
    fn default_neighbor(
450✔
2535
        addr: SocketAddr,
450✔
2536
        pubk: Secp256k1PublicKey,
450✔
2537
        chain_id: u32,
450✔
2538
        peer_version: u32,
450✔
2539
    ) -> Neighbor {
450✔
2540
        Neighbor {
450✔
2541
            addr: NeighborKey {
450✔
2542
                peer_version,
450✔
2543
                network_id: chain_id,
450✔
2544
                addrbytes: PeerAddress::from_socketaddr(&addr),
450✔
2545
                port: addr.port(),
450✔
2546
            },
450✔
2547
            public_key: pubk,
450✔
2548
            expire_block: 9999999,
450✔
2549
            last_contact_time: 0,
450✔
2550
            allowed: 0,
450✔
2551
            denied: 0,
450✔
2552
            asn: 0,
450✔
2553
            org: 0,
450✔
2554
            in_degree: 0,
450✔
2555
            out_degree: 0,
450✔
2556
        }
450✔
2557
    }
450✔
2558

2559
    pub fn add_bootstrap_node(&mut self, bootstrap_node: &str, chain_id: u32, peer_version: u32) {
450✔
2560
        let parts: Vec<&str> = bootstrap_node.split('@').collect();
450✔
2561
        let Ok(parts) = TryInto::<&[_; 2]>::try_into(parts.as_slice()) else {
450✔
2562
            panic!("Invalid bootstrap node '{bootstrap_node}': expected PUBKEY@IP:PORT");
×
2563
        };
2564
        let (pubkey_str, hostport) = (parts[0], parts[1]);
450✔
2565
        let pubkey = Secp256k1PublicKey::from_hex(pubkey_str)
450✔
2566
            .unwrap_or_else(|_| panic!("Invalid public key '{pubkey_str}'"));
450✔
2567
        debug!("Resolve '{hostport}'");
450✔
2568

2569
        let mut attempts = 0;
450✔
2570
        let max_attempts = 5;
450✔
2571
        let mut delay = Duration::from_secs(2);
450✔
2572

2573
        let sockaddr = loop {
450✔
2574
            match hostport.to_socket_addrs() {
450✔
2575
                Ok(mut addrs) => {
450✔
2576
                    if let Some(addr) = addrs.next() {
450✔
2577
                        break addr;
450✔
2578
                    } else {
2579
                        panic!("No addresses found for '{hostport}'");
×
2580
                    }
2581
                }
2582
                Err(e) => {
×
2583
                    if attempts >= max_attempts {
×
2584
                        panic!("Failed to resolve '{hostport}' after {max_attempts} attempts: {e}");
×
2585
                    } else {
2586
                        error!(
×
2587
                            "Attempt {} - Failed to resolve '{hostport}': {e}. Retrying in {delay:?}...",
2588
                            attempts + 1,
×
2589
                        );
2590
                        thread::sleep(delay);
×
2591
                        attempts += 1;
×
2592
                        delay *= 2;
×
2593
                    }
2594
                }
2595
            }
2596
        };
2597

2598
        let neighbor = NodeConfig::default_neighbor(sockaddr, pubkey, chain_id, peer_version);
450✔
2599
        self.bootstrap_node.push(neighbor);
450✔
2600
    }
450✔
2601

2602
    pub fn set_bootstrap_nodes(
369✔
2603
        &mut self,
369✔
2604
        bootstrap_nodes: String,
369✔
2605
        chain_id: u32,
369✔
2606
        peer_version: u32,
369✔
2607
    ) {
369✔
2608
        for part in bootstrap_nodes.split(',') {
369✔
2609
            if !part.is_empty() {
369✔
2610
                self.add_bootstrap_node(part, chain_id, peer_version);
369✔
2611
            }
369✔
2612
        }
2613
    }
369✔
2614

2615
    pub fn add_deny_node(&mut self, deny_node: &str, chain_id: u32, peer_version: u32) {
×
2616
        let sockaddr = deny_node.to_socket_addrs().unwrap().next().unwrap();
×
2617
        let neighbor = NodeConfig::default_neighbor(
×
2618
            sockaddr,
×
2619
            Secp256k1PublicKey::from_private(&Secp256k1PrivateKey::random()),
×
2620
            chain_id,
×
2621
            peer_version,
×
2622
        );
2623
        self.deny_nodes.push(neighbor);
×
2624
    }
×
2625

2626
    pub fn set_deny_nodes(&mut self, deny_nodes: String, chain_id: u32, peer_version: u32) {
×
2627
        for part in deny_nodes.split(',') {
×
2628
            if !part.is_empty() {
×
2629
                self.add_deny_node(part, chain_id, peer_version);
×
2630
            }
×
2631
        }
2632
    }
×
2633

2634
    pub fn get_marf_opts(&self) -> MARFOpenOpts {
16,042,808✔
2635
        let hash_mode = if self.marf_defer_hashing {
16,042,808✔
2636
            TrieHashCalculationMode::Deferred
16,042,807✔
2637
        } else {
2638
            TrieHashCalculationMode::Immediate
1✔
2639
        };
2640

2641
        MARFOpenOpts::new(
16,042,808✔
2642
            hash_mode,
16,042,808✔
2643
            self.marf_cache_strategy.as_deref().unwrap_or("noop"),
16,042,808✔
2644
            false,
2645
        )
2646
        .with_compression(self.marf_compress)
16,042,808✔
2647
    }
16,042,808✔
2648

2649
    pub fn effective_event_dispatcher_queue_size(&self) -> usize {
295✔
2650
        if self.event_dispatcher_blocking {
295✔
2651
            0
295✔
2652
        } else {
2653
            self.event_dispatcher_queue_size
×
2654
        }
2655
    }
295✔
2656
}
2657

2658
#[derive(Clone, Debug, PartialEq)]
2659
pub struct MinerConfig {
2660
    /// Time to wait (in milliseconds) before the first attempt to mine a block.
2661
    /// ---
2662
    /// @default: `10`
2663
    /// @units: milliseconds
2664
    /// @deprecated: This setting is ignored in Epoch 3.0+. Only used in the neon chain mode.
2665
    pub first_attempt_time_ms: u64,
2666
    /// Time to wait (in milliseconds) for subsequent attempts to mine a block,
2667
    /// after the first attempt fails.
2668
    /// ---
2669
    /// @default: `120_000` (2 minutes)
2670
    /// @units: milliseconds
2671
    /// @deprecated: This setting is ignored in Epoch 3.0+. Only used in the neon chain mode.
2672
    pub subsequent_attempt_time_ms: u64,
2673
    /// Time to wait (in milliseconds) to mine a microblock.
2674
    /// ---
2675
    /// @default: `30_000` (30 seconds)
2676
    /// @units: milliseconds
2677
    /// @deprecated: This setting is ignored in Epoch 3.0+. Only used in the neon chain mode.
2678
    pub microblock_attempt_time_ms: u64,
2679
    /// Maximum time (in milliseconds) the miner spends selecting transactions from
2680
    /// the mempool when assembling a Nakamoto block. Once this duration is exceeded,
2681
    /// the miner stops adding transactions and finalizes the block with those
2682
    /// already selected.
2683
    /// ---
2684
    /// @default: `5_000` (5 seconds)
2685
    /// @units: milliseconds
2686
    pub nakamoto_attempt_time_ms: u64,
2687
    /// Strategy for selecting the next transaction candidate from the mempool.
2688
    /// Controls prioritization between maximizing immediate fee capture vs. ensuring
2689
    /// transaction nonce order for account progression and processing efficiency.
2690
    ///
2691
    /// See [`MemPoolWalkStrategy`] for variant details.
2692
    ///
2693
    /// Possible values (use variant names for configuration):
2694
    /// - `"GlobalFeeRate"`: Selects the transaction with the highest fee rate globally.
2695
    /// - `"NextNonceWithHighestFeeRate"`: Selects the highest-fee transaction among those
2696
    ///   matching the next expected nonce for sender/sponsor accounts.
2697
    /// ---
2698
    /// @default: `"NextNonceWithHighestFeeRate"`
2699
    pub mempool_walk_strategy: MemPoolWalkStrategy,
2700
    /// Probability (percentage, 0-100) of prioritizing a transaction without a
2701
    /// known fee rate during candidate selection.
2702
    ///
2703
    /// Only effective when `mempool_walk_strategy` is `GlobalFeeRate`. Helps ensure
2704
    /// transactions lacking fee estimates are periodically considered alongside
2705
    /// high-fee ones, preventing potential starvation. A value of 0 means never
2706
    /// prioritize them first, 100 means always prioritize them first (if available).
2707
    /// ---
2708
    /// @default: `25` (25% chance)
2709
    /// @units: percent
2710
    /// @notes:
2711
    ///   - Values: 0-100.
2712
    pub probability_pick_no_estimate_tx: u8,
2713
    /// Optional recipient for the coinbase block reward, overriding the default miner address.
2714
    ///
2715
    /// By default (`None`), the reward is sent to the miner's primary address
2716
    /// ([`NodeConfig::seed`]). If set to some principal address *and* the current
2717
    /// Stacks epoch is > 2.1, the reward will be directed to the specified
2718
    /// address instead.
2719
    /// ---
2720
    /// @default: `None`
2721
    pub block_reward_recipient: Option<PrincipalData>,
2722
    /// If possible, mine with a p2wpkh address.
2723
    /// ---
2724
    /// @default: `false`
2725
    pub segwit: bool,
2726
    /// Wait for a downloader pass before mining.
2727
    /// This can only be disabled in testing; it can't be changed in the config file.
2728
    /// ---
2729
    /// @default: `true`
2730
    pub wait_for_block_download: bool,
2731
    /// Max size (in bytes) of the in-memory cache for storing expected account nonces.
2732
    ///
2733
    /// This cache accelerates mempool processing (e.g., during block building) by
2734
    /// storing the anticipated next nonce for accounts, reducing expensive lookups
2735
    /// into the node's state (MARF trie). A larger cache can improve performance
2736
    /// for workloads involving many unique accounts but increases memory consumption.
2737
    /// ---
2738
    /// @default: `1048576` (1 MiB)
2739
    /// @units: bytes
2740
    /// @notes:
2741
    ///   - Must be configured to a value greater than 0.
2742
    pub nonce_cache_size: usize,
2743
    /// Max size (in *number* of items) of transaction candidates to hold in the in-memory
2744
    /// retry cache.
2745
    ///
2746
    /// This cache stores transactions encountered during a `GlobalFeeRate` mempool
2747
    /// walk whose nonces are currently too high for immediate processing. These
2748
    /// candidates are prioritized for reconsideration later within the *same* walk,
2749
    /// potentially becoming valid if other processed transactions update the
2750
    /// expected nonces.
2751
    ///
2752
    /// A larger cache retains more potentially valid future candidates but uses more
2753
    /// memory. This setting is primarily relevant for the `GlobalFeeRate` strategy.
2754
    /// ---
2755
    /// @default: `1048576`
2756
    /// @units: items
2757
    /// @notes:
2758
    ///   - Each element [`crate::core::mempool::MemPoolTxInfoPartial`] is currently 112 bytes.
2759
    pub candidate_retry_cache_size: usize,
2760
    /// Amount of time (in seconds) to wait for unprocessed blocks before mining a new block.
2761
    /// ---
2762
    /// @default: `30`
2763
    /// @units: seconds
2764
    /// @deprecated: This setting is ignored in Epoch 3.0+. Only used in the neon chain mode.
2765
    pub unprocessed_block_deadline_secs: u64,
2766
    /// The private key (Secp256k1) used for signing blocks, provided as a hex string.
2767
    ///
2768
    /// This key must be present at runtime for mining operations to succeed.
2769
    /// ---
2770
    /// @default: |
2771
    ///   - if the `[miner]` section *is present* in the config file: [`NodeConfig::seed`]
2772
    ///   - else: `None`
2773
    pub mining_key: Option<Secp256k1PrivateKey>,
2774
    /// Amount of time while mining in nakamoto to wait in between mining interim blocks.
2775
    /// ---
2776
    /// @default: `None`
2777
    /// @deprecated: Use `min_time_between_blocks_ms` instead.
2778
    pub wait_on_interim_blocks: Option<Duration>,
2779
    /// Minimum number of transactions that must be in a block if we're going to
2780
    /// replace a pending block-commit with a new block-commit.
2781
    /// ---
2782
    /// @default: `0`
2783
    /// @deprecated: This setting is ignored in Epoch 3.0+. Only used in the neon chain mode.
2784
    pub min_tx_count: u64,
2785
    /// If true, requires subsequent mining attempts for the same block height to have
2786
    /// a transaction count >= the previous best attempt.
2787
    /// ---
2788
    /// @default: `false`
2789
    /// @deprecated: This setting is ignored in Epoch 3.0+. Only used in the neon chain mode.
2790
    pub only_increase_tx_count: bool,
2791
    /// Optional path to an external helper script for fetching unconfirmed
2792
    /// block-commits. Used to inform the miner's dynamic burn fee bidding strategy
2793
    /// with off-chain data.
2794
    ///
2795
    /// If a path is provided, the target script must:
2796
    /// - Be executable by the user running the Stacks node process.
2797
    /// - Accept a list of active miner burnchain addresses as command-line arguments.
2798
    /// - On successful execution, print a JSON array representing `Vec<UnconfirmedBlockCommit>`
2799
    ///   (see [`stacks::config::chain_data::UnconfirmedBlockCommit`] struct) to stdout.
2800
    /// - Exit with code 0 on success.
2801
    ///
2802
    /// Look at `test_get_unconfirmed_commits` in `stackslib/src/config/chain_data.rs`
2803
    /// for an example script.
2804
    /// ---
2805
    /// @default: `None` (feature disabled).
2806
    /// @deprecated: This setting is ignored in Epoch 3.0+. Only used in the neon chain mode
2807
    ///   and by the `get-spend-amount` cli subcommand.
2808
    pub unconfirmed_commits_helper: Option<String>,
2809
    /// The minimum win probability this miner aims to achieve in block sortitions.
2810
    ///
2811
    /// This target is used to detect prolonged periods of underperformance. If the
2812
    /// miner's calculated win probability consistently falls below this value for a
2813
    /// duration specified by [`MinerConfig::underperform_stop_threshold`] (after
2814
    /// an initial startup phase), the miner may cease spending in subsequent
2815
    /// sortitions (returning a burn fee cap of 0) to conserve resources.
2816
    ///
2817
    /// Setting this value close to 0.0 effectively disables the underperformance check.
2818
    /// ---
2819
    /// @default: `0.0`
2820
    /// @deprecated: This setting is ignored in Epoch 3.0+. Only used in the neon chain mode.
2821
    pub target_win_probability: f64,
2822
    /// Path to a file for storing and loading the currently active, registered VRF leader key.
2823
    ///
2824
    /// Loading: On startup or when needing to register a key, if this path is set,
2825
    /// the relayer first attempts to load a serialized [`RegisteredKey`] from this
2826
    /// file. If successful, it uses the loaded key and skips the on-chain VRF key
2827
    /// registration transaction, saving time and fees.
2828
    /// Saving: After a new VRF key registration transaction is confirmed and
2829
    /// activated on the burnchain, if this path is set, the node saves the details
2830
    /// of the newly activated [`RegisteredKey`] to this file. This allows the
2831
    /// miner to persist its active VRF key across restarts.
2832
    /// If the file doesn't exist during load, or the path is `None`, the node
2833
    /// proceeds with a new registration.
2834
    /// ---
2835
    /// @default: `None`
2836
    pub activated_vrf_key_path: Option<String>,
2837
    /// Controls how the miner estimates its win probability when checking for underperformance.
2838
    ///
2839
    /// This estimation is used in conjunction with [`MinerConfig::target_win_probability`] and
2840
    /// [`MinerConfig::underperform_stop_threshold`] to decide whether to pause
2841
    /// mining due to low predicted success rate.
2842
    ///
2843
    /// - If `true`: The win probability estimation looks at projected spend
2844
    ///   distributions ~6 blocks into the future. This might help the miner adjust
2845
    ///   its spending more quickly based on anticipated competition changes.
2846
    /// - If `false`: The win probability estimation uses the currently observed
2847
    ///   spend distribution for the next block.
2848
    /// ---
2849
    /// @default: `false`
2850
    /// @deprecated: This setting is ignored in Epoch 3.0+. Only used in the neon chain mode and by the
2851
    ///   `get-spend-amount` cli subcommand.
2852
    pub fast_rampup: bool,
2853
    /// The maximum number of consecutive Bitcoin blocks the miner will tolerate
2854
    /// underperforming (i.e., having a calculated win probability below
2855
    /// [`MinerConfig::target_win_probability`]) before temporarily pausing mining efforts.
2856
    ///
2857
    /// This check is only active after an initial startup phase (6 blocks past the
2858
    /// mining start height). If the miner underperforms for this number of
2859
    /// consecutive blocks, the [`BlockMinerThread::get_mining_spend_amount`] function
2860
    /// will return 0, effectively preventing the miner from submitting a block commit
2861
    /// for the current sortition to conserve funds.
2862
    /// ---
2863
    /// @default: `None` (underperformance check is disabled).
2864
    /// @deprecated: This setting is ignored in Epoch 3.0+. Only used in the neon chain mode.
2865
    pub underperform_stop_threshold: Option<u64>,
2866
    /// Specifies which types of transactions the miner should consider including in a
2867
    /// block during the mempool walk process. Transactions of types not included in
2868
    /// this set will be skipped.
2869
    ///
2870
    /// This allows miners to exclude specific transaction categories.
2871
    /// Configured as a comma-separated string of transaction type names in the configuration file.
2872
    ///
2873
    /// Accepted values correspond to variants of [`MemPoolWalkTxTypes`]:
2874
    /// - `"TokenTransfer"`
2875
    /// - `"SmartContract"`
2876
    /// - `"ContractCall"`
2877
    /// ---
2878
    /// @default: All transaction types are considered (equivalent to [`MemPoolWalkTxTypes::all()`]).
2879
    /// @toml_example: |
2880
    ///   txs_to_consider = "TokenTransfer,ContractCall"
2881
    pub txs_to_consider: HashSet<MemPoolWalkTxTypes>,
2882
    /// A comma separated list of Stacks addresses to whitelist so that only
2883
    /// transactions from these addresses should be considered during the mempool walk
2884
    /// for block building. If this list is non-empty, any transaction whose origin
2885
    /// address is *not* in this set will be skipped.
2886
    ///
2887
    /// This allows miners to prioritize transactions originating from specific accounts that are
2888
    /// important to them.
2889
    /// Configured as a comma-separated string of standard Stacks addresses
2890
    /// (e.g., "ST123...,ST456...") in the configuration file.
2891
    /// ---
2892
    /// @default: Empty set (all origins are considered).
2893
    /// @toml_example: |
2894
    ///   filter_origins = "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2,ST319CF5WV77KYR1H3GT0GZ7B8Q4AQPY42ETP1VPF"
2895
    pub filter_origins: HashSet<StacksAddress>,
2896
    /// Defines the maximum depth (in Stacks blocks) the miner considers when
2897
    /// evaluating potential chain tips when selecting the best tip to mine the next
2898
    /// block on.
2899
    ///
2900
    /// The miner analyzes candidate tips within this depth from the highest known
2901
    /// tip. It selects the "nicest" tip, often defined as the one that minimizes
2902
    /// chain reorganizations or orphans within this lookback window. A lower value
2903
    /// restricts the analysis to shallower forks, while a higher value considers
2904
    /// deeper potential reorganizations.
2905
    ///
2906
    /// This setting influences which fork the miner chooses to build upon if multiple valid tips exist.
2907
    /// ---
2908
    /// @default: `3`
2909
    /// @deprecated: This setting is ignored in Epoch 3.0+. Only used in the neon chain mode and the
2910
    ///   `pick-best-tip` cli subcommand.
2911
    pub max_reorg_depth: u64,
2912
    /// Enables a mock signing process for testing purposes, specifically designed
2913
    /// for use during Epoch 2.5 before the activation of Nakamoto consensus.
2914
    ///
2915
    /// When set to `true` and [`MinerConfig::mining_key`] is provided, the miner
2916
    /// will interact with the `.miners` and `.signers` contracts via the stackerdb
2917
    /// to send and receive mock proposals and signatures, simulating aspects of the
2918
    /// Nakamoto leader election and block signing flow.
2919
    /// ---
2920
    /// @default: `false` (Should only default true if [`MinerConfig::mining_key`] is set).
2921
    /// @deprecated: This setting is ignored in Epoch 3.0+.
2922
    /// @notes:
2923
    ///   - This is intended strictly for testing Epoch 2.5 conditions.
2924
    pub pre_nakamoto_mock_signing: bool,
2925
    /// The minimum time to wait between mining blocks in milliseconds. The value
2926
    /// must be greater than or equal to 1000 ms because if a block is mined
2927
    /// within the same second as its parent, it will be rejected by the signers.
2928
    ///
2929
    /// This check ensures compliance with signer rules that prevent blocks with
2930
    /// identical timestamps (at second resolution) to their parents. If a lower
2931
    /// value is configured, 1000 ms is used instead.
2932
    /// ---
2933
    /// @default: [`DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS`]
2934
    /// @units: milliseconds
2935
    pub min_time_between_blocks_ms: u64,
2936
    /// The amount of time in milliseconds that the miner should sleep in between
2937
    /// attempts to mine a block when the mempool is empty.
2938
    ///
2939
    /// This prevents the miner from busy-looping when there are no pending
2940
    /// transactions, conserving CPU resources. During this sleep, the miner still
2941
    /// checks burnchain tip changes.
2942
    /// ---
2943
    /// @default: [`DEFAULT_EMPTY_MEMPOOL_SLEEP_MS`]
2944
    /// @units: milliseconds
2945
    pub empty_mempool_sleep_time: Duration,
2946
    /// Time in milliseconds to pause after receiving the first threshold rejection,
2947
    /// before proposing a new block.
2948
    ///
2949
    /// When a miner's block proposal fails to gather enough signatures from the
2950
    /// signers for the first time at a given height, the miner will pause for this
2951
    /// duration before attempting to mine and propose again.
2952
    /// ---
2953
    /// @default: [`DEFAULT_FIRST_REJECTION_PAUSE_MS`]
2954
    /// @units: milliseconds
2955
    pub first_rejection_pause_ms: u64,
2956
    /// Time in milliseconds to pause after receiving subsequent threshold rejections,
2957
    /// before proposing a new block.
2958
    ///
2959
    /// If a miner's block proposal is rejected multiple times at the same height
2960
    /// (after the first rejection), this potentially longer pause duration is used
2961
    /// before retrying. This gives more significant time for network state changes
2962
    /// or signer coordination.
2963
    /// ---
2964
    /// @default: [`DEFAULT_SUBSEQUENT_REJECTION_PAUSE_MS`]
2965
    /// @units: milliseconds
2966
    pub subsequent_rejection_pause_ms: u64,
2967
    /// Time in milliseconds to wait for a Nakamoto block after seeing a burnchain
2968
    /// block before submitting a block commit.
2969
    ///
2970
    /// After observing a new burnchain block, the miner's relayer waits for this
2971
    /// duration before submitting its next block commit transaction to Bitcoin.
2972
    /// This delay provides an opportunity for a new Nakamoto block (produced by the
2973
    /// winner of the latest sortition) to arrive. Waiting helps avoid situations
2974
    /// where the relayer immediately submits a commit that needs to be replaced
2975
    /// via RBF if a new Stacks block appears shortly after. This delay is skipped
2976
    /// if the new burnchain blocks leading to the tip contain no sortitions.
2977
    /// ---
2978
    /// @default: [`DEFAULT_BLOCK_COMMIT_DELAY_MS`]
2979
    /// @units: milliseconds
2980
    pub block_commit_delay: Duration,
2981
    /// The percentage of the remaining tenure cost limit to consume each block.
2982
    ///
2983
    /// This setting limits the execution cost (Clarity cost) a single Nakamoto block
2984
    /// can incur, expressed as a percentage of the *remaining* cost budget for the
2985
    /// current mining tenure. For example, if set to 25, a block can use at most
2986
    /// 25% of the tenure's currently available cost limit. This allows miners to
2987
    /// spread the tenure's total execution budget across multiple blocks rather than
2988
    /// potentially consuming it all in the first block.
2989
    /// ---
2990
    /// @default: [`DEFAULT_TENURE_COST_LIMIT_PER_BLOCK_PERCENTAGE`]
2991
    /// @units: percent
2992
    /// @notes:
2993
    ///   - Values: 1-100.
2994
    ///   - Setting to 100 effectively disables this per-block limit, allowing a block to use the
2995
    ///     entire remaining tenure budget.
2996
    pub tenure_cost_limit_per_block_percentage: Option<u8>,
2997
    /// The percentage of a block’s execution cost limit at which the miner changes
2998
    /// transaction selection behavior for non-boot contract calls.
2999
    ///
3000
    /// When the total cost of included transactions in the current block reaches this
3001
    /// percentage of the block’s maximum execution cost (Clarity cost), and the next
3002
    /// available **non-bootcode** contract call in the mempool would cause a
3003
    /// `BlockTooBigError`, the miner will stop attempting to include additional
3004
    /// non-boot contract calls. Instead, it will consider only STX transfers and
3005
    /// boot contract calls for the remainder of the block budget.
3006
    ///
3007
    /// This allows miners to avoid repeatedly attempting to fit large non-boot
3008
    /// contract calls late in block assembly when space is tight, improving block
3009
    /// packing efficiency and ensuring other transaction types are not starved.
3010
    ///
3011
    /// ---
3012
    /// @default: [`DEFAULT_CONTRACT_COST_LIMIT_PERCENTAGE`]
3013
    /// @units: percent
3014
    /// @notes:
3015
    ///   - Values: 0–100.
3016
    ///   - Setting to 100 effectively disables this behavior, allowing miners to
3017
    ///     attempt non-boot contract calls until the block is full.
3018
    ///   - This setting only affects **non-boot** contract calls; boot contract calls
3019
    ///     and STX transfers are unaffected.
3020
    pub contract_cost_limit_percentage: Option<u8>,
3021
    /// Duration to wait in-between polling the sortition DB to see if we need to
3022
    /// extend the ongoing tenure (e.g. because the current sortition is empty or invalid).
3023
    ///
3024
    /// After the relayer determines that a tenure extension might be needed but
3025
    /// cannot proceed immediately (e.g., because a miner thread is already active
3026
    /// for the current burn view), it will wait for this duration before
3027
    /// re-checking the conditions for tenure extension.
3028
    /// ---
3029
    /// @default: [`DEFAULT_TENURE_EXTEND_POLL_SECS`]
3030
    /// @units: seconds
3031
    pub tenure_extend_poll_timeout: Duration,
3032
    /// Duration to wait before trying to continue a tenure because the next miner
3033
    /// did not produce blocks.
3034
    ///
3035
    /// If the node was the winner of the previous sortition but not the most recent
3036
    /// one, the relayer waits for this duration before attempting to extend its own
3037
    /// tenure. This gives the new winner of the most recent sortition a grace period
3038
    /// to produce their first block. Also used in scenarios with empty sortitions
3039
    /// to give the winner of the *last valid* sortition time to produce a block
3040
    /// before the current miner attempts an extension.
3041
    /// ---
3042
    /// @default: [`DEFAULT_TENURE_EXTEND_WAIT_MS`]
3043
    /// @units: milliseconds
3044
    pub tenure_extend_wait_timeout: Duration,
3045
    /// Duration to wait before attempting to issue a time-based tenure extend.
3046
    ///
3047
    /// A miner can proactively attempt to extend its tenure if a significant amount
3048
    /// of time has passed since the last tenure change, even without an explicit
3049
    /// trigger like an empty sortition. If the time elapsed since the last tenure
3050
    /// change exceeds this value, and the signer coordinator indicates an extension
3051
    /// is timely, and the cost usage threshold ([`MinerConfig::tenure_extend_cost_threshold`])
3052
    /// is met, the miner will include a tenure extension transaction in its next block.
3053
    /// ---
3054
    /// @default: [`DEFAULT_TENURE_TIMEOUT_SECS`]
3055
    /// @units: seconds
3056
    pub tenure_timeout: Duration,
3057
    /// Percentage of block budget that must be used before attempting a time-based tenure extend.
3058
    ///
3059
    /// This sets a minimum threshold for the accumulated execution cost within a
3060
    /// tenure before a time-based tenure extension ([`MinerConfig::tenure_timeout`])
3061
    /// can be initiated. The miner checks if the proportion of the total tenure
3062
    /// budget consumed so far exceeds this percentage. If the cost usage is below
3063
    /// this threshold, a time-based extension will not be attempted, even if the
3064
    /// [`MinerConfig::tenure_timeout`] duration has elapsed. This prevents miners
3065
    /// from extending tenures very early if they have produced only low-cost blocks.
3066
    /// ---
3067
    /// @default: [`DEFAULT_TENURE_EXTEND_COST_THRESHOLD`]
3068
    /// @units: percent
3069
    /// @notes:
3070
    ///   - Values: 0-100.
3071
    pub tenure_extend_cost_threshold: u64,
3072
    /// Percentage of block budget that must be used before attempting a time-based tenure extend.
3073
    ///
3074
    /// This sets a minimum threshold for the accumulated execution cost within a
3075
    /// tenure before a time-based tenure extension ([`MinerConfig::tenure_timeout`])
3076
    /// can be initiated. The miner checks if the proportion of the total tenure
3077
    /// budget consumed so far exceeds this percentage. If the cost usage is below
3078
    /// this threshold, a time-based extension will not be attempted, even if the
3079
    /// [`MinerConfig::tenure_timeout`] duration has elapsed. This prevents miners
3080
    /// from extending tenures very early if they have produced only low-cost blocks.
3081
    /// ---
3082
    /// @default: [`DEFAULT_READ_COUNT_EXTEND_COST_THRESHOLD`]
3083
    /// @units: percent
3084
    /// @notes:
3085
    ///   - Values: 0-100.
3086
    pub read_count_extend_cost_threshold: u64,
3087
    /// Defines adaptive timeouts for waiting for signer responses, based on the
3088
    /// accumulated weight of rejections.
3089
    ///
3090
    /// Configured as a map where keys represent rejection count thresholds in
3091
    /// percentage, and values are the timeout durations (in seconds) to apply when
3092
    /// the rejection count reaches or exceeds that key but is less than the next key.
3093
    ///
3094
    /// When a miner proposes a block, it waits for signer responses (approvals or
3095
    /// rejections). The SignerCoordinator tracks the total weight of received
3096
    /// rejections. It uses this map to determine the current timeout duration. It
3097
    /// selects the timeout value associated with the largest key in the map that is
3098
    /// less than or equal to the current accumulated rejection weight. If this
3099
    /// timeout duration expires before a decision is reached, the coordinator
3100
    /// signals a timeout. This prompts the miner to potentially retry proposing the
3101
    /// block. As more rejections come in, the applicable timeout step might change
3102
    /// (likely decrease), allowing the miner to abandon unviable proposals faster.
3103
    ///
3104
    /// A key for 0 (zero rejections) must be defined, representing the initial
3105
    /// timeout when no rejections have been received.
3106
    /// ---
3107
    /// @default: `{ 0: 180, 10: 90, 20: 45, 30: 0 }` (times in seconds)
3108
    /// @notes:
3109
    ///   - Keys are rejection weight percentages (0-100).
3110
    ///   - Values are timeout durations.
3111
    /// @toml_example: |
3112
    ///   # Keys are rejection counts (as strings), values are timeouts in seconds.
3113
    ///   [miner.block_rejection_timeout_steps]
3114
    ///   "0" = 180
3115
    ///   "10" = 90
3116
    ///   "20" = 45
3117
    ///   "30" = 0
3118
    pub block_rejection_timeout_steps: HashMap<u32, Duration>,
3119
    /// Defines the maximum execution time (in seconds) allowed for a single contract call
3120
    /// transaction during mining.
3121
    ///
3122
    /// When processing a transaction (contract call or smart contract deployment), if the
3123
    /// execution time exceeds this limit, the transaction processing fails with an
3124
    /// `ExecutionTimeout` error and the transaction is skipped. This prevents
3125
    /// long-running or infinite-loop transactions from blocking block production.
3126
    ///
3127
    /// Mining always enforces a limit; there is no way to disable it. To effectively
3128
    /// "turn it off," set this to a value larger than any tx is expected to take.
3129
    /// ---
3130
    /// @default: [`DEFAULT_MAX_EXECUTION_TIME_SECS`]
3131
    /// @units: seconds
3132
    pub max_execution_time_secs: u64,
3133
    /// TODO: remove this option when its no longer a testing feature and it becomes default behaviour
3134
    /// The miner will attempt to replay transactions that a threshold number of signers are expecting in the next block
3135
    pub replay_transactions: bool,
3136
    /// Defines the socket timeout (in seconds) for stackerdb communcation.
3137
    /// ---
3138
    /// @default: [`DEFAULT_STACKERDB_TIMEOUT_SECS`]
3139
    /// @units: seconds.
3140
    pub stackerdb_timeout: Duration,
3141
    /// Defines them maximum numnber of bytes to allow in a tenure.
3142
    /// The miner will stop mining if the limit is reached.
3143
    /// ---
3144
    /// @default: [`DEFAULT_MAX_TENURE_BYTES`]
3145
    /// @units: bytes.
3146
    pub max_tenure_bytes: u64,
3147
    /// Enable logging of skipped transactions (generally used for tests)
3148
    /// ---
3149
    /// @default: `false`
3150
    /// @notes:
3151
    ///   - Primarily intended for testing purposes.
3152
    pub log_skipped_transactions: bool,
3153
    /// Maximum number of bytes the miner thread may allocate during block
3154
    /// assembly before aborting. Tracked via `TrackingAllocator`
3155
    ///
3156
    /// A value of `0` disables the limit.
3157
    /// ---
3158
    /// @default: [`DEFAULT_MINER_ASSEMBLY_MEMORY_BYTES`]
3159
    /// @units: bytes
3160
    pub max_assembly_mem_bytes: u64,
3161
}
3162

3163
impl Default for MinerConfig {
3164
    fn default() -> MinerConfig {
4,453✔
3165
        MinerConfig {
4,453✔
3166
            first_attempt_time_ms: 10,
4,453✔
3167
            subsequent_attempt_time_ms: 120_000,
4,453✔
3168
            microblock_attempt_time_ms: 30_000,
4,453✔
3169
            nakamoto_attempt_time_ms: 5_000,
4,453✔
3170
            probability_pick_no_estimate_tx: 25,
4,453✔
3171
            block_reward_recipient: None,
4,453✔
3172
            segwit: false,
4,453✔
3173
            wait_for_block_download: true,
4,453✔
3174
            nonce_cache_size: 1024 * 1024,
4,453✔
3175
            candidate_retry_cache_size: 1024 * 1024,
4,453✔
3176
            unprocessed_block_deadline_secs: 30,
4,453✔
3177
            mining_key: None,
4,453✔
3178
            wait_on_interim_blocks: None,
4,453✔
3179
            min_tx_count: 0,
4,453✔
3180
            only_increase_tx_count: false,
4,453✔
3181
            unconfirmed_commits_helper: None,
4,453✔
3182
            target_win_probability: 0.0,
4,453✔
3183
            activated_vrf_key_path: None,
4,453✔
3184
            fast_rampup: false,
4,453✔
3185
            underperform_stop_threshold: None,
4,453✔
3186
            mempool_walk_strategy: MemPoolWalkStrategy::NextNonceWithHighestFeeRate,
4,453✔
3187
            txs_to_consider: MemPoolWalkTxTypes::all(),
4,453✔
3188
            filter_origins: HashSet::new(),
4,453✔
3189
            max_reorg_depth: 3,
4,453✔
3190
            pre_nakamoto_mock_signing: false, // Should only default true if mining key is set
4,453✔
3191
            min_time_between_blocks_ms: DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS,
4,453✔
3192
            empty_mempool_sleep_time: Duration::from_millis(DEFAULT_EMPTY_MEMPOOL_SLEEP_MS),
4,453✔
3193
            first_rejection_pause_ms: DEFAULT_FIRST_REJECTION_PAUSE_MS,
4,453✔
3194
            subsequent_rejection_pause_ms: DEFAULT_SUBSEQUENT_REJECTION_PAUSE_MS,
4,453✔
3195
            block_commit_delay: Duration::from_millis(DEFAULT_BLOCK_COMMIT_DELAY_MS),
4,453✔
3196
            tenure_cost_limit_per_block_percentage: Some(
4,453✔
3197
                DEFAULT_TENURE_COST_LIMIT_PER_BLOCK_PERCENTAGE,
4,453✔
3198
            ),
4,453✔
3199
            contract_cost_limit_percentage: Some(DEFAULT_CONTRACT_COST_LIMIT_PERCENTAGE),
4,453✔
3200
            tenure_extend_poll_timeout: Duration::from_secs(DEFAULT_TENURE_EXTEND_POLL_SECS),
4,453✔
3201
            tenure_extend_wait_timeout: Duration::from_millis(DEFAULT_TENURE_EXTEND_WAIT_MS),
4,453✔
3202
            tenure_timeout: Duration::from_secs(DEFAULT_TENURE_TIMEOUT_SECS),
4,453✔
3203
            tenure_extend_cost_threshold: DEFAULT_TENURE_EXTEND_COST_THRESHOLD,
4,453✔
3204
            read_count_extend_cost_threshold: DEFAULT_READ_COUNT_EXTEND_COST_THRESHOLD,
4,453✔
3205

4,453✔
3206
            block_rejection_timeout_steps: {
4,453✔
3207
                let mut rejections_timeouts_default_map = HashMap::<u32, Duration>::new();
4,453✔
3208
                rejections_timeouts_default_map.insert(0, Duration::from_secs(180));
4,453✔
3209
                rejections_timeouts_default_map.insert(10, Duration::from_secs(90));
4,453✔
3210
                rejections_timeouts_default_map.insert(20, Duration::from_secs(45));
4,453✔
3211
                rejections_timeouts_default_map.insert(30, Duration::from_secs(0));
4,453✔
3212
                rejections_timeouts_default_map
4,453✔
3213
            },
4,453✔
3214
            max_execution_time_secs: DEFAULT_MAX_EXECUTION_TIME_SECS,
4,453✔
3215
            replay_transactions: false,
4,453✔
3216
            stackerdb_timeout: Duration::from_secs(DEFAULT_STACKERDB_TIMEOUT_SECS),
4,453✔
3217
            max_tenure_bytes: DEFAULT_MAX_TENURE_BYTES,
4,453✔
3218
            log_skipped_transactions: false,
4,453✔
3219
            max_assembly_mem_bytes: DEFAULT_MINER_ASSEMBLY_MEMORY_BYTES,
4,453✔
3220
        }
4,453✔
3221
    }
4,453✔
3222
}
3223

3224
#[derive(Clone, Default, Deserialize, Debug)]
3225
#[serde(deny_unknown_fields)]
3226
pub struct ConnectionOptionsFile {
3227
    /// Maximum number of messages allowed in the per-connection incoming buffer.
3228
    /// The limits apply individually to each established connection (both P2P and HTTP).
3229
    /// ---
3230
    /// @default: `100`
3231
    pub inbox_maxlen: Option<usize>,
3232
    /// Maximum number of messages allowed in the per-connection outgoing buffer.
3233
    /// The limit applies individually to each established connection (both P2P and HTTP).
3234
    /// ---
3235
    /// @default: `100`
3236
    pub outbox_maxlen: Option<usize>,
3237
    /// Maximum duration (in seconds) a connection attempt is allowed to remain in
3238
    /// the connecting state.
3239
    ///
3240
    /// This applies to both incoming P2P and HTTP connections. If a remote peer
3241
    /// initiates a connection but does not complete the connection process
3242
    /// (e.g., handshake for P2P) within this time, the node will consider it
3243
    /// unresponsive and drop the connection attempt.
3244
    /// ---
3245
    /// @default: `10`
3246
    /// @units: seconds
3247
    pub connect_timeout: Option<u64>,
3248
    /// Maximum duration (in seconds) a P2P peer is allowed after connecting before
3249
    /// completing the handshake.
3250
    ///
3251
    /// If a P2P peer connects successfully but fails to send the necessary handshake
3252
    /// messages within this time, the node will consider it unresponsive and drop the
3253
    /// connection.
3254
    /// ---
3255
    /// @default: `5`
3256
    /// @units: seconds
3257
    pub handshake_timeout: Option<u64>,
3258
    /// General communication timeout (in seconds).
3259
    ///
3260
    /// - For HTTP connections: Governs two timeout aspects:
3261
    ///   - Server-side: Defines the maximum allowed time since the last request was
3262
    ///     received from a client. An idle connection is dropped if both this
3263
    ///     timeout and [`ConnectionOptionsFile::idle_timeout`] are exceeded.
3264
    ///   - Client-side: Sets the timeout duration (TTL) for outgoing HTTP requests
3265
    ///     initiated by the node itself.
3266
    /// - For P2P connections: Used as the specific timeout for NAT punch-through requests.
3267
    /// ---
3268
    /// @default: `15`
3269
    /// @units: seconds
3270
    pub timeout: Option<u64>,
3271
    /// Maximum idle time (in seconds) for HTTP connections.
3272
    ///
3273
    /// This applies only to HTTP connections. It defines the maximum allowed time
3274
    /// since the last response was sent by the node to the client. An HTTP
3275
    /// connection is dropped if both this `idle_timeout` and the general
3276
    /// [`ConnectionOptionsFile::timeout`] (time since last request received) are exceeded.
3277
    /// ---
3278
    /// @default: `15`
3279
    /// @units: seconds
3280
    pub idle_timeout: Option<u64>,
3281
    /// Interval (in seconds) at which this node expects to send or receive P2P
3282
    /// keep-alive messages.
3283
    ///
3284
    /// During the P2P handshake, this node advertises this configured `heartbeat`
3285
    /// value to its peers. Each peer uses the other's advertised heartbeat
3286
    /// interval (plus a timeout margin) to monitor responsiveness and detect
3287
    /// potential disconnections. This node also uses its own configured value to
3288
    /// proactively send Ping messages if the connection would otherwise be idle,
3289
    /// helping to keep it active.
3290
    /// ---
3291
    /// @default: `3_600` (1 hour)
3292
    /// @units: seconds
3293
    pub heartbeat: Option<u32>,
3294
    /// Validity duration (in number of bitcoin blocks) for the node's P2P session
3295
    /// private key.
3296
    ///
3297
    /// The node uses a temporary private key for signing P2P messages. This key has
3298
    /// an associated expiry bitcoin block height stored in the peer database. When
3299
    /// the current bitcoin height reaches or exceeds the key's expiry height, the
3300
    /// node automatically generates a new random private key.
3301
    /// The expiry block height for this new key is calculated by adding the
3302
    /// configured [`ConnectionOptionsFile::private_key_lifetime`] (in blocks) to the
3303
    /// previous key's expiry block height. The node then re-handshakes with peers
3304
    /// to transition to the new key. This provides periodic key rotation for P2P communication.
3305
    /// ---
3306
    /// @default: `9223372036854775807` (i64::MAX, effectively infinite, disabling automatic re-keying).
3307
    /// @units: bitcoin blocks
3308
    pub private_key_lifetime: Option<u64>,
3309
    /// Target number of peers for StackerDB replication.
3310
    ///
3311
    /// Sets the maximum number of potential replication target peers requested from
3312
    /// the StackerDB control contract (`get-replication-targets`) when configuring a replica.
3313
    ///
3314
    /// Note: Formerly (pre-Epoch 3.0), this also controlled the target peer count for
3315
    /// inventory synchronization.
3316
    /// ---
3317
    /// @default: `32`
3318
    pub num_neighbors: Option<u64>,
3319
    /// Maximum number of allowed concurrent inbound P2P connections.
3320
    ///
3321
    /// This acts as a hard limit. If the node already has this many active inbound
3322
    /// P2P connections, any new incoming P2P connection attempts will be rejected.
3323
    /// Outbound P2P connections initiated by this node are not counted against this limit.
3324
    /// ---
3325
    /// @default: `750`
3326
    pub num_clients: Option<u64>,
3327
    /// Maximum total number of allowed concurrent HTTP connections.
3328
    ///
3329
    /// This limits the total number of simultaneous connections the node's RPC/HTTP
3330
    /// server will accept. If this limit is reached, new incoming HTTP connection
3331
    /// attempts will be rejected.
3332
    /// ---
3333
    /// @default: `1000`
3334
    pub max_http_clients: Option<u64>,
3335
    /// Target number of outbound P2P connections the node aims to maintain.
3336
    ///
3337
    /// The connection pruning logic only activates if the current number of established
3338
    /// outbound P2P connections exceeds this value. Pruning aims to reduce the
3339
    /// connection count back down to this target, ensuring the node maintains a
3340
    /// baseline number of outbound peers for network connectivity.
3341
    /// ---
3342
    /// @default: `16`
3343
    pub soft_num_neighbors: Option<u64>,
3344
    /// Soft limit threshold for triggering inbound P2P connection pruning.
3345
    ///
3346
    /// If the total number of currently active inbound P2P connections exceeds this
3347
    /// value, the node will activate pruning logic to reduce the count, typically by
3348
    /// applying per-host limits (see [`ConnectionOptionsFile::soft_max_clients_per_host`]).
3349
    /// This helps manage the overall load from inbound peers.
3350
    /// ---
3351
    /// @default: `750`
3352
    pub soft_num_clients: Option<u64>,
3353
    /// Maximum number of neighbors per host we permit.
3354
    /// ---
3355
    /// @default: `1`
3356
    /// @deprecated: It does not have any effect on the node's behavior.
3357
    pub max_neighbors_per_host: Option<u64>,
3358
    /// Maximum number of inbound p2p connections per host we permit.
3359
    /// ---
3360
    /// @default: `4`
3361
    /// @deprecated: It does not have any effect on the node's behavior.
3362
    pub max_clients_per_host: Option<u64>,
3363
    /// Soft limit on the number of neighbors per host we permit.
3364
    /// ---
3365
    /// @default: `1`
3366
    /// @deprecated: It does not have any effect on the node's behavior.
3367
    pub soft_max_neighbors_per_host: Option<u64>,
3368
    /// Soft limit on the number of outbound P2P connections per network organization (ASN).
3369
    ///
3370
    /// During connection pruning (when total outbound connections >
3371
    /// [`ConnectionOptionsFile::soft_num_neighbors`]), the node checks if any single
3372
    /// network organization (identified by ASN) has more outbound connections than
3373
    /// this limit. If so, it preferentially prunes the least healthy/newest
3374
    /// connections from that overrepresented organization until its count is
3375
    /// reduced to this limit or the total outbound count reaches
3376
    /// [`ConnectionOptionsFile::soft_num_neighbors`]. This encourages connection diversity
3377
    /// across different network providers.
3378
    /// ---
3379
    /// @default: `32`
3380
    pub soft_max_neighbors_per_org: Option<u64>,
3381
    /// Soft limit on the number of inbound P2P connections allowed per host IP address.
3382
    ///
3383
    /// During inbound connection pruning (when total inbound connections >
3384
    /// [`ConnectionOptionsFile::soft_num_clients`]), the node checks if any single
3385
    /// IP address has more connections than this limit. If so, it preferentially
3386
    /// prunes the newest connections originating from that specific IP address
3387
    /// until its count is reduced to this limit. This prevents a single host from
3388
    /// dominating the node's inbound connection capacity.
3389
    /// ---
3390
    /// @default: `4`
3391
    pub soft_max_clients_per_host: Option<u64>,
3392
    /// Maximum total number of concurrent network sockets the node is allowed to manage.
3393
    ///
3394
    /// This limit applies globally to all types of sockets handled by the node's
3395
    /// networking layer, including listening sockets (P2P and RPC/HTTP),
3396
    /// established P2P connections (inbound/outbound), and established HTTP connections.
3397
    /// It serves as a hard limit to prevent the node from exhausting operating
3398
    /// system resources related to socket descriptors.
3399
    /// ---
3400
    /// @default: `800`
3401
    pub max_sockets: Option<u64>,
3402
    /// Minimum interval (in seconds) between the start of consecutive neighbor discovery walks.
3403
    ///
3404
    /// The node periodically performs "neighbor walks" to discover new peers and
3405
    /// maintain an up-to-date view of the P2P network topology. This setting
3406
    /// controls how frequently these walks can be initiated, preventing excessive
3407
    /// network traffic and processing.
3408
    /// ---
3409
    /// @default: `60`
3410
    /// @units: seconds
3411
    pub walk_interval: Option<u64>,
3412
    /// Probability (0.0 to 1.0) of forcing a neighbor walk to start from a seed/bootstrap peer.
3413
    ///
3414
    /// This probability applies only when the node is not in Initial Block Download (IBD)
3415
    /// and is already connected to at least one seed/bootstrap peer.
3416
    /// Normally, in this situation, the walk would start from a random inbound or
3417
    /// outbound peer. However, with this probability, the walk is forced to start
3418
    /// from a seed peer instead. This helps ensure the node periodically
3419
    /// re-establishes its network view from trusted entry points.
3420
    /// ---
3421
    /// @default: `0.1` (10%)
3422
    pub walk_seed_probability: Option<f64>,
3423
    /// Frequency (in milliseconds) for logging the current P2P neighbor list at the
3424
    /// DEBUG level.
3425
    ///
3426
    /// If set to a non-zero value, the node will periodically log details about its
3427
    /// currently established P2P connections (neighbors). Setting this to 0 disables
3428
    /// this periodic logging.
3429
    /// ---
3430
    /// @default: `60_000` (1 minute)
3431
    /// @units: milliseconds
3432
    pub log_neighbors_freq: Option<u64>,
3433
    /// Maximum time (in milliseconds) to wait for a DNS query to resolve.
3434
    ///
3435
    /// When the node needs to resolve a hostname (e.g., from a peer's advertised
3436
    /// [`NodeConfig::data_url`] or an Atlas attachment URL) into an IP address, it
3437
    /// initiates a DNS lookup. This setting defines the maximum duration the node will
3438
    /// wait for the DNS server to respond before considering the lookup timed out.
3439
    /// ---
3440
    /// @default: `15_000` (15 seconds)
3441
    /// @units: milliseconds
3442
    pub dns_timeout: Option<u64>,
3443
    /// Maximum number of concurrent Nakamoto block download requests allowed.
3444
    ///
3445
    /// This limits how many separate block download processes for Nakamoto tenures
3446
    /// (both confirmed and unconfirmed) can be active simultaneously. Helps manage
3447
    /// network bandwidth and processing load during chain synchronization.
3448
    /// ---
3449
    /// @default: `6`
3450
    pub max_inflight_blocks: Option<u64>,
3451
    /// Maximum number of concurrent Atlas data attachment download requests allowed.
3452
    ///
3453
    /// This limits how many separate download requests for Atlas data attachments
3454
    /// can be active simultaneously. Helps manage network resources when fetching
3455
    /// potentially large attachment data.
3456
    /// ---
3457
    /// @default: `6`
3458
    pub max_inflight_attachments: Option<u64>,
3459
    /// Maximum total size (in bytes) of data allowed to be written during a read-only call.
3460
    /// ---
3461
    /// @default: `0`
3462
    /// @notes:
3463
    ///   - This limit is effectively forced to 0 by the API handler, ensuring read-only behavior.
3464
    ///   - Configuring a non-zero value has no effect on read-only call execution.
3465
    /// @units: bytes
3466
    pub read_only_call_limit_write_length: Option<u64>,
3467
    /// Maximum total size (in bytes) of data allowed to be read from Clarity data
3468
    /// space (variables, maps) during a read-only call.
3469
    /// ---
3470
    /// @default: `100_000` (100 KB).
3471
    /// @units: bytes
3472
    pub read_only_call_limit_read_length: Option<u64>,
3473
    /// Maximum number of distinct write operations allowed during a read-only call.
3474
    /// ---
3475
    /// @default: `0`
3476
    /// @notes:
3477
    ///   - This limit is effectively forced to 0 by the API handler, ensuring read-only behavior.
3478
    ///   - Configuring a non-zero value has no effect on read-only call execution.
3479
    pub read_only_call_limit_write_count: Option<u64>,
3480
    /// Maximum number of distinct read operations from Clarity data space allowed
3481
    /// during a read-only call.
3482
    /// ---
3483
    /// @default: `30`
3484
    pub read_only_call_limit_read_count: Option<u64>,
3485
    /// Runtime cost limit for an individual read-only function call. This represents
3486
    /// computation effort within the Clarity VM.
3487
    /// (See SIP-006: https://github.com/stacksgov/sips/blob/main/sips/sip-006/sip-006-runtime-cost-assessment.md)
3488
    /// ---
3489
    /// @default: `1_000_000_000`
3490
    /// @units: Clarity VM cost units
3491
    pub read_only_call_limit_runtime: Option<u64>,
3492
    /// Maximum size (in bytes) of the HTTP request body for read-only contract calls.
3493
    ///
3494
    /// This limit is enforced on the `Content-Length` of incoming requests to the
3495
    /// `/v2/contracts/call-read-only/...` RPC endpoint. It prevents excessively large
3496
    /// request bodies, which might contain numerous or very large hex-encoded
3497
    /// function arguments, from overwhelming the node.
3498
    /// ---
3499
    /// @default: `83_886_080` (80 MiB)
3500
    /// @units: bytes
3501
    /// @notes:
3502
    ///   - Calculated as 20 * [`clarity::vm::types::BOUND_VALUE_SERIALIZATION_HEX`].
3503
    pub maximum_call_argument_size: Option<u32>,
3504
    /// Minimum interval (in seconds) between consecutive block download scans in epoch 2.x.
3505
    ///
3506
    /// In the pre-Nakamoto block download logic, if a full scan for blocks completed
3507
    /// without finding any new blocks to download, and if the known peer inventories
3508
    /// had not changed, the node would wait at least this duration before
3509
    /// initiating the next download scan. This throttled the downloader when the
3510
    /// node was likely already synchronized.
3511
    /// ---
3512
    /// @default: `10`
3513
    /// @units: seconds
3514
    /// @deprecated: This setting is ignored in Epoch 3.0+.
3515
    pub download_interval: Option<u64>,
3516
    /// Minimum interval (in seconds) between initiating inventory synchronization
3517
    /// attempts with the same peer.
3518
    ///
3519
    /// Acts as a per-peer cooldown to throttle sync requests. A new sync cycle with
3520
    /// a peer generally starts only after this interval has passed since the previous
3521
    /// attempt began *and* the previous cycle is considered complete.
3522
    /// ---
3523
    /// @default: `45`
3524
    /// @units: seconds
3525
    pub inv_sync_interval: Option<u64>,
3526
    /// Deprecated: it does not have any effect on the node's behavior.
3527
    /// ---
3528
    /// @default: `None`
3529
    /// @deprecated: It does not have any effect on the node's behavior.
3530
    pub full_inv_sync_interval: Option<u64>,
3531
    /// Lookback depth (in PoX reward cycles) for Nakamoto inventory synchronization requests.
3532
    ///
3533
    /// When initiating an inventory sync cycle with a peer, the node requests data
3534
    /// starting from `inv_reward_cycles` cycles before the current target reward
3535
    /// cycle. This determines how much historical inventory information is requested
3536
    /// in each sync attempt.
3537
    /// ---
3538
    /// @default: |
3539
    ///   - if [`BurnchainConfig::mode`] is `"mainnet"`: `3`
3540
    ///   - else: [`INV_REWARD_CYCLES_TESTNET`]
3541
    /// @units: PoX reward cycles
3542
    pub inv_reward_cycles: Option<u64>,
3543
    /// The Public IPv4 address and port (e.g. "203.0.113.42:20444") to advertise to other nodes.
3544
    ///
3545
    /// If this option is not set (`None`), the node will attempt to automatically
3546
    /// discover its public IP address.
3547
    /// ---
3548
    /// @default: `None` (triggers automatic discovery attempt)
3549
    pub public_ip_address: Option<String>,
3550
    /// If true, disables the neighbor discovery mechanism from starting walks from
3551
    /// inbound peers. Walks will only initiate from seed/bootstrap peers, outbound
3552
    /// connections, or pingbacks.
3553
    /// ---
3554
    /// @default: `false`
3555
    /// @notes:
3556
    ///   - Primarily intended for testing or specific network debugging scenarios.
3557
    pub disable_inbound_walks: Option<bool>,
3558
    /// If true, prevents the node from processing initial handshake messages from new
3559
    /// inbound P2P connections.
3560
    ///
3561
    /// This effectively stops the node from establishing new authenticated inbound
3562
    /// P2P sessions. Outbound connections initiated by this node are unaffected.
3563
    /// ---
3564
    /// @default: `false`
3565
    /// @notes:
3566
    ///   - Primarily intended for testing purposes.
3567
    pub disable_inbound_handshakes: Option<bool>,
3568
    /// If true, completely disables the block download state machine.
3569
    ///
3570
    /// The node will not attempt to download Stacks blocks (neither Nakamoto
3571
    /// tenures nor legacy blocks) from peers.
3572
    /// ---
3573
    /// @default: `false`
3574
    /// @notes:
3575
    ///   - Intended for testing or specialized node configurations.
3576
    pub disable_block_download: Option<bool>,
3577
    /// Fault injection setting for testing purposes. Interval (in seconds) for
3578
    /// forced disconnection of all peers.
3579
    ///
3580
    /// If set to a positive value, the node will periodically disconnect all of its
3581
    /// P2P peers at roughly this interval. This simulates network churn or
3582
    /// partitioning for testing node resilience.
3583
    /// ---
3584
    /// @default: `None` (feature disabled)
3585
    /// @notes:
3586
    ///   - If set to a positive value, the node will periodically disconnect all of
3587
    ///     its P2P peers at roughly this interval.
3588
    ///   - This simulates network churn or partitioning for testing node resilience.
3589
    ///   - The code enforcing this behavior is conditionally compiled using `cfg!(test)`
3590
    ///     and is only active during test runs.
3591
    ///   - This setting has no effect in standard production builds.
3592
    /// @units: seconds
3593
    pub force_disconnect_interval: Option<u64>,
3594
    /// Controls whether a node with public inbound connections should still push
3595
    /// blocks, even if not NAT'ed.
3596
    ///
3597
    /// In the Stacks 2.x anti-entropy logic, if a node detected it had inbound
3598
    /// connections from public IPs (suggesting it wasn't behind NAT) and this flag
3599
    /// was set to `false`, it would refrain from proactively pushing blocks and
3600
    /// microblocks to peers. The assumption was that publicly reachable nodes should
3601
    /// primarily serve downloads. If set to `true` (default), the node would push
3602
    /// data regardless of its perceived reachability.
3603
    /// ---
3604
    /// @default: `true`
3605
    /// @deprecated: This setting is ignored in Epoch 3.0+.
3606
    pub antientropy_public: Option<bool>,
3607
    /// Whether to allow connections and interactions with peers having private IP addresses.
3608
    ///
3609
    /// If `false` (default), the node will generally:
3610
    /// - Reject incoming connection attempts from peers with private IPs.
3611
    /// - Avoid initiating connections to peers known to have private IPs.
3612
    /// - Ignore peers with private IPs during neighbor discovery (walks).
3613
    /// - Skip querying peers with private IPs for mempool or StackerDB data.
3614
    /// - Filter out peers with private IPs from API responses listing potential peers.
3615
    ///
3616
    /// Setting this to `true` disables these restrictions, which can be useful for
3617
    /// local testing environments or fully private network deployments.
3618
    /// ---
3619
    /// @default: `false`
3620
    pub private_neighbors: Option<bool>,
3621
    /// HTTP auth password to use when communicating with stacks-signer binary.
3622
    ///
3623
    /// This token is used in the `Authorization` header for certain requests.
3624
    /// Primarily, it secures the communication channel between this node and a
3625
    /// connected `stacks-signer` instance.
3626
    ///
3627
    /// It is also used to authenticate requests to `/v2/blocks?broadcast=1`.
3628
    /// ---
3629
    /// @default: `None` (authentication disabled for relevant endpoints)
3630
    /// @notes:
3631
    ///   - This field **must** be configured if the node needs to receive
3632
    ///     block proposals from a configured `stacks-signer` [[events_observer]]
3633
    ///     via the `/v3/block_proposal` endpoint.
3634
    ///   - The value must match the token configured on the signer.
3635
    pub auth_token: Option<String>,
3636
    /// Minimum interval (in seconds) between attempts to run the Epoch 2.x anti-entropy
3637
    /// data push mechanism.
3638
    ///
3639
    /// The Stacks 2.x anti-entropy protocol involves the node proactively pushing its
3640
    /// known Stacks blocks and microblocks to peers. This value specifies the
3641
    /// cooldown period for this operation. This prevents the node from excessively
3642
    /// attempting to push data to its peers.
3643
    /// ---
3644
    /// @default: `3_600` (1 hour)
3645
    /// @deprecated: This setting is ignored in Epoch 3.0+.
3646
    /// @units: seconds
3647
    pub antientropy_retry: Option<u64>,
3648
    /// Controls whether the node accepts Nakamoto blocks pushed proactively by peers.
3649
    ///
3650
    /// - If `true`: Pushed blocks are ignored (logged at DEBUG and discarded). The
3651
    ///   node will still process blocks that it actively downloads.
3652
    /// - If `false`: Both pushed blocks and actively downloaded blocks are processed.
3653
    /// ---
3654
    /// @default: `false`
3655
    pub reject_blocks_pushed: Option<bool>,
3656
    /// Static list of preferred replica peers for specific StackerDB contracts,
3657
    /// provided as a JSON string.
3658
    ///
3659
    /// This allows manually specifying known peers to use for replicating particular
3660
    /// StackerDBs, potentially overriding or supplementing the peers discovered via
3661
    /// the StackerDB's control contract.
3662
    ///
3663
    /// Format: The configuration value must be a TOML string containing valid JSON.
3664
    /// The JSON structure must be an array of tuples, where each tuple pairs a
3665
    /// contract identifier with a list of preferred neighbor addresses:
3666
    /// `[[ContractIdentifier, [NeighborAddress, ...]], ...]`
3667
    ///
3668
    /// 1.  `ContractIdentifier`: A JSON object representing the [`QualifiedContractIdentifier`].
3669
    ///     It must have the specific structure:
3670
    ///     `{"issuer": [version_byte, [byte_array_20]], "name": "contract-name"}`
3671
    ///
3672
    /// 2.  `NeighborAddress`: A JSON object specifying the peer details:
3673
    ///     `{"ip": "...", "port": ..., "public_key_hash": "..."}`
3674
    /// ---
3675
    /// @default: `None` (no hints provided)
3676
    /// @notes:
3677
    ///   - Use this option with caution, primarily for advanced testing or bootstrapping.
3678
    /// @toml_example: |
3679
    ///   stackerdb_hint_replicas = '''
3680
    ///   [
3681
    ///     [
3682
    ///       {
3683
    ///         "issuer": [1, [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]],
3684
    ///         "name": "my-contract"
3685
    ///       },
3686
    ///       [
3687
    ///         {
3688
    ///           "ip": "192.0.2.1",
3689
    ///           "port": 20444,
3690
    ///           "public_key_hash": "0102030405060708090a0b0c0d0e0f1011121314"
3691
    ///         }
3692
    ///       ]
3693
    ///     ]
3694
    ///   ]
3695
    ///   '''
3696
    pub stackerdb_hint_replicas: Option<String>,
3697
    /// Maximum age (in seconds) allowed for a block proposal received via the
3698
    /// `/v3/block_proposal` RPC endpoint.
3699
    ///
3700
    /// If a block proposal is received whose timestamp is older than the current
3701
    /// time minus this configured value, the node will reject the proposal with an
3702
    /// HTTP 422 (Unprocessable Entity) error, considering it too stale. This
3703
    /// prevents the node from spending resources validating outdated proposals.
3704
    /// ---
3705
    /// @default: [`DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS`]
3706
    /// @units: seconds
3707
    pub block_proposal_max_age_secs: Option<u64>,
3708

3709
    /// Maximum time (in seconds) that a readonly call in free cost tracking mode
3710
    /// can run before being interrupted
3711
    /// ---
3712
    /// @default: 30
3713
    /// @units: seconds
3714
    pub read_only_max_execution_time_secs: Option<u64>,
3715

3716
    /// Maximum time (in seconds) to spend validating a block when processing
3717
    /// a block proposal received via the `/v3/block_proposal` RPC endpoint.
3718
    ///
3719
    /// If a block takes longer than this timeout to validate, it will be aborted.
3720
    /// This prevents the node from getting stuck on slow validations when processing
3721
    /// a block proposal.
3722
    /// ---
3723
    /// @default: [`DEFAULT_BLOCK_PROPOSAL_VALIDATION_TIMEOUT_SECS`]
3724
    /// @units: seconds
3725
    pub block_proposal_validation_timeout_secs: Option<u64>,
3726

3727
    /// Maximum time (in seconds) to spend executing a single transaction
3728
    /// during block proposal validation. This is a per-transaction cap that
3729
    /// is applied independently from the overall block validation timeout.
3730
    /// A transaction that exceeds this limit on its own is classified as
3731
    /// problematic; a transaction interrupted because the overall block
3732
    /// validation budget was exceeded is not.
3733
    /// ---
3734
    /// @default: [`DEFAULT_BLOCK_PROPOSAL_MAX_TX_EXECUTION_TIME_SECS`]
3735
    /// @units: seconds
3736
    pub block_proposal_max_tx_execution_time_secs: Option<u64>,
3737

3738
    /// Maximum bytes a single transaction may allocate on the heap during
3739
    /// block-proposal validation before it is rejected.
3740
    /// `0` disables the limit.
3741
    /// ---
3742
    /// @default: [`DEFAULT_PROPOSAL_MEMORY_BYTES`]
3743
    /// @units: bytes
3744
    pub block_proposal_max_tx_mem_bytes: Option<u64>,
3745
}
3746

3747
impl ConnectionOptionsFile {
3748
    fn into_config(self, is_mainnet: bool) -> Result<ConnectionOptions, String> {
1✔
3749
        let ip_addr = self
1✔
3750
            .public_ip_address
1✔
3751
            .map(|public_ip_address| {
1✔
3752
                public_ip_address
×
3753
                    .parse::<SocketAddr>()
×
3754
                    .map(|addr| (PeerAddress::from_socketaddr(&addr), addr.port()))
×
3755
                    .map_err(|e| format!("Invalid connection_option.public_ip_address: {e}"))
×
3756
            })
×
3757
            .transpose()?;
1✔
3758
        let mut read_only_call_limit = HELIUM_DEFAULT_CONNECTION_OPTIONS
1✔
3759
            .read_only_call_limit
1✔
3760
            .clone();
1✔
3761
        if let Some(x) = self.read_only_call_limit_write_length {
1✔
3762
            read_only_call_limit.write_length = x;
×
3763
        }
1✔
3764
        if let Some(x) = self.read_only_call_limit_write_count {
1✔
3765
            read_only_call_limit.write_count = x;
×
3766
        }
1✔
3767
        if let Some(x) = self.read_only_call_limit_read_length {
1✔
3768
            read_only_call_limit.read_length = x;
×
3769
        }
1✔
3770
        if let Some(x) = self.read_only_call_limit_read_count {
1✔
3771
            read_only_call_limit.read_count = x;
×
3772
        }
1✔
3773
        if let Some(x) = self.read_only_call_limit_runtime {
1✔
3774
            read_only_call_limit.runtime = x;
×
3775
        };
1✔
3776
        let default = ConnectionOptions::default();
1✔
3777
        Ok(ConnectionOptions {
3778
            read_only_call_limit,
1✔
3779
            inbox_maxlen: self
1✔
3780
                .inbox_maxlen
1✔
3781
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.inbox_maxlen),
1✔
3782
            outbox_maxlen: self
1✔
3783
                .outbox_maxlen
1✔
3784
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.outbox_maxlen),
1✔
3785
            timeout: self
1✔
3786
                .timeout
1✔
3787
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.timeout),
1✔
3788
            idle_timeout: self
1✔
3789
                .idle_timeout
1✔
3790
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.idle_timeout),
1✔
3791
            heartbeat: self
1✔
3792
                .heartbeat
1✔
3793
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.heartbeat),
1✔
3794
            private_key_lifetime: self
1✔
3795
                .private_key_lifetime
1✔
3796
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.private_key_lifetime),
1✔
3797
            num_neighbors: self
1✔
3798
                .num_neighbors
1✔
3799
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.num_neighbors),
1✔
3800
            num_clients: self
1✔
3801
                .num_clients
1✔
3802
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.num_clients),
1✔
3803
            soft_num_neighbors: self
1✔
3804
                .soft_num_neighbors
1✔
3805
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.soft_num_neighbors),
1✔
3806
            soft_num_clients: self
1✔
3807
                .soft_num_clients
1✔
3808
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.soft_num_clients),
1✔
3809
            max_neighbors_per_host: self
1✔
3810
                .max_neighbors_per_host
1✔
3811
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.max_neighbors_per_host),
1✔
3812
            max_clients_per_host: self
1✔
3813
                .max_clients_per_host
1✔
3814
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.max_clients_per_host),
1✔
3815
            soft_max_neighbors_per_host: self
1✔
3816
                .soft_max_neighbors_per_host
1✔
3817
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.soft_max_neighbors_per_host),
1✔
3818
            soft_max_neighbors_per_org: self
1✔
3819
                .soft_max_neighbors_per_org
1✔
3820
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.soft_max_neighbors_per_org),
1✔
3821
            soft_max_clients_per_host: self
1✔
3822
                .soft_max_clients_per_host
1✔
3823
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.soft_max_clients_per_host),
1✔
3824
            walk_interval: self
1✔
3825
                .walk_interval
1✔
3826
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.walk_interval),
1✔
3827
            walk_seed_probability: self
1✔
3828
                .walk_seed_probability
1✔
3829
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.walk_seed_probability),
1✔
3830
            log_neighbors_freq: self
1✔
3831
                .log_neighbors_freq
1✔
3832
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.log_neighbors_freq),
1✔
3833
            dns_timeout: self
1✔
3834
                .dns_timeout
1✔
3835
                .map(|dns_timeout| dns_timeout as u128)
1✔
3836
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.dns_timeout),
1✔
3837
            max_inflight_blocks: self
1✔
3838
                .max_inflight_blocks
1✔
3839
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.max_inflight_blocks),
1✔
3840
            max_inflight_attachments: self
1✔
3841
                .max_inflight_attachments
1✔
3842
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.max_inflight_attachments),
1✔
3843
            maximum_call_argument_size: self
1✔
3844
                .maximum_call_argument_size
1✔
3845
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.maximum_call_argument_size),
1✔
3846
            download_interval: self
1✔
3847
                .download_interval
1✔
3848
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.download_interval),
1✔
3849
            inv_sync_interval: self
1✔
3850
                .inv_sync_interval
1✔
3851
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.inv_sync_interval),
1✔
3852
            inv_reward_cycles: self.inv_reward_cycles.unwrap_or_else(|| {
1✔
3853
                if is_mainnet {
1✔
3854
                    HELIUM_DEFAULT_CONNECTION_OPTIONS.inv_reward_cycles
×
3855
                } else {
3856
                    // testnet reward cycles are a bit smaller (and blocks can go by
3857
                    // faster), so make our inventory
3858
                    // reward cycle depth a bit longer to compensate
3859
                    INV_REWARD_CYCLES_TESTNET
1✔
3860
                }
3861
            }),
1✔
3862
            public_ip_address: ip_addr,
1✔
3863
            disable_inbound_walks: self.disable_inbound_walks.unwrap_or(false),
1✔
3864
            disable_inbound_handshakes: self.disable_inbound_handshakes.unwrap_or(false),
1✔
3865
            disable_block_download: self.disable_block_download.unwrap_or(false),
1✔
3866
            force_disconnect_interval: self.force_disconnect_interval,
1✔
3867
            max_http_clients: self
1✔
3868
                .max_http_clients
1✔
3869
                .unwrap_or_else(|| HELIUM_DEFAULT_CONNECTION_OPTIONS.max_http_clients),
1✔
3870
            connect_timeout: self.connect_timeout.unwrap_or(10),
1✔
3871
            handshake_timeout: self.handshake_timeout.unwrap_or(5),
1✔
3872
            max_sockets: self.max_sockets.unwrap_or(800) as usize,
1✔
3873
            antientropy_public: self.antientropy_public.unwrap_or(true),
1✔
3874
            private_neighbors: self.private_neighbors.unwrap_or(false),
1✔
3875
            auth_token: self.auth_token,
1✔
3876
            antientropy_retry: self.antientropy_retry.unwrap_or(default.antientropy_retry),
1✔
3877
            reject_blocks_pushed: self
1✔
3878
                .reject_blocks_pushed
1✔
3879
                .unwrap_or(default.reject_blocks_pushed),
1✔
3880
            stackerdb_hint_replicas: self
1✔
3881
                .stackerdb_hint_replicas
1✔
3882
                .map(|stackerdb_hint_replicas_json| {
1✔
3883
                    let hint_replicas_res: Result<
×
3884
                        Vec<(QualifiedContractIdentifier, Vec<NeighborAddress>)>,
×
3885
                        String,
×
3886
                    > = serde_json::from_str(&stackerdb_hint_replicas_json)
×
3887
                        .map_err(|e| format!("Failed to decode `stackerdb_hint_replicas`: {e:?}"));
×
3888
                    hint_replicas_res
×
3889
                })
×
3890
                .transpose()?
1✔
3891
                .map(HashMap::from_iter)
1✔
3892
                .unwrap_or(default.stackerdb_hint_replicas),
1✔
3893
            block_proposal_max_age_secs: self
1✔
3894
                .block_proposal_max_age_secs
1✔
3895
                .unwrap_or(DEFAULT_BLOCK_PROPOSAL_MAX_AGE_SECS),
1✔
3896
            read_only_max_execution_time_secs: self
1✔
3897
                .read_only_max_execution_time_secs
1✔
3898
                .unwrap_or(default.read_only_max_execution_time_secs),
1✔
3899
            block_proposal_validation_timeout_secs: self
1✔
3900
                .block_proposal_validation_timeout_secs
1✔
3901
                .unwrap_or(DEFAULT_BLOCK_PROPOSAL_VALIDATION_TIMEOUT_SECS),
1✔
3902
            block_proposal_max_tx_execution_time_secs: self
1✔
3903
                .block_proposal_max_tx_execution_time_secs
1✔
3904
                .unwrap_or(DEFAULT_BLOCK_PROPOSAL_MAX_TX_EXECUTION_TIME_SECS),
1✔
3905
            block_proposal_max_tx_mem_bytes: self
1✔
3906
                .block_proposal_max_tx_mem_bytes
1✔
3907
                .unwrap_or(default.block_proposal_max_tx_mem_bytes),
1✔
3908
            ..default
3909
        })
3910
    }
1✔
3911
}
3912

3913
#[derive(Clone, Deserialize, Default, Debug)]
3914
#[serde(deny_unknown_fields)]
3915
pub struct NodeConfigFile {
3916
    pub name: Option<String>,
3917
    pub seed: Option<String>,
3918
    pub deny_nodes: Option<String>,
3919
    pub working_dir: Option<String>,
3920
    pub rpc_bind: Option<String>,
3921
    pub p2p_bind: Option<String>,
3922
    pub p2p_address: Option<String>,
3923
    pub data_url: Option<String>,
3924
    pub bootstrap_node: Option<String>,
3925
    pub local_peer_seed: Option<String>,
3926
    pub miner: Option<bool>,
3927
    pub stacker: Option<bool>,
3928
    pub mock_mining: Option<bool>,
3929
    pub mock_mining_output_dir: Option<String>,
3930
    pub mine_microblocks: Option<bool>,
3931
    pub microblock_frequency: Option<u64>,
3932
    pub max_microblocks: Option<u64>,
3933
    pub wait_time_for_microblocks: Option<u64>,
3934
    pub wait_time_for_blocks: Option<u64>,
3935
    pub next_initiative_delay: Option<u64>,
3936
    pub prometheus_bind: Option<String>,
3937
    pub marf_cache_strategy: Option<String>,
3938
    pub marf_defer_hashing: Option<bool>,
3939
    pub marf_compress: Option<bool>,
3940
    pub pox_sync_sample_secs: Option<u64>,
3941
    pub use_test_genesis_chainstate: Option<bool>,
3942
    /// At most, how often should the chain-liveness thread
3943
    ///  wake up the chains-coordinator. Defaults to 300s (5 min).
3944
    pub chain_liveness_poll_time_secs: Option<u64>,
3945
    pub event_dispatcher_blocking: Option<bool>,
3946
    /// Only relevant if `event_dispatcher_blocking` is false
3947
    pub event_dispatcher_queue_size: Option<usize>,
3948
    /// Stacker DBs we replicate
3949
    pub stacker_dbs: Option<Vec<String>>,
3950
    /// fault injection: fail to push blocks with this probability (0-100)
3951
    pub fault_injection_block_push_fail_probability: Option<u8>,
3952
    /// enable transactions indexing, note this will require additional storage (in the order of gigabytes)
3953
    pub txindex: Option<bool>,
3954
}
3955

3956
impl NodeConfigFile {
3957
    fn into_config_default(self, default_node_config: NodeConfig) -> Result<NodeConfig, String> {
337✔
3958
        let rpc_bind = self.rpc_bind.unwrap_or(default_node_config.rpc_bind);
337✔
3959
        let miner = self.miner.unwrap_or(default_node_config.miner);
337✔
3960
        let stacker = self.stacker.unwrap_or(default_node_config.stacker);
337✔
3961
        let node_config = NodeConfig {
335✔
3962
            name: self.name.unwrap_or(default_node_config.name),
337✔
3963
            seed: match self.seed {
337✔
3964
                Some(seed) => hex_bytes(&seed)
1✔
3965
                    .map_err(|_e| "node.seed should be a hex encoded string".to_string())?,
1✔
3966
                None => default_node_config.seed,
336✔
3967
            },
3968
            working_dir: std::env::var("STACKS_WORKING_DIR")
336✔
3969
                .unwrap_or(self.working_dir.unwrap_or(default_node_config.working_dir)),
336✔
3970
            rpc_bind: rpc_bind.clone(),
336✔
3971
            p2p_bind: self.p2p_bind.unwrap_or(default_node_config.p2p_bind),
336✔
3972
            p2p_address: self.p2p_address.unwrap_or(rpc_bind.clone()),
336✔
3973
            bootstrap_node: vec![],
336✔
3974
            deny_nodes: vec![],
336✔
3975
            data_url: self
336✔
3976
                .data_url
336✔
3977
                .unwrap_or_else(|| format!("http://{rpc_bind}")),
336✔
3978
            local_peer_seed: match self.local_peer_seed {
336✔
3979
                Some(seed) => hex_bytes(&seed).map_err(|_e| {
1✔
3980
                    "node.local_peer_seed should be a hex encoded string".to_string()
1✔
3981
                })?,
1✔
3982
                None => default_node_config.local_peer_seed,
335✔
3983
            },
3984
            miner,
335✔
3985
            stacker,
335✔
3986
            mock_mining: self.mock_mining.unwrap_or(default_node_config.mock_mining),
335✔
3987
            mock_mining_output_dir: self
335✔
3988
                .mock_mining_output_dir
335✔
3989
                .map(PathBuf::from)
335✔
3990
                .map(fs::canonicalize)
335✔
3991
                .transpose()
335✔
3992
                .unwrap_or_else(|e| {
335✔
3993
                    panic!("Failed to construct PathBuf from node.mock_mining_output_dir: {e}")
×
3994
                }),
3995
            mine_microblocks: self
335✔
3996
                .mine_microblocks
335✔
3997
                .unwrap_or(default_node_config.mine_microblocks),
335✔
3998
            microblock_frequency: self
335✔
3999
                .microblock_frequency
335✔
4000
                .unwrap_or(default_node_config.microblock_frequency),
335✔
4001
            max_microblocks: self
335✔
4002
                .max_microblocks
335✔
4003
                .unwrap_or(default_node_config.max_microblocks),
335✔
4004
            wait_time_for_microblocks: self
335✔
4005
                .wait_time_for_microblocks
335✔
4006
                .unwrap_or(default_node_config.wait_time_for_microblocks),
335✔
4007
            wait_time_for_blocks: self
335✔
4008
                .wait_time_for_blocks
335✔
4009
                .unwrap_or(default_node_config.wait_time_for_blocks),
335✔
4010
            next_initiative_delay: self
335✔
4011
                .next_initiative_delay
335✔
4012
                .unwrap_or(default_node_config.next_initiative_delay),
335✔
4013
            prometheus_bind: self.prometheus_bind,
335✔
4014
            marf_cache_strategy: self.marf_cache_strategy,
335✔
4015
            marf_defer_hashing: self
335✔
4016
                .marf_defer_hashing
335✔
4017
                .unwrap_or(default_node_config.marf_defer_hashing),
335✔
4018
            marf_compress: self
335✔
4019
                .marf_compress
335✔
4020
                .unwrap_or(default_node_config.marf_compress),
335✔
4021
            pox_sync_sample_secs: self
335✔
4022
                .pox_sync_sample_secs
335✔
4023
                .unwrap_or(default_node_config.pox_sync_sample_secs),
335✔
4024
            use_test_genesis_chainstate: self.use_test_genesis_chainstate,
335✔
4025
            // chainstate fault_injection activation for hide_blocks.
4026
            // you can't set this in the config file.
4027
            fault_injection_hide_blocks: false,
4028
            chain_liveness_poll_time_secs: self
335✔
4029
                .chain_liveness_poll_time_secs
335✔
4030
                .unwrap_or(default_node_config.chain_liveness_poll_time_secs),
335✔
4031
            event_dispatcher_blocking: self
335✔
4032
                .event_dispatcher_blocking
335✔
4033
                .unwrap_or(default_node_config.event_dispatcher_blocking),
335✔
4034
            event_dispatcher_queue_size: self
335✔
4035
                .event_dispatcher_queue_size
335✔
4036
                .unwrap_or(default_node_config.event_dispatcher_queue_size),
335✔
4037
            stacker_dbs: self
335✔
4038
                .stacker_dbs
335✔
4039
                .unwrap_or_default()
335✔
4040
                .iter()
335✔
4041
                .filter_map(|contract_id| QualifiedContractIdentifier::parse(contract_id).ok())
335✔
4042
                .collect(),
335✔
4043
            fault_injection_block_push_fail_probability: if self
335✔
4044
                .fault_injection_block_push_fail_probability
335✔
4045
                .is_some()
335✔
4046
            {
4047
                self.fault_injection_block_push_fail_probability
×
4048
            } else {
4049
                default_node_config.fault_injection_block_push_fail_probability
335✔
4050
            },
4051

4052
            txindex: self.txindex.unwrap_or(default_node_config.txindex),
335✔
4053
        };
4054
        Ok(node_config)
335✔
4055
    }
337✔
4056
}
4057

4058
#[derive(Clone, Deserialize, Default, Debug)]
4059
#[serde(deny_unknown_fields)]
4060
pub struct FeeEstimationConfigFile {
4061
    /// Specifies the name of the cost estimator to use.
4062
    /// This controls how the node estimates computational costs for transactions.
4063
    ///
4064
    /// Accepted values:
4065
    /// - `"NaivePessimistic"`: The only currently supported cost estimator. This estimator
4066
    ///   tracks the highest observed costs for each operation type and uses the average
4067
    ///   of the top 10 values as its estimate, providing a conservative approach to
4068
    ///   cost estimation.
4069
    /// ---
4070
    /// @default: `"NaivePessimistic"`
4071
    /// @notes:
4072
    ///   - If [`FeeEstimationConfigFile::disabled`] is `true`, the node will
4073
    ///     use the default unit cost estimator.
4074
    pub cost_estimator: Option<String>,
4075
    /// Specifies the name of the fee estimator to use.
4076
    /// This controls how the node calculates appropriate transaction fees based on costs.
4077
    ///
4078
    /// Accepted values:
4079
    /// - `"ScalarFeeRate"`: Simple multiplier-based fee estimation that uses percentiles
4080
    ///   (5th, 50th, and 95th) of observed fee rates from recent blocks.
4081
    /// - `"FuzzedWeightedMedianFeeRate"`: Fee estimation that adds controlled randomness
4082
    ///   to a weighted median rate calculator. This helps prevent fee optimization attacks
4083
    ///   by adding unpredictability to fee estimates while still maintaining accuracy.
4084
    /// ---
4085
    /// @default: `"ScalarFeeRate"`
4086
    /// @notes:
4087
    ///   - If [`FeeEstimationConfigFile::disabled`] is `true`, the node will
4088
    ///     use the default unit fee estimator.
4089
    pub fee_estimator: Option<String>,
4090
    /// Specifies the name of the cost metric to use.
4091
    /// This controls how the node measures and compares transaction costs.
4092
    ///
4093
    /// Accepted values:
4094
    /// - `"ProportionDotProduct"`: The only currently supported cost metric. This metric
4095
    ///   computes a weighted sum of cost dimensions (runtime, read/write counts, etc.)
4096
    ///   proportional to how much of the block limit they consume.
4097
    /// ---
4098
    /// @default: `"ProportionDotProduct"`
4099
    /// @notes:
4100
    ///   - If [`FeeEstimationConfigFile::disabled`] is `true`, the node will
4101
    ///     use the default unit cost metric.
4102
    pub cost_metric: Option<String>,
4103
    /// If `true`, all fee and cost estimation features are disabled.
4104
    /// The node will use unit estimators and metrics, which effectively provide no
4105
    /// actual estimation capabilities.
4106
    ///
4107
    /// When disabled, the node will:
4108
    /// 1. Not track historical transaction costs or fee rates.
4109
    /// 2. Return simple unit values for costs for any transaction, regardless of
4110
    ///    its actual complexity.
4111
    /// 3. Be unable to provide meaningful fee estimates for API requests (always
4112
    ///    returns an error).
4113
    /// 4. Consider only raw transaction fees (not fees per cost unit) when
4114
    ///    assembling blocks.
4115
    ///
4116
    /// This setting takes precedence over individual estimator/metric configurations.
4117
    /// ---
4118
    /// @default: `false`
4119
    /// @notes:
4120
    ///   - When `true`, the values for [`FeeEstimationConfigFile::cost_estimator`],
4121
    ///     [`FeeEstimationConfigFile::fee_estimator`], and
4122
    ///     [`FeeEstimationConfigFile::cost_metric`] are ignored.
4123
    pub disabled: Option<bool>,
4124
    /// If `true`, errors encountered during cost or fee estimation will be logged.
4125
    /// This can help diagnose issues with the fee estimation subsystem.
4126
    /// ---
4127
    /// @default: `false`
4128
    pub log_error: Option<bool>,
4129
    /// Specifies the fraction of random noise to add if using the
4130
    /// `FuzzedWeightedMedianFeeRate` fee estimator. This value should be in the
4131
    /// range [0, 1], representing a percentage of the base fee rate.
4132
    ///
4133
    /// For example, with a value of 0.1 (10%), fee rate estimates will have random
4134
    /// noise added within the range of ±10% of the original estimate. This
4135
    /// randomization makes it difficult for users to precisely optimize their fees
4136
    /// while still providing reasonable estimates.
4137
    /// ---
4138
    /// @default: `0.1` (10%)
4139
    /// @notes:
4140
    ///   - This setting is only relevant when [`FeeEstimationConfigFile::fee_estimator`] is set to
4141
    ///     `"FuzzedWeightedMedianFeeRate"`.
4142
    pub fee_rate_fuzzer_fraction: Option<f64>,
4143
    /// Specifies the window size for the `WeightedMedianFeeRateEstimator`.
4144
    /// This determines how many historical fee rate data points are considered
4145
    /// when calculating the median fee rate.
4146
    ///
4147
    // The window size controls how quickly the fee estimator responds to changing
4148
    // network conditions. A smaller window size (e.g., 5) makes the estimator more
4149
    // responsive to recent fee rate changes but potentially more volatile. A larger
4150
    // window size (e.g., 10) produces more stable estimates but may be slower to
4151
    // adapt to rapid network changes.
4152
    /// ---
4153
    /// @default: `5`
4154
    /// @notes:
4155
    ///   - This setting is primarily relevant when [`FeeEstimationConfigFile::fee_estimator`] is set
4156
    ///     to `"FuzzedWeightedMedianFeeRate"`.
4157
    pub fee_rate_window_size: Option<u64>,
4158
}
4159

4160
#[derive(Clone, Deserialize, Default, Debug)]
4161
#[serde(deny_unknown_fields)]
4162
pub struct MinerConfigFile {
4163
    pub first_attempt_time_ms: Option<u64>,
4164
    pub subsequent_attempt_time_ms: Option<u64>,
4165
    pub microblock_attempt_time_ms: Option<u64>,
4166
    pub nakamoto_attempt_time_ms: Option<u64>,
4167
    pub mempool_walk_strategy: Option<String>,
4168
    pub probability_pick_no_estimate_tx: Option<u8>,
4169
    pub block_reward_recipient: Option<String>,
4170
    pub segwit: Option<bool>,
4171
    pub nonce_cache_size: Option<usize>,
4172
    pub candidate_retry_cache_size: Option<usize>,
4173
    pub unprocessed_block_deadline_secs: Option<u64>,
4174
    pub mining_key: Option<String>,
4175
    pub wait_on_interim_blocks_ms: Option<u64>,
4176
    pub min_tx_count: Option<u64>,
4177
    pub only_increase_tx_count: Option<bool>,
4178
    pub unconfirmed_commits_helper: Option<String>,
4179
    pub target_win_probability: Option<f64>,
4180
    pub activated_vrf_key_path: Option<String>,
4181
    pub fast_rampup: Option<bool>,
4182
    pub underperform_stop_threshold: Option<u64>,
4183
    pub txs_to_consider: Option<String>,
4184
    pub filter_origins: Option<String>,
4185
    pub max_reorg_depth: Option<u64>,
4186
    pub pre_nakamoto_mock_signing: Option<bool>,
4187
    pub min_time_between_blocks_ms: Option<u64>,
4188
    pub empty_mempool_sleep_ms: Option<u64>,
4189
    pub first_rejection_pause_ms: Option<u64>,
4190
    pub subsequent_rejection_pause_ms: Option<u64>,
4191
    pub block_commit_delay_ms: Option<u64>,
4192
    pub tenure_cost_limit_per_block_percentage: Option<u8>,
4193
    pub contract_cost_limit_percentage: Option<u8>,
4194
    pub tenure_extend_poll_secs: Option<u64>,
4195
    pub tenure_extend_wait_timeout_ms: Option<u64>,
4196
    pub tenure_timeout_secs: Option<u64>,
4197
    pub tenure_extend_cost_threshold: Option<u64>,
4198
    pub block_rejection_timeout_steps: Option<HashMap<String, u64>>,
4199
    pub max_execution_time_secs: Option<u64>,
4200
    /// TODO: remove this config option once its no longer a testing feature
4201
    pub replay_transactions: Option<bool>,
4202
    pub stackerdb_timeout_secs: Option<u64>,
4203
    pub max_tenure_bytes: Option<u64>,
4204
    pub log_skipped_transactions: Option<bool>,
4205
    pub max_assembly_mem_bytes: Option<u64>,
4206
}
4207

4208
impl MinerConfigFile {
4209
    fn into_config_default(self, miner_default_config: MinerConfig) -> Result<MinerConfig, String> {
×
4210
        match &self.mining_key {
×
4211
            Some(_) => {}
×
4212
            None => {
4213
                panic!("mining key not set");
×
4214
            }
4215
        }
4216

4217
        let mining_key = self
×
4218
            .mining_key
×
4219
            .as_ref()
×
4220
            .map(|x| Secp256k1PrivateKey::from_hex(x))
×
4221
            .transpose()?;
×
4222
        let pre_nakamoto_mock_signing = mining_key.is_some();
×
4223

4224
        let tenure_cost_limit_per_block_percentage =
×
4225
            if let Some(percentage) = self.tenure_cost_limit_per_block_percentage {
×
4226
                if percentage == 100 {
×
4227
                    None
×
4228
                } else if percentage > 0 && percentage < 100 {
×
4229
                    Some(percentage)
×
4230
                } else {
4231
                    return Err(
×
4232
                        "miner.tenure_cost_limit_per_block_percentage must be between 1 and 100"
×
4233
                            .to_string(),
×
4234
                    );
×
4235
                }
4236
            } else {
4237
                miner_default_config.tenure_cost_limit_per_block_percentage
×
4238
            };
4239

4240
        let contract_cost_limit_percentage = if let Some(percentage) =
×
4241
            self.contract_cost_limit_percentage
×
4242
        {
4243
            if percentage <= 100 {
×
4244
                Some(percentage)
×
4245
            } else {
4246
                return Err(
×
4247
                    "miner.contract_cost_limit_percentage must be between 0 and 100".to_string(),
×
4248
                );
×
4249
            }
4250
        } else {
4251
            miner_default_config.contract_cost_limit_percentage
×
4252
        };
4253

4254
        let nonce_cache_size = self
×
4255
            .nonce_cache_size
×
4256
            .unwrap_or(miner_default_config.nonce_cache_size);
×
4257
        if nonce_cache_size == 0 {
×
4258
            return Err("miner.nonce_cache_size must be greater than 0".to_string());
×
4259
        }
×
4260

4261
        Ok(MinerConfig {
4262
            first_attempt_time_ms: self
×
4263
                .first_attempt_time_ms
×
4264
                .unwrap_or(miner_default_config.first_attempt_time_ms),
×
4265
            subsequent_attempt_time_ms: self
×
4266
                .subsequent_attempt_time_ms
×
4267
                .unwrap_or(miner_default_config.subsequent_attempt_time_ms),
×
4268
            microblock_attempt_time_ms: self
×
4269
                .microblock_attempt_time_ms
×
4270
                .unwrap_or(miner_default_config.microblock_attempt_time_ms),
×
4271
            nakamoto_attempt_time_ms: self
×
4272
                .nakamoto_attempt_time_ms
×
4273
                .unwrap_or(miner_default_config.nakamoto_attempt_time_ms),
×
4274
            probability_pick_no_estimate_tx: self
×
4275
                .probability_pick_no_estimate_tx
×
4276
                .unwrap_or(miner_default_config.probability_pick_no_estimate_tx),
×
4277
            block_reward_recipient: self
×
4278
                .block_reward_recipient
×
4279
                .map(|c| {
×
4280
                    PrincipalData::parse(&c).map_err(|e| {
×
4281
                        format!(
×
4282
                            "miner.block_reward_recipient is not a valid principal identifier: {e}"
4283
                        )
4284
                    })
×
4285
                })
×
4286
                .transpose()?,
×
4287
            segwit: self.segwit.unwrap_or(miner_default_config.segwit),
×
4288
            wait_for_block_download: miner_default_config.wait_for_block_download,
×
4289
            nonce_cache_size: self
×
4290
                .nonce_cache_size
×
4291
                .unwrap_or(miner_default_config.nonce_cache_size),
×
4292
            candidate_retry_cache_size: self
×
4293
                .candidate_retry_cache_size
×
4294
                .unwrap_or(miner_default_config.candidate_retry_cache_size),
×
4295
            unprocessed_block_deadline_secs: self
×
4296
                .unprocessed_block_deadline_secs
×
4297
                .unwrap_or(miner_default_config.unprocessed_block_deadline_secs),
×
4298
            mining_key: self
×
4299
                .mining_key
×
4300
                .as_ref()
×
4301
                .map(|x| Secp256k1PrivateKey::from_hex(x))
×
4302
                .transpose()?,
×
4303
            wait_on_interim_blocks: self
×
4304
                .wait_on_interim_blocks_ms
×
4305
                .map(Duration::from_millis),
×
4306
            min_tx_count: self
×
4307
                .min_tx_count
×
4308
                .unwrap_or(miner_default_config.min_tx_count),
×
4309
            only_increase_tx_count: self
×
4310
                .only_increase_tx_count
×
4311
                .unwrap_or(miner_default_config.only_increase_tx_count),
×
4312
            unconfirmed_commits_helper: self.unconfirmed_commits_helper.clone(),
×
4313
            target_win_probability: self
×
4314
                .target_win_probability
×
4315
                .unwrap_or(miner_default_config.target_win_probability),
×
4316
            activated_vrf_key_path: self.activated_vrf_key_path.clone(),
×
4317
            fast_rampup: self.fast_rampup.unwrap_or(miner_default_config.fast_rampup),
×
4318
            underperform_stop_threshold: self.underperform_stop_threshold,
×
4319
            mempool_walk_strategy: self.mempool_walk_strategy
×
4320
                .map(|s| str::parse(&s).unwrap_or_else(|e| panic!("Could not parse '{s}': {e}")))
×
4321
                .unwrap_or(MemPoolWalkStrategy::NextNonceWithHighestFeeRate),
×
4322
            txs_to_consider: {
4323
                if let Some(txs_to_consider) = &self.txs_to_consider {
×
4324
                    txs_to_consider
×
4325
                        .split(',')
×
4326
                        .map(
×
4327
                            |txs_to_consider_str| match str::parse(txs_to_consider_str) {
×
4328
                                Ok(txtype) => txtype,
×
4329
                                Err(e) => {
×
4330
                                    panic!("could not parse '{txs_to_consider_str}': {e}");
×
4331
                                }
4332
                            },
×
4333
                        )
4334
                        .collect()
×
4335
                } else {
4336
                    MemPoolWalkTxTypes::all()
×
4337
                }
4338
            },
4339
            filter_origins: {
4340
                if let Some(filter_origins) = &self.filter_origins {
×
4341
                    filter_origins
×
4342
                        .split(',')
×
4343
                        .map(|origin_str| match StacksAddress::from_string(origin_str) {
×
4344
                            Some(addr) => addr,
×
4345
                            None => {
4346
                                panic!("could not parse '{origin_str}' into a Stacks address");
×
4347
                            }
4348
                        })
×
4349
                        .collect()
×
4350
                } else {
4351
                    HashSet::new()
×
4352
                }
4353
            },
4354
            max_reorg_depth: self
×
4355
                .max_reorg_depth
×
4356
                .unwrap_or(miner_default_config.max_reorg_depth),
×
4357
            pre_nakamoto_mock_signing: self
×
4358
                .pre_nakamoto_mock_signing
×
4359
                .unwrap_or(pre_nakamoto_mock_signing), // Should only default true if mining key is set
×
4360
            min_time_between_blocks_ms: self.min_time_between_blocks_ms.map(|ms| if ms < DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS {
×
4361
                warn!("miner.min_time_between_blocks_ms is less than the minimum allowed value of {DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS} ms. Using the default value instead.");
×
4362
                DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS
×
4363
            } else {
4364
                ms
×
4365
            }).unwrap_or(miner_default_config.min_time_between_blocks_ms),
×
4366
            empty_mempool_sleep_time: self.empty_mempool_sleep_ms.map(Duration::from_millis).unwrap_or(miner_default_config.empty_mempool_sleep_time),
×
4367
            first_rejection_pause_ms: self.first_rejection_pause_ms.unwrap_or(miner_default_config.first_rejection_pause_ms),
×
4368
            subsequent_rejection_pause_ms: self.subsequent_rejection_pause_ms.unwrap_or(miner_default_config.subsequent_rejection_pause_ms),
×
4369
            block_commit_delay: self.block_commit_delay_ms.map(Duration::from_millis).unwrap_or(miner_default_config.block_commit_delay),
×
4370
            tenure_cost_limit_per_block_percentage,
×
4371
            contract_cost_limit_percentage,
×
4372
            tenure_extend_poll_timeout: self.tenure_extend_poll_secs.map(Duration::from_secs).unwrap_or(miner_default_config.tenure_extend_poll_timeout),
×
4373
            tenure_extend_wait_timeout: self.tenure_extend_wait_timeout_ms.map(Duration::from_millis).unwrap_or(miner_default_config.tenure_extend_wait_timeout),
×
4374
            tenure_timeout: self.tenure_timeout_secs.map(Duration::from_secs).unwrap_or(miner_default_config.tenure_timeout),
×
4375
            tenure_extend_cost_threshold: self.tenure_extend_cost_threshold.unwrap_or(miner_default_config.tenure_extend_cost_threshold),
×
4376

4377
            block_rejection_timeout_steps: {
4378
                if let Some(block_rejection_timeout_items) = self.block_rejection_timeout_steps {
×
4379
                    let mut rejection_timeout_durations = HashMap::<u32, Duration>::new();
×
4380
                    for (slice, seconds) in block_rejection_timeout_items.iter() {
×
4381
                        match slice.parse::<u32>() {
×
4382
                            Ok(slice_slot) => rejection_timeout_durations.insert(slice_slot, Duration::from_secs(*seconds)),
×
4383
                            Err(e) => panic!("block_rejection_timeout_steps keys must be unsigned integers: {}", e)
×
4384
                        };
4385
                    }
4386
                    if !rejection_timeout_durations.contains_key(&0) {
×
4387
                        panic!("block_rejection_timeout_steps requires a definition for the '0' key/step");
×
4388
                    }
×
4389
                    rejection_timeout_durations
×
4390
                } else{
4391
                    miner_default_config.block_rejection_timeout_steps
×
4392
                }
4393
            },
4394

4395
            max_execution_time_secs: self
×
4396
                .max_execution_time_secs
×
4397
                .unwrap_or(miner_default_config.max_execution_time_secs),
×
4398
            replay_transactions: self.replay_transactions.unwrap_or_default(),
×
4399
            stackerdb_timeout: self.stackerdb_timeout_secs.map(Duration::from_secs).unwrap_or(miner_default_config.stackerdb_timeout),
×
4400
            max_tenure_bytes: self.max_tenure_bytes.unwrap_or(miner_default_config.max_tenure_bytes),
×
4401
            log_skipped_transactions: self.log_skipped_transactions.unwrap_or(miner_default_config.log_skipped_transactions),
×
4402
            read_count_extend_cost_threshold: miner_default_config.read_count_extend_cost_threshold,
×
4403
            max_assembly_mem_bytes: self.max_assembly_mem_bytes.unwrap_or(miner_default_config.max_assembly_mem_bytes),
×
4404
        })
4405
    }
×
4406
}
4407

4408
#[derive(Clone, Deserialize, Default, Debug)]
4409
#[serde(deny_unknown_fields)]
4410
pub struct AtlasConfigFile {
4411
    pub attachments_max_size: Option<u32>,
4412
    pub max_uninstantiated_attachments: Option<u32>,
4413
    pub uninstantiated_attachments_expire_after: Option<u32>,
4414
    pub unresolved_attachment_instances_expire_after: Option<u32>,
4415
}
4416

4417
impl AtlasConfigFile {
4418
    // Can't inplement `Into` trait because this takes a parameter
4419
    #[allow(clippy::wrong_self_convention)]
4420
    fn into_config(&self, mainnet: bool) -> AtlasConfig {
×
4421
        let mut conf = AtlasConfig::new(mainnet);
×
4422
        if let Some(val) = self.attachments_max_size {
×
4423
            conf.attachments_max_size = val
×
4424
        }
×
4425
        if let Some(val) = self.max_uninstantiated_attachments {
×
4426
            conf.max_uninstantiated_attachments = val
×
4427
        }
×
4428
        if let Some(val) = self.uninstantiated_attachments_expire_after {
×
4429
            conf.uninstantiated_attachments_expire_after = val
×
4430
        }
×
4431
        if let Some(val) = self.unresolved_attachment_instances_expire_after {
×
4432
            conf.unresolved_attachment_instances_expire_after = val
×
4433
        }
×
4434
        conf
×
4435
    }
×
4436
}
4437

4438
#[derive(Clone, Deserialize, Default, Debug, Hash, PartialEq, Eq, PartialOrd)]
4439
#[serde(deny_unknown_fields)]
4440
pub struct EventObserverConfigFile {
4441
    /// URL endpoint (hostname and port) where event notifications will be sent via
4442
    /// HTTP POST requests.
4443
    ///
4444
    /// The node will automatically prepend `http://` to this endpoint and append the
4445
    /// specific event path (e.g., `/new_block`, `/new_mempool_tx`). Therefore, this
4446
    /// value should be specified as `hostname:port` (e.g., "localhost:3700").
4447
    ///
4448
    /// This should point to a service capable of receiving and processing Stacks event data.
4449
    /// ---
4450
    /// @default: No default.
4451
    /// @required: true
4452
    /// @notes:
4453
    ///   - **Do NOT include the `http://` scheme in this configuration value.**
4454
    /// @toml_example: |
4455
    ///   endpoint = "localhost:3700"
4456
    pub endpoint: String,
4457
    /// List of event types that this observer is configured to receive.
4458
    ///
4459
    /// Each string in the list specifies an event category or a specific event to
4460
    /// subscribe to. For an observer to receive any notifications, this list must
4461
    /// contain at least one valid key. Providing an invalid string that doesn't match
4462
    /// any of the valid formats below will cause the node to panic on startup when
4463
    /// parsing the configuration.
4464
    ///
4465
    /// All observers, regardless of their `events_keys` configuration, implicitly
4466
    /// receive payloads on the `/attachments/new` endpoint.
4467
    ///
4468
    /// Valid Event Keys:
4469
    /// - `"*"`: Subscribes to a broad set of common events.
4470
    ///   - Events delivered to:
4471
    ///     - `/new_block`: For blocks containing transactions that generate STX, FT,
4472
    ///       NFT, or smart contract events.
4473
    ///     - `/new_microblocks`: For all new microblock streams. Note: Only until epoch 2.5.
4474
    ///     - `/new_mempool_tx`: For new mempool transactions.
4475
    ///     - `/drop_mempool_tx`: For dropped mempool transactions.
4476
    ///     - `/new_burn_block`: For new burnchain blocks.
4477
    ///   - Note: This key does NOT by itself subscribe to `/stackerdb_chunks` or `/proposal_response`.
4478
    ///
4479
    /// - `"stx"`: Subscribes to STX token operation events (transfer, mint, burn, lock).
4480
    ///   - Events delivered to: `/new_block`, `/new_microblocks`.
4481
    ///   - Payload details: The "events" array in the delivered payloads will be
4482
    ///     filtered to include only STX-related events.
4483
    ///
4484
    /// - `"memtx"`: Subscribes to new and dropped mempool transaction events.
4485
    ///   - Events delivered to: `/new_mempool_tx`, `/drop_mempool_tx`.
4486
    ///
4487
    /// - `"burn_blocks"`: Subscribes to new burnchain block events.
4488
    ///   - Events delivered to: `/new_burn_block`.
4489
    ///
4490
    /// - `"microblocks"`: Subscribes to new microblock stream events.
4491
    ///   - Events delivered to: `/new_microblocks`.
4492
    ///   - Payload details:
4493
    ///     - The "transactions" field will contain all transactions from the microblocks.
4494
    ///     - The "events" field will contain STX, FT, NFT, or specific smart contract
4495
    ///       events *only if* this observer is also subscribed to those more specific
4496
    ///       event types (e.g., via `"stx"`, `"*"`, a specific contract event key,
4497
    ///       or a specific asset identifier key).
4498
    ///   - Note: Only until epoch 2.5.
4499
    ///
4500
    /// - `"stackerdb"`: Subscribes to StackerDB chunk update events.
4501
    ///   - Events delivered to: `/stackerdb_chunks`.
4502
    ///
4503
    /// - `"block_proposal"`: Subscribes to block proposal response events (for Nakamoto consensus).
4504
    ///   - Events delivered to: `/proposal_response`.
4505
    ///
4506
    /// - Smart Contract Event: Subscribes to a specific smart contract event.
4507
    ///   - Format: `"{deployer_address}.{contract_name}::{event_name}"`
4508
    ///     (e.g., `ST0000000000000000000000000000000000000000.my-contract::my-custom-event`)
4509
    ///   - Events delivered to: `/new_block`, `/new_microblocks`.
4510
    ///   - Payload details: The "events" array in the delivered payloads will be
4511
    ///     filtered for this specific event.
4512
    ///
4513
    /// - Asset Identifier for FT/NFT Events: Subscribes to events (mint, burn,
4514
    ///   transfer) for a specific Fungible Token (FT) or Non-Fungible Token (NFT).
4515
    ///   - Format: `"{deployer_address}.{contract_name}.{asset_name}"`
4516
    ///     (e.g., for an FT: `ST0000000000000000000000000000000000000000.contract.token`)
4517
    ///   - Events delivered to: `/new_block`, `/new_microblocks`.
4518
    ///   - Payload details: The "events" array in the delivered payloads will be
4519
    ///     filtered for events related to the specified asset.
4520
    /// ---
4521
    /// @default: No default.
4522
    /// @required: true
4523
    /// @notes:
4524
    ///   - For a more detailed documentation check the event-dispatcher docs in the `/docs` folder.
4525
    /// @toml_example: |
4526
    ///   events_keys = [
4527
    ///     "burn_blocks",
4528
    ///     "memtx",
4529
    ///     "ST0000000000000000000000000000000000000000.my-contract::my-custom-event",
4530
    ///     "ST0000000000000000000000000000000000000000.token-contract.my-ft"
4531
    ///   ]
4532
    pub events_keys: Vec<String>,
4533
    /// Maximum duration (in milliseconds) to wait for the observer endpoint to respond.
4534
    ///
4535
    /// When the node sends an event notification to this observer, it will wait at
4536
    /// most this long for a successful HTTP response (status code 200) before
4537
    /// considering the request timed out. If a timeout occurs and retries are enabled
4538
    /// (see [`EventObserverConfigFile::disable_retries`]), the request will be attempted
4539
    /// again according to the retry strategy.
4540
    /// ---
4541
    /// @default: `1_000`
4542
    /// @units: milliseconds
4543
    pub timeout_ms: Option<u64>,
4544
    /// Controls whether the node should retry sending event notifications if delivery
4545
    /// fails or times out.
4546
    ///
4547
    /// If `false` (default): The node will attempt to deliver event notifications
4548
    ///   persistently. If an attempt fails (due to network error, timeout, or a
4549
    ///   non-200 HTTP response), the event payload is saved and retried indefinitely.
4550
    ///   This ensures that all events will eventually be delivered. However, this can
4551
    ///   cause the node's block processing to stall if an observer is down, or
4552
    ///   indefinitely fails to process the event.
4553
    ///
4554
    /// - If `true`: The node will make only a single attempt to deliver each event
4555
    ///   notification. If this single attempt fails for any reason, the event is
4556
    ///   discarded, and no further retries will be made for that specific event.
4557
    /// ---
4558
    /// @default: `false` (retries are enabled)
4559
    /// @notes:
4560
    ///   - **Warning:** Setting this to `true` can lead to missed events if the
4561
    ///     observer endpoint is temporarily unavailable or experiences issues.
4562
    pub disable_retries: Option<bool>,
4563
}
4564

4565
#[derive(Clone, Default, Debug, Hash, PartialEq, Eq, PartialOrd)]
4566
pub struct EventObserverConfig {
4567
    pub endpoint: String,
4568
    pub events_keys: Vec<EventKeyType>,
4569
    pub timeout_ms: u64,
4570
    pub disable_retries: bool,
4571
}
4572

4573
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd)]
4574
pub enum EventKeyType {
4575
    SmartContractEvent((QualifiedContractIdentifier, String)),
4576
    AssetEvent(AssetIdentifier),
4577
    STXEvent,
4578
    MemPoolTransactions,
4579
    Microblocks,
4580
    AnyEvent,
4581
    BurnchainBlocks,
4582
    MinedBlocks,
4583
    MinedMicroblocks,
4584
    StackerDBChunks,
4585
    BlockProposal,
4586
}
4587

4588
impl EventKeyType {
4589
    fn from_string(raw_key: &str) -> Option<EventKeyType> {
×
4590
        if raw_key == "*" {
×
4591
            return Some(EventKeyType::AnyEvent);
×
4592
        }
×
4593

4594
        if raw_key == "stx" {
×
4595
            return Some(EventKeyType::STXEvent);
×
4596
        }
×
4597

4598
        if raw_key == "memtx" {
×
4599
            return Some(EventKeyType::MemPoolTransactions);
×
4600
        }
×
4601

4602
        if raw_key == "burn_blocks" {
×
4603
            return Some(EventKeyType::BurnchainBlocks);
×
4604
        }
×
4605

4606
        if raw_key == "microblocks" {
×
4607
            return Some(EventKeyType::Microblocks);
×
4608
        }
×
4609

4610
        if raw_key == "stackerdb" {
×
4611
            return Some(EventKeyType::StackerDBChunks);
×
4612
        }
×
4613

4614
        if raw_key == "block_proposal" {
×
4615
            return Some(EventKeyType::BlockProposal);
×
4616
        }
×
4617

4618
        let comps: Vec<_> = raw_key.split("::").collect();
×
4619
        if let Ok(comps) = TryInto::<&[_; 1]>::try_into(comps.as_slice()) {
×
4620
            let split_vec: Vec<_> = comps[0].split('.').collect();
×
4621
            let Ok(split) = TryInto::<&[_; 3]>::try_into(split_vec.as_slice()) else {
×
4622
                return None;
×
4623
            };
4624
            let components = (
×
4625
                PrincipalData::parse_standard_principal(split[0]),
×
4626
                split[1].to_string().try_into(),
×
4627
                split[2].to_string().try_into(),
×
4628
            );
×
4629
            match components {
×
4630
                (Ok(address), Ok(name), Ok(asset_name)) => {
×
4631
                    let contract_identifier = QualifiedContractIdentifier::new(address, name);
×
4632
                    let asset_identifier = AssetIdentifier {
×
4633
                        contract_identifier,
×
4634
                        asset_name,
×
4635
                    };
×
4636
                    Some(EventKeyType::AssetEvent(asset_identifier))
×
4637
                }
4638
                (_, _, _) => None,
×
4639
            }
4640
        } else if let Ok(comps) = TryInto::<&[_; 2]>::try_into(comps.as_slice()) {
×
4641
            if let Ok(contract_identifier) = QualifiedContractIdentifier::parse(comps[0]) {
×
4642
                Some(EventKeyType::SmartContractEvent((
×
4643
                    contract_identifier,
×
4644
                    comps[1].to_string(),
×
4645
                )))
×
4646
            } else {
4647
                None
×
4648
            }
4649
        } else {
4650
            None
×
4651
        }
4652
    }
×
4653
}
4654

4655
#[derive(Debug, Clone, Deserialize)]
4656
pub struct InitialBalance {
4657
    pub address: PrincipalData,
4658
    pub amount: u64,
4659
}
4660

4661
#[derive(Clone, Deserialize, Default, Debug)]
4662
#[serde(deny_unknown_fields)]
4663
pub struct InitialBalanceFile {
4664
    /// The Stacks address to receive the initial STX balance.
4665
    /// Must be a valid "non-mainnet" Stacks address (e.g., "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2").
4666
    /// ---
4667
    /// @default: No default.
4668
    /// @required: true
4669
    pub address: String,
4670
    /// The amount of microSTX to allocate to the address at node startup.
4671
    /// 1 STX = 1,000,000 microSTX.
4672
    /// ---
4673
    /// @default: No default.
4674
    /// @required: true
4675
    /// @units: microSTX
4676
    pub amount: u64,
4677
}
4678

4679
#[cfg(test)]
4680
mod tests {
4681
    use std::path::Path;
4682

4683
    use super::*;
4684

4685
    mod utils {
4686
        use super::*;
4687

4688
        /// Creates a [`Config`] from a valid configuration string. Panics otherwise.
4689
        pub fn config_from_valid_string(valid_config: &str) -> Config {
2✔
4690
            Config::from_config_file(ConfigFile::from_str(valid_config).unwrap(), false).unwrap()
2✔
4691
        }
2✔
4692
    }
4693

4694
    #[test]
4695
    fn test_config_file() {
1✔
4696
        assert_eq!(
1✔
4697
            format!("Invalid path: No such file or directory (os error 2)"),
1✔
4698
            ConfigFile::from_path("some_path").unwrap_err()
1✔
4699
        );
4700
        assert_eq!(
1✔
4701
            format!("Invalid toml: unexpected character found: `/` at line 1 column 1"),
1✔
4702
            ConfigFile::from_str("//[node]").unwrap_err()
1✔
4703
        );
4704
        assert!(ConfigFile::from_str("").is_ok());
1✔
4705
    }
1✔
4706

4707
    #[test]
4708
    fn test_config() {
1✔
4709
        assert_eq!(
1✔
4710
            format!("node.seed should be a hex encoded string"),
1✔
4711
            Config::from_config_file(
1✔
4712
                ConfigFile::from_str(
1✔
4713
                    r#"
1✔
4714
                    [node]
1✔
4715
                    seed = "invalid-hex-value"
1✔
4716
                    "#,
1✔
4717
                )
4718
                .unwrap(),
1✔
4719
                false
4720
            )
4721
            .unwrap_err()
1✔
4722
        );
4723

4724
        assert_eq!(
1✔
4725
            format!("node.local_peer_seed should be a hex encoded string"),
1✔
4726
            Config::from_config_file(
1✔
4727
                ConfigFile::from_str(
1✔
4728
                    r#"
1✔
4729
                    [node]
1✔
4730
                    local_peer_seed = "invalid-hex-value"
1✔
4731
                    "#,
1✔
4732
                )
4733
                .unwrap(),
1✔
4734
                false
4735
            )
4736
            .unwrap_err()
1✔
4737
        );
4738

4739
        let expected_err_prefix =
1✔
4740
            "Invalid burnchain.peer_host: failed to lookup address information:";
1✔
4741
        let actual_err_msg = Config::from_config_file(
1✔
4742
            ConfigFile::from_str(
1✔
4743
                r#"
1✔
4744
                [burnchain]
1✔
4745
                peer_host = "bitcoin2.blockstack.com"
1✔
4746
                "#,
1✔
4747
            )
4748
            .unwrap(),
1✔
4749
            false,
4750
        )
4751
        .unwrap_err();
1✔
4752
        assert_eq!(
1✔
4753
            expected_err_prefix,
4754
            &actual_err_msg[..expected_err_prefix.len()]
1✔
4755
        );
4756

4757
        assert!(Config::from_config_file(ConfigFile::from_str("").unwrap(), false).is_ok());
1✔
4758
    }
1✔
4759

4760
    #[test]
4761
    fn test_deny_unknown_fields() {
1✔
4762
        {
4763
            let err = ConfigFile::from_str(
1✔
4764
                r#"
1✔
4765
            [node]
1✔
4766
            name = "test"
1✔
4767
            unknown_field = "test"
1✔
4768
            "#,
1✔
4769
            )
4770
            .unwrap_err();
1✔
4771
            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
1✔
4772
        }
4773

4774
        {
4775
            let err = ConfigFile::from_str(
1✔
4776
                r#"
1✔
4777
            [burnchain]
1✔
4778
            chain_id = 0x00000500
1✔
4779
            unknown_field = "test"
1✔
4780
            chain = "bitcoin"
1✔
4781
            "#,
1✔
4782
            )
4783
            .unwrap_err();
1✔
4784
            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
1✔
4785
        }
4786

4787
        {
4788
            let err = ConfigFile::from_str(
1✔
4789
                r#"
1✔
4790
            [node]
1✔
4791
            rpc_bind = "0.0.0.0:20443"
1✔
4792
            unknown_field = "test"
1✔
4793
            p2p_bind = "0.0.0.0:20444"
1✔
4794
            "#,
1✔
4795
            )
4796
            .unwrap_err();
1✔
4797
            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
1✔
4798
        }
4799

4800
        {
4801
            let err = ConfigFile::from_str(
1✔
4802
                r#"
1✔
4803
            [[ustx_balance]]
1✔
4804
            address = "ST3AM1A56AK2C1XAFJ4115ZSV26EB49BVQ10MGCS0"
1✔
4805
            amount = 10000000000000000
1✔
4806
            unknown_field = "test"
1✔
4807
            "#,
1✔
4808
            )
4809
            .unwrap_err();
1✔
4810
            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
1✔
4811
        }
4812

4813
        {
4814
            let err = ConfigFile::from_str(
1✔
4815
                r#"
1✔
4816
            [[events_observer]]
1✔
4817
            endpoint = "localhost:30000"
1✔
4818
            unknown_field = "test"
1✔
4819
            events_keys = ["stackerdb", "block_proposal", "burn_blocks"]
1✔
4820
            "#,
1✔
4821
            )
4822
            .unwrap_err();
1✔
4823
            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
1✔
4824
        }
4825

4826
        {
4827
            let err = ConfigFile::from_str(
1✔
4828
                r#"
1✔
4829
            [connection_options]
1✔
4830
            inbox_maxlen = 100
1✔
4831
            outbox_maxlen = 200
1✔
4832
            unknown_field = "test"
1✔
4833
            "#,
1✔
4834
            )
4835
            .unwrap_err();
1✔
4836
            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
1✔
4837
        }
4838

4839
        {
4840
            let err = ConfigFile::from_str(
1✔
4841
                r#"
1✔
4842
            [fee_estimation]
1✔
4843
            cost_estimator = "foo"
1✔
4844
            unknown_field = "test"
1✔
4845
            "#,
1✔
4846
            )
4847
            .unwrap_err();
1✔
4848
            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
1✔
4849
        }
4850

4851
        {
4852
            let err = ConfigFile::from_str(
1✔
4853
                r#"
1✔
4854
            [miner]
1✔
4855
            first_attempt_time_ms = 180_000
1✔
4856
            unknown_field = "test"
1✔
4857
            subsequent_attempt_time_ms = 360_000
1✔
4858
            "#,
1✔
4859
            )
4860
            .unwrap_err();
1✔
4861
            println!("{err}");
1✔
4862
            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
1✔
4863
        }
4864

4865
        {
4866
            let err = ConfigFile::from_str(
1✔
4867
                r#"
1✔
4868
                [atlas]
1✔
4869
                attachments_max_size = 100
1✔
4870
                unknown_field = "test"
1✔
4871
                "#,
1✔
4872
            )
4873
            .unwrap_err();
1✔
4874
            assert!(err.starts_with("Invalid toml: unknown field `unknown_field`"));
1✔
4875
        }
4876
    }
1✔
4877

4878
    #[test]
4879
    fn test_example_confs() {
1✔
4880
        // For each config file in the ../conf/ directory, we should be able to parse it
4881
        let conf_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("conf");
1✔
4882
        println!("Reading config files from: {conf_dir:?}");
1✔
4883
        let conf_files = fs::read_dir(conf_dir).unwrap();
1✔
4884

4885
        for entry in conf_files {
9✔
4886
            let entry = entry.unwrap();
9✔
4887
            let path = entry.path();
9✔
4888
            if path.is_file() {
9✔
4889
                let file_name = path.file_name().unwrap().to_str().unwrap();
8✔
4890
                if file_name.ends_with(".toml") {
8✔
4891
                    debug!("Parsing config file: {file_name}");
8✔
4892
                    let _config = ConfigFile::from_path(path.to_str().unwrap()).unwrap();
8✔
4893
                    debug!("Parsed config file: {file_name}");
8✔
4894
                }
×
4895
            }
1✔
4896
        }
4897
    }
1✔
4898

4899
    #[test]
4900
    fn should_load_legacy_mstx_balances_toml() {
1✔
4901
        let config = ConfigFile::from_str(
1✔
4902
            r#"
1✔
4903
            [[ustx_balance]]
1✔
4904
            address = "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2"
1✔
4905
            amount = 10000000000000000
1✔
4906

1✔
4907
            [[ustx_balance]]
1✔
4908
            address = "ST319CF5WV77KYR1H3GT0GZ7B8Q4AQPY42ETP1VPF"
1✔
4909
            amount = 10000000000000000
1✔
4910

1✔
4911
            [[mstx_balance]] # legacy property name
1✔
4912
            address = "ST221Z6TDTC5E0BYR2V624Q2ST6R0Q71T78WTAX6H"
1✔
4913
            amount = 10000000000000000
1✔
4914

1✔
4915
            [[mstx_balance]] # legacy property name
1✔
4916
            address = "ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B"
1✔
4917
            amount = 10000000000000000
1✔
4918
            "#,
1✔
4919
        );
4920
        let config = config.unwrap();
1✔
4921
        assert!(config.ustx_balance.is_some());
1✔
4922
        let balances = config
1✔
4923
            .ustx_balance
1✔
4924
            .expect("Failed to parse stx balances from toml");
1✔
4925
        assert_eq!(balances.len(), 4);
1✔
4926
        assert_eq!(
1✔
4927
            balances[0].address,
1✔
4928
            "ST2QKZ4FKHAH1NQKYKYAYZPY440FEPK7GZ1R5HBP2"
4929
        );
4930
        assert_eq!(
1✔
4931
            balances[1].address,
1✔
4932
            "ST319CF5WV77KYR1H3GT0GZ7B8Q4AQPY42ETP1VPF"
4933
        );
4934
        assert_eq!(
1✔
4935
            balances[2].address,
1✔
4936
            "ST221Z6TDTC5E0BYR2V624Q2ST6R0Q71T78WTAX6H"
4937
        );
4938
        assert_eq!(
1✔
4939
            balances[3].address,
1✔
4940
            "ST2TFVBMRPS5SSNP98DQKQ5JNB2B6NZM91C4K3P7B"
4941
        );
4942
    }
1✔
4943

4944
    #[test]
4945
    fn should_load_auth_token() {
1✔
4946
        let config = Config::from_config_file(
1✔
4947
            ConfigFile::from_str(
1✔
4948
                r#"
1✔
4949
                [connection_options]
1✔
4950
                auth_token = "password"
1✔
4951
                "#,
1✔
4952
            )
4953
            .unwrap(),
1✔
4954
            false,
4955
        )
4956
        .expect("Expected to be able to parse block proposal token from file");
1✔
4957

4958
        assert_eq!(
1✔
4959
            config.connection_options.auth_token,
4960
            Some("password".to_string())
1✔
4961
        );
4962
    }
1✔
4963

4964
    #[test]
4965
    fn test_into_config_default_chain_id() {
1✔
4966
        // Helper function to create BurnchainConfigFile with mode and optional chain_id
4967
        fn make_burnchain_config_file(mainnet: bool, chain_id: Option<u32>) -> BurnchainConfigFile {
5✔
4968
            let mut config = BurnchainConfigFile::default();
5✔
4969
            if mainnet {
5✔
4970
                config.mode = Some("mainnet".to_string());
3✔
4971
            }
3✔
4972
            config.chain_id = chain_id;
5✔
4973
            config
5✔
4974
        }
5✔
4975
        let default_burnchain_config = BurnchainConfig::default();
1✔
4976

4977
        // **Case 1a:** Should panic when `is_mainnet` is true and `chain_id` != `CHAIN_ID_MAINNET`
4978
        {
4979
            let config_file = make_burnchain_config_file(true, Some(CHAIN_ID_TESTNET));
1✔
4980

4981
            let result = config_file.into_config_default(default_burnchain_config.clone());
1✔
4982

4983
            assert!(
1✔
4984
                result.is_err(),
1✔
4985
                "Expected error when chain_id != CHAIN_ID_MAINNET on mainnet"
4986
            );
4987
        }
4988

4989
        // **Case 1b:** Should not panic when `is_mainnet` is true and `chain_id` == `CHAIN_ID_MAINNET`
4990
        {
4991
            let config_file = make_burnchain_config_file(true, Some(CHAIN_ID_MAINNET));
1✔
4992

4993
            let config = config_file
1✔
4994
                .into_config_default(default_burnchain_config.clone())
1✔
4995
                .expect("Should not panic");
1✔
4996
            assert_eq!(config.chain_id, CHAIN_ID_MAINNET);
1✔
4997
        }
4998

4999
        // **Case 1c:** Should not panic when `is_mainnet` is false; chain_id should be as provided
5000
        {
5001
            let chain_id = 123456;
1✔
5002
            let config_file = make_burnchain_config_file(false, Some(chain_id));
1✔
5003

5004
            let config = config_file
1✔
5005
                .into_config_default(default_burnchain_config.clone())
1✔
5006
                .expect("Should not panic");
1✔
5007
            assert_eq!(config.chain_id, chain_id);
1✔
5008
        }
5009

5010
        // **Case 2a:** Should not panic when `chain_id` is None and `is_mainnet` is true
5011
        {
5012
            let config_file = make_burnchain_config_file(true, None);
1✔
5013

5014
            let config = config_file
1✔
5015
                .into_config_default(default_burnchain_config.clone())
1✔
5016
                .expect("Should not panic");
1✔
5017
            assert_eq!(config.chain_id, CHAIN_ID_MAINNET);
1✔
5018
        }
5019

5020
        // **Case 2b:** Should not panic when `chain_id` is None and `is_mainnet` is false
5021
        {
5022
            let config_file = make_burnchain_config_file(false, None);
1✔
5023

5024
            let config = config_file
1✔
5025
                .into_config_default(default_burnchain_config)
1✔
5026
                .expect("Should not panic");
1✔
5027
            assert_eq!(config.chain_id, CHAIN_ID_TESTNET);
1✔
5028
        }
5029
    }
1✔
5030

5031
    #[test]
5032
    fn test_load_node_marf_config() {
1✔
5033
        // Check MARF defaults
5034
        let config = utils::config_from_valid_string(
1✔
5035
            r#"
1✔
5036
                [node]
1✔
5037
                "#,
1✔
5038
        );
5039

5040
        assert_eq!(None, config.node.marf_cache_strategy, "default cache");
1✔
5041
        assert_eq!(
1✔
5042
            true, config.node.marf_defer_hashing,
5043
            "default defer hashing"
5044
        );
5045
        assert_eq!(true, config.node.marf_compress, "default compress");
1✔
5046

5047
        let cfg_opts = config.node.get_marf_opts();
1✔
5048
        assert_eq!("noop", cfg_opts.cache_strategy, "default cache opt");
1✔
5049
        assert_eq!(
1✔
5050
            TrieHashCalculationMode::Deferred,
5051
            cfg_opts.hash_calculation_mode,
5052
            "default defer hashing opt"
5053
        );
5054
        assert_eq!(true, cfg_opts.compress, "default compress opt");
1✔
5055
        assert_eq!(
1✔
5056
            false, cfg_opts.external_blobs,
5057
            "internal default blob setting"
5058
        );
5059
        assert_eq!(
1✔
5060
            false, cfg_opts.force_db_migrate,
5061
            "internal default migrate setting"
5062
        );
5063

5064
        // Check MARF full config
5065
        let config = utils::config_from_valid_string(
1✔
5066
            r#"
1✔
5067
                [node]
1✔
5068
                marf_cache_strategy = "everything"
1✔
5069
                marf_defer_hashing = false
1✔
5070
                marf_compress = false
1✔
5071
                "#,
1✔
5072
        );
5073

5074
        assert_eq!(
1✔
5075
            Some("everything".to_string()),
1✔
5076
            config.node.marf_cache_strategy,
5077
            "configured cache"
5078
        );
5079
        assert_eq!(
1✔
5080
            false, config.node.marf_defer_hashing,
5081
            "configured defer hashing"
5082
        );
5083
        assert_eq!(false, config.node.marf_compress, "configured compress");
1✔
5084

5085
        let cfg_opts = config.node.get_marf_opts();
1✔
5086
        assert_eq!(
1✔
5087
            "everything", cfg_opts.cache_strategy,
5088
            "configured cache opt"
5089
        );
5090
        assert_eq!(
1✔
5091
            TrieHashCalculationMode::Immediate,
5092
            cfg_opts.hash_calculation_mode,
5093
            "configured hash opt"
5094
        );
5095
        assert_eq!(false, cfg_opts.compress, "configured compress opt");
1✔
5096
        assert_eq!(
1✔
5097
            false, cfg_opts.external_blobs,
5098
            "internal default blob setting"
5099
        );
5100
        assert_eq!(
1✔
5101
            false, cfg_opts.force_db_migrate,
5102
            "internal default migrate setting"
5103
        );
5104
    }
1✔
5105
}
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