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

stacks-network / stacks-core / 25903914664-1

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

Pull #7199

github

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

103343 of 219309 relevant lines covered (47.12%)

12880462.62 hits per line

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

87.31
/stacks-node/src/node.rs
1
use std::collections::{HashMap, HashSet};
2
use std::net::SocketAddr;
3
use std::thread::JoinHandle;
4
use std::{env, thread, time};
5

6
use rand::RngCore;
7
use stacks::burnchains::bitcoin::BitcoinNetworkType;
8
use stacks::burnchains::db::BurnchainDB;
9
use stacks::burnchains::{PoxConstants, Txid};
10
use stacks::chainstate::burn::db::sortdb::SortitionDB;
11
use stacks::chainstate::burn::operations::leader_block_commit::{
12
    RewardSetInfo, BURN_BLOCK_MINED_AT_MODULUS,
13
};
14
use stacks::chainstate::burn::operations::{
15
    BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp,
16
};
17
use stacks::chainstate::burn::ConsensusHash;
18
use stacks::chainstate::stacks::address::PoxAddress;
19
use stacks::chainstate::stacks::db::{
20
    ChainStateBootData, ChainstateAccountBalance, ChainstateAccountLockup, ChainstateBNSName,
21
    ChainstateBNSNamespace, ClarityTx, StacksChainState, StacksEpochReceipt, StacksHeaderInfo,
22
};
23
use stacks::chainstate::stacks::events::{
24
    StacksTransactionEvent, StacksTransactionReceipt, TransactionOrigin,
25
};
26
use stacks::chainstate::stacks::{
27
    CoinbasePayload, StacksBlock, StacksMicroblock, StacksTransaction, StacksTransactionSigner,
28
    TransactionAnchorMode, TransactionPayload, TransactionVersion,
29
};
30
use stacks::core::mempool::MemPoolDB;
31
use stacks::core::{EpochList, STACKS_EPOCH_2_1_MARKER};
32
use stacks::cost_estimates::metrics::UnitMetric;
33
use stacks::cost_estimates::UnitEstimator;
34
use stacks::net::atlas::{AtlasConfig, AtlasDB, AttachmentInstance};
35
use stacks::net::db::PeerDB;
36
use stacks::net::p2p::PeerNetwork;
37
use stacks::net::stackerdb::StackerDBs;
38
use stacks::net::RPCHandlerArgs;
39
use stacks::util_lib::strings::UrlString;
40
use stacks_common::types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, TrieHash, VRFSeed};
41
use stacks_common::types::net::PeerAddress;
42
use stacks_common::util::get_epoch_time_secs;
43
use stacks_common::util::hash::Sha256Sum;
44
use stacks_common::util::secp256k1::Secp256k1PrivateKey;
45
use stacks_common::util::vrf::VRFPublicKey;
46

47
use super::{BurnchainController, BurnchainTip, Config, EventDispatcher, Keychain, Tenure};
48
use crate::burnchains::make_bitcoin_indexer;
49
use crate::genesis_data::USE_TEST_GENESIS_CHAINSTATE;
50
use crate::run_loop;
51
use crate::run_loop::RegisteredKey;
52

53
#[derive(Debug, Clone)]
54
pub struct ChainTip {
55
    pub metadata: StacksHeaderInfo,
56
    pub block: StacksBlock,
57
    pub receipts: Vec<StacksTransactionReceipt>,
58
}
59

60
impl ChainTip {
61
    pub fn genesis(
426✔
62
        first_burnchain_block_hash: &BurnchainHeaderHash,
426✔
63
        first_burnchain_block_height: u64,
426✔
64
        first_burnchain_block_timestamp: u64,
426✔
65
    ) -> ChainTip {
426✔
66
        ChainTip {
426✔
67
            metadata: StacksHeaderInfo::genesis(
426✔
68
                TrieHash([0u8; 32]),
426✔
69
                first_burnchain_block_hash,
426✔
70
                first_burnchain_block_height as u32,
426✔
71
                first_burnchain_block_timestamp,
426✔
72
            ),
426✔
73
            block: StacksBlock::genesis_block(),
426✔
74
            receipts: vec![],
426✔
75
        }
426✔
76
    }
426✔
77
}
78

79
/// Node is a structure modelising an active node working on the stacks chain.
80
pub struct Node {
81
    pub chain_state: StacksChainState,
82
    pub config: Config,
83
    active_registered_key: Option<RegisteredKey>,
84
    bootstraping_chain: bool,
85
    pub burnchain_tip: Option<BurnchainTip>,
86
    pub chain_tip: Option<ChainTip>,
87
    keychain: Keychain,
88
    last_sortitioned_block: Option<BurnchainTip>,
89
    event_dispatcher: EventDispatcher,
90
    nonce: u64,
91
    leader_key_registers: HashSet<Txid>,
92
    block_commits: HashSet<Txid>,
93
}
94

95
pub fn get_account_lockups(
282✔
96
    use_test_chainstate_data: bool,
282✔
97
) -> Box<dyn Iterator<Item = ChainstateAccountLockup>> {
282✔
98
    Box::new(
282✔
99
        stx_genesis::GenesisData::new(use_test_chainstate_data)
282✔
100
            .read_lockups()
282✔
101
            .map(|item| ChainstateAccountLockup {
282✔
102
                address: item.address,
5,640✔
103
                amount: item.amount,
5,640✔
104
                block_height: item.block_height,
5,640✔
105
            }),
5,640✔
106
    )
107
}
282✔
108

109
pub fn get_account_balances(
282✔
110
    use_test_chainstate_data: bool,
282✔
111
) -> Box<dyn Iterator<Item = ChainstateAccountBalance>> {
282✔
112
    Box::new(
282✔
113
        stx_genesis::GenesisData::new(use_test_chainstate_data)
282✔
114
            .read_balances()
282✔
115
            .map(|item| ChainstateAccountBalance {
282✔
116
                address: item.address,
6,768✔
117
                amount: item.amount,
6,768✔
118
            }),
6,768✔
119
    )
120
}
282✔
121

122
pub fn get_namespaces(
282✔
123
    use_test_chainstate_data: bool,
282✔
124
) -> Box<dyn Iterator<Item = ChainstateBNSNamespace>> {
282✔
125
    Box::new(
282✔
126
        stx_genesis::GenesisData::new(use_test_chainstate_data)
282✔
127
            .read_namespaces()
282✔
128
            .map(|item| ChainstateBNSNamespace {
282✔
129
                namespace_id: item.namespace_id,
1,410✔
130
                importer: item.importer,
1,410✔
131
                buckets: item.buckets,
1,410✔
132
                base: item.base as u64,
1,410✔
133
                coeff: item.coeff as u64,
1,410✔
134
                nonalpha_discount: item.nonalpha_discount as u64,
1,410✔
135
                no_vowel_discount: item.no_vowel_discount as u64,
1,410✔
136
                lifetime: item.lifetime as u64,
1,410✔
137
            }),
1,410✔
138
    )
139
}
282✔
140

141
pub fn get_names(use_test_chainstate_data: bool) -> Box<dyn Iterator<Item = ChainstateBNSName>> {
282✔
142
    Box::new(
282✔
143
        stx_genesis::GenesisData::new(use_test_chainstate_data)
282✔
144
            .read_names()
282✔
145
            .map(|item| ChainstateBNSName {
282✔
146
                fully_qualified_name: item.fully_qualified_name,
3,384✔
147
                owner: item.owner,
3,384✔
148
                zonefile_hash: item.zonefile_hash,
3,384✔
149
            }),
3,384✔
150
    )
151
}
282✔
152

153
// This function is called for helium and mocknet.
154
#[allow(clippy::too_many_arguments)]
155
fn spawn_peer(
2✔
156
    is_mainnet: bool,
2✔
157
    chain_id: u32,
2✔
158
    mut this: PeerNetwork,
2✔
159
    p2p_sock: &SocketAddr,
2✔
160
    rpc_sock: &SocketAddr,
2✔
161
    burn_db_path: String,
2✔
162
    stacks_chainstate_path: String,
2✔
163
    pox_consts: PoxConstants,
2✔
164
    event_dispatcher: EventDispatcher,
2✔
165
    exit_at_block_height: Option<u64>,
2✔
166
    genesis_chainstate_hash: Sha256Sum,
2✔
167
    poll_timeout: u64,
2✔
168
    config: Config,
2✔
169
) -> JoinHandle<()> {
2✔
170
    this.bind(p2p_sock, rpc_sock).unwrap();
2✔
171
    let server_thread = thread::spawn(move || {
2✔
172
        // create estimators, metric instances for RPC handler
173
        let cost_estimator = config
2✔
174
            .make_cost_estimator()
2✔
175
            .unwrap_or_else(|| Box::new(UnitEstimator));
2✔
176
        let metric = config
2✔
177
            .make_cost_metric()
2✔
178
            .unwrap_or_else(|| Box::new(UnitMetric));
2✔
179
        let fee_estimator = config.make_fee_estimator();
2✔
180

181
        let handler_args = RPCHandlerArgs {
2✔
182
            exit_at_block_height,
2✔
183
            cost_estimator: Some(cost_estimator.as_ref()),
2✔
184
            cost_metric: Some(metric.as_ref()),
2✔
185
            fee_estimator: fee_estimator.as_ref().map(|x| x.as_ref()),
2✔
186
            genesis_chainstate_hash,
2✔
187
            ..RPCHandlerArgs::default()
2✔
188
        };
189

190
        loop {
191
            let sortdb = match SortitionDB::open(
84✔
192
                &burn_db_path,
84✔
193
                false,
84✔
194
                pox_consts.clone(),
84✔
195
                Some(config.node.get_marf_opts()),
84✔
196
            ) {
84✔
197
                Ok(x) => x,
84✔
198
                Err(e) => {
×
199
                    warn!("Error while connecting burnchain db in peer loop: {e}");
×
200
                    thread::sleep(time::Duration::from_secs(1));
×
201
                    continue;
×
202
                }
203
            };
204
            let (mut chainstate, _) = match StacksChainState::open(
84✔
205
                is_mainnet,
84✔
206
                chain_id,
84✔
207
                &stacks_chainstate_path,
84✔
208
                Some(config.node.get_marf_opts()),
84✔
209
            ) {
84✔
210
                Ok(x) => x,
84✔
211
                Err(e) => {
×
212
                    warn!("Error while connecting chainstate db in peer loop: {e}");
×
213
                    thread::sleep(time::Duration::from_secs(1));
×
214
                    continue;
×
215
                }
216
            };
217

218
            let estimator = Box::new(UnitEstimator);
84✔
219
            let metric = Box::new(UnitMetric);
84✔
220

221
            let mut mem_pool = match MemPoolDB::open(
84✔
222
                is_mainnet,
84✔
223
                chain_id,
84✔
224
                &stacks_chainstate_path,
84✔
225
                estimator,
84✔
226
                metric,
84✔
227
            ) {
84✔
228
                Ok(x) => x,
84✔
229
                Err(e) => {
×
230
                    warn!("Error while connecting to mempool db in peer loop: {e}");
×
231
                    thread::sleep(time::Duration::from_secs(1));
×
232
                    continue;
×
233
                }
234
            };
235

236
            let indexer = make_bitcoin_indexer(&config, None);
84✔
237

238
            let net_result = this
84✔
239
                .run(
84✔
240
                    &indexer,
84✔
241
                    &sortdb,
84✔
242
                    &mut chainstate,
84✔
243
                    &mut mem_pool,
84✔
244
                    None,
84✔
245
                    false,
246
                    false,
247
                    poll_timeout,
84✔
248
                    &handler_args,
84✔
249
                    config.node.txindex,
84✔
250
                )
251
                .unwrap();
84✔
252
            if net_result.has_transactions() {
84✔
253
                event_dispatcher.process_new_mempool_txs(net_result.transactions())
1✔
254
            }
81✔
255
            // Dispatch retrieved attachments, if any.
256
            if net_result.has_attachments() {
82✔
257
                event_dispatcher.process_new_attachments(&net_result.attachments);
×
258
            }
82✔
259
        }
260
    });
261
    server_thread
2✔
262
}
2✔
263

264
// Check if the small test genesis chainstate data should be used.
265
// First check env var, then config file, then use default.
266
pub fn use_test_genesis_chainstate(config: &Config) -> bool {
1,043✔
267
    if env::var("BLOCKSTACK_USE_TEST_GENESIS_CHAINSTATE") == Ok("1".to_string()) {
1,043✔
268
        true
×
269
    } else if let Some(use_test_genesis_chainstate) = config.node.use_test_genesis_chainstate {
1,043✔
270
        use_test_genesis_chainstate
×
271
    } else {
272
        USE_TEST_GENESIS_CHAINSTATE
1,043✔
273
    }
274
}
1,043✔
275

276
impl Node {
277
    /// Instantiate and initialize a new node, given a config
278
    pub fn new(config: Config, boot_block_exec: Box<dyn FnOnce(&mut ClarityTx)>) -> Self {
2✔
279
        let use_test_genesis_data = if config.burnchain.mode == "mocknet" {
2✔
280
            use_test_genesis_chainstate(&config)
2✔
281
        } else {
282
            USE_TEST_GENESIS_CHAINSTATE
×
283
        };
284

285
        let keychain = Keychain::default(config.node.seed.clone());
2✔
286

287
        let initial_balances = config
2✔
288
            .initial_balances
2✔
289
            .iter()
2✔
290
            .map(|e| (e.address.clone(), e.amount))
7✔
291
            .collect();
2✔
292
        let pox_constants = match config.burnchain.get_bitcoin_network() {
2✔
293
            (_, BitcoinNetworkType::Mainnet) => PoxConstants::mainnet_default(),
×
294
            (_, BitcoinNetworkType::Testnet) => PoxConstants::testnet_default(),
×
295
            (_, BitcoinNetworkType::Regtest) => PoxConstants::regtest_default(),
2✔
296
        };
297

298
        let mut boot_data = ChainStateBootData {
2✔
299
            initial_balances,
2✔
300
            first_burnchain_block_hash: BurnchainHeaderHash::zero(),
2✔
301
            first_burnchain_block_height: 0,
302
            first_burnchain_block_timestamp: 0,
303
            pox_constants,
2✔
304
            post_flight_callback: Some(boot_block_exec),
2✔
305
            get_bulk_initial_lockups: Some(Box::new(move || {
2✔
306
                get_account_lockups(use_test_genesis_data)
2✔
307
            })),
2✔
308
            get_bulk_initial_balances: Some(Box::new(move || {
2✔
309
                get_account_balances(use_test_genesis_data)
2✔
310
            })),
2✔
311
            get_bulk_initial_namespaces: Some(Box::new(move || {
2✔
312
                get_namespaces(use_test_genesis_data)
2✔
313
            })),
2✔
314
            get_bulk_initial_names: Some(Box::new(move || get_names(use_test_genesis_data))),
2✔
315
        };
316

317
        let chain_state_result = StacksChainState::open_and_exec(
2✔
318
            config.is_mainnet(),
2✔
319
            config.burnchain.chain_id,
2✔
320
            &config.get_chainstate_path_str(),
2✔
321
            Some(&mut boot_data),
2✔
322
            Some(config.node.get_marf_opts()),
2✔
323
        );
324

325
        let (chain_state, receipts) = match chain_state_result {
2✔
326
            Ok(res) => res,
2✔
327
            Err(err) => panic!(
×
328
                "Error while opening chain state at path {}: {err:?}",
329
                config.get_chainstate_path_str()
×
330
            ),
331
        };
332

333
        let estimator = Box::new(UnitEstimator);
2✔
334
        let metric = Box::new(UnitMetric);
2✔
335

336
        // avoid race to create condition on mempool db
337
        let _mem_pool = MemPoolDB::open(
2✔
338
            config.is_mainnet(),
2✔
339
            config.burnchain.chain_id,
2✔
340
            &chain_state.root_path,
2✔
341
            estimator,
2✔
342
            metric,
2✔
343
        )
344
        .expect("FATAL: failed to initiate mempool");
2✔
345

346
        let mut event_dispatcher = EventDispatcher::new(config.get_working_dir());
2✔
347

348
        for observer in &config.events_observers {
2✔
349
            event_dispatcher.register_observer(observer);
×
350
        }
×
351
        event_dispatcher.process_pending_payloads();
2✔
352

353
        let burnchain_config = config.get_burnchain();
2✔
354

355
        // instantiate DBs
356
        let _burnchain_db = BurnchainDB::connect(
2✔
357
            &burnchain_config.get_burnchaindb_path(),
2✔
358
            &burnchain_config,
2✔
359
            true,
360
        )
361
        .expect("FATAL: failed to connect to burnchain DB");
2✔
362

363
        run_loop::announce_boot_receipts(
2✔
364
            &mut event_dispatcher,
2✔
365
            &chain_state,
2✔
366
            &burnchain_config.pox_constants,
2✔
367
            &receipts,
2✔
368
        );
369

370
        Self {
2✔
371
            active_registered_key: None,
2✔
372
            bootstraping_chain: false,
2✔
373
            chain_state,
2✔
374
            chain_tip: None,
2✔
375
            keychain,
2✔
376
            last_sortitioned_block: None,
2✔
377
            config,
2✔
378
            burnchain_tip: None,
2✔
379
            nonce: 0,
2✔
380
            event_dispatcher,
2✔
381
            leader_key_registers: HashSet::new(),
2✔
382
            block_commits: HashSet::new(),
2✔
383
        }
2✔
384
    }
2✔
385

386
    fn make_atlas_config() -> AtlasConfig {
20✔
387
        AtlasConfig::new(false)
20✔
388
    }
20✔
389

390
    pub fn make_atlas_db(&self) -> AtlasDB {
11✔
391
        AtlasDB::connect(
11✔
392
            Self::make_atlas_config(),
11✔
393
            &self.config.get_atlas_db_file_path(),
11✔
394
            true,
395
        )
396
        .unwrap()
11✔
397
    }
11✔
398

399
    // This function is used for helium and mocknet.
400
    pub fn spawn_peer_server(&mut self) {
2✔
401
        // we can call _open_ here rather than _connect_, since connect is first called in
402
        //   make_genesis_block
403
        let burnchain = self.config.get_burnchain();
2✔
404
        let sortdb = SortitionDB::open(
2✔
405
            &self.config.get_burn_db_file_path(),
2✔
406
            true,
407
            burnchain.pox_constants.clone(),
2✔
408
            Some(self.config.node.get_marf_opts()),
2✔
409
        )
410
        .expect("Error while instantiating burnchain db");
2✔
411

412
        let epochs_vec = SortitionDB::get_stacks_epochs(sortdb.conn())
2✔
413
            .expect("Error while loading stacks epochs");
2✔
414
        let epochs = EpochList::new(&epochs_vec);
2✔
415

416
        Config::assert_valid_epoch_settings(&burnchain, &epochs);
2✔
417

418
        let view = {
2✔
419
            let sortition_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
2✔
420
                .expect("Failed to get sortition tip");
2✔
421
            SortitionDB::get_burnchain_view(&sortdb.index_conn(), &burnchain, &sortition_tip)
2✔
422
                .unwrap()
2✔
423
        };
424

425
        // create a new peerdb
426
        let data_url = UrlString::try_from(self.config.node.data_url.to_string()).unwrap();
2✔
427

428
        let initial_neighbors = self.config.node.bootstrap_node.clone();
2✔
429

430
        println!("BOOTSTRAP WITH {initial_neighbors:?}");
2✔
431

432
        let rpc_sock: SocketAddr =
2✔
433
            self.config.node.rpc_bind.parse().unwrap_or_else(|_| {
2✔
434
                panic!("Failed to parse socket: {}", &self.config.node.rpc_bind)
×
435
            });
436
        let p2p_sock: SocketAddr =
2✔
437
            self.config.node.p2p_bind.parse().unwrap_or_else(|_| {
2✔
438
                panic!("Failed to parse socket: {}", &self.config.node.p2p_bind)
×
439
            });
440
        let p2p_addr: SocketAddr = self.config.node.p2p_address.parse().unwrap_or_else(|_| {
2✔
441
            panic!("Failed to parse socket: {}", &self.config.node.p2p_address)
×
442
        });
443
        let node_privkey = {
2✔
444
            let mut re_hashed_seed = self.config.node.local_peer_seed.clone();
2✔
445
            let my_private_key = loop {
2✔
446
                match Secp256k1PrivateKey::from_slice(&re_hashed_seed[..]) {
2✔
447
                    Ok(sk) => break sk,
2✔
448
                    Err(_) => {
449
                        re_hashed_seed = Sha256Sum::from_data(&re_hashed_seed[..])
×
450
                            .as_bytes()
×
451
                            .to_vec()
×
452
                    }
453
                }
454
            };
455
            my_private_key
2✔
456
        };
457

458
        let mut peerdb = PeerDB::connect(
2✔
459
            &self.config.get_peer_db_file_path(),
2✔
460
            true,
461
            self.config.burnchain.chain_id,
2✔
462
            burnchain.network_id,
2✔
463
            Some(node_privkey),
2✔
464
            self.config.connection_options.private_key_lifetime,
2✔
465
            PeerAddress::from_socketaddr(&p2p_addr),
2✔
466
            p2p_sock.port(),
2✔
467
            data_url,
2✔
468
            &[],
2✔
469
            Some(&initial_neighbors),
2✔
470
            &[],
2✔
471
        )
472
        .unwrap();
2✔
473

474
        println!("DENY NEIGHBORS {:?}", &self.config.node.deny_nodes);
2✔
475
        {
476
            let tx = peerdb.tx_begin().unwrap();
2✔
477
            for denied in self.config.node.deny_nodes.iter() {
2✔
478
                PeerDB::set_deny_peer(
×
479
                    &tx,
×
480
                    denied.addr.network_id,
×
481
                    &denied.addr.addrbytes,
×
482
                    denied.addr.port,
×
483
                    get_epoch_time_secs() + 24 * 365 * 3600,
×
484
                )
×
485
                .unwrap();
×
486
            }
×
487
            tx.commit().unwrap();
2✔
488
        }
489
        let atlasdb = self.make_atlas_db();
2✔
490

491
        let stackerdbs =
2✔
492
            StackerDBs::connect(&self.config.get_stacker_db_file_path(), true).unwrap();
2✔
493

494
        let local_peer = match PeerDB::get_local_peer(peerdb.conn()) {
2✔
495
            Ok(local_peer) => local_peer,
2✔
496
            _ => panic!("Unable to retrieve local peer"),
×
497
        };
498

499
        let event_dispatcher = self.event_dispatcher.clone();
2✔
500
        let exit_at_block_height = self.config.burnchain.process_exit_at_block_height;
2✔
501
        let burnchain_db = burnchain
2✔
502
            .open_burnchain_db(false)
2✔
503
            .expect("Failed to open burnchain DB");
2✔
504

505
        let p2p_net = PeerNetwork::new(
2✔
506
            peerdb,
2✔
507
            atlasdb,
2✔
508
            stackerdbs,
2✔
509
            burnchain_db,
2✔
510
            local_peer,
2✔
511
            self.config.burnchain.peer_version,
2✔
512
            burnchain.clone(),
2✔
513
            view,
2✔
514
            self.config.connection_options.clone(),
2✔
515
            HashMap::new(),
2✔
516
            epochs,
2✔
517
        );
518
        let _join_handle = spawn_peer(
2✔
519
            self.config.is_mainnet(),
2✔
520
            self.config.burnchain.chain_id,
2✔
521
            p2p_net,
2✔
522
            &p2p_sock,
2✔
523
            &rpc_sock,
2✔
524
            self.config.get_burn_db_file_path(),
2✔
525
            self.config.get_chainstate_path_str(),
2✔
526
            burnchain.pox_constants,
2✔
527
            event_dispatcher,
2✔
528
            exit_at_block_height,
2✔
529
            Sha256Sum::from_hex(stx_genesis::GENESIS_CHAINSTATE_HASH).unwrap(),
2✔
530
            1000,
531
            self.config.clone(),
2✔
532
        );
533

534
        info!("Start HTTP server on: {}", &self.config.node.rpc_bind);
2✔
535
        info!("Start P2P server on: {}", &self.config.node.p2p_bind);
2✔
536
    }
2✔
537

538
    pub fn setup(&mut self, burnchain_controller: &mut Box<dyn BurnchainController>) {
2✔
539
        // Register a new key
540
        let burnchain_tip = burnchain_controller.get_chain_tip();
2✔
541
        let (vrf_pk, _) = self
2✔
542
            .keychain
2✔
543
            .make_vrf_keypair(burnchain_tip.block_snapshot.block_height);
2✔
544
        let consensus_hash = burnchain_tip.block_snapshot.consensus_hash;
2✔
545

546
        let burnchain = self.config.get_burnchain();
2✔
547

548
        let sortdb = SortitionDB::open(
2✔
549
            &self.config.get_burn_db_file_path(),
2✔
550
            true,
551
            burnchain.pox_constants.clone(),
2✔
552
            Some(self.config.node.get_marf_opts()),
2✔
553
        )
554
        .expect("Error while opening sortition db");
2✔
555

556
        let epochs = SortitionDB::get_stacks_epochs(sortdb.conn())
2✔
557
            .expect("FATAL: failed to read sortition DB");
2✔
558

559
        Config::assert_valid_epoch_settings(&burnchain, &epochs);
2✔
560

561
        let cur_epoch =
2✔
562
            SortitionDB::get_stacks_epoch(sortdb.conn(), burnchain_tip.block_snapshot.block_height)
2✔
563
                .expect("FATAL: failed to read sortition DB")
2✔
564
                .expect("FATAL: no epoch defined");
2✔
565

566
        let key_reg_op = self.generate_leader_key_register_op(vrf_pk, consensus_hash);
2✔
567
        let mut op_signer = self.keychain.generate_op_signer();
2✔
568
        let key_txid = burnchain_controller
2✔
569
            .submit_operation(cur_epoch.epoch_id, key_reg_op, &mut op_signer)
2✔
570
            .expect("FATAL: failed to submit leader key register operation");
2✔
571

572
        self.leader_key_registers.insert(key_txid);
2✔
573
    }
2✔
574

575
    /// Process an state coming from the burnchain, by extracting the validated KeyRegisterOp
576
    /// and inspecting if a sortition was won.
577
    pub fn process_burnchain_state(
13✔
578
        &mut self,
13✔
579
        burnchain_tip: &BurnchainTip,
13✔
580
    ) -> (Option<BurnchainTip>, bool) {
13✔
581
        let mut new_key = None;
13✔
582
        let mut last_sortitioned_block = None;
13✔
583
        let mut won_sortition = false;
13✔
584
        let ops = &burnchain_tip.state_transition.accepted_ops;
13✔
585

586
        for op in ops.iter() {
13✔
587
            match op {
11✔
588
                BlockstackOperationType::LeaderKeyRegister(ref op) => {
2✔
589
                    if self.leader_key_registers.contains(&op.txid) {
2✔
590
                        // Registered key has been mined
2✔
591
                        new_key = Some(RegisteredKey {
2✔
592
                            vrf_public_key: op.public_key.clone(),
2✔
593
                            block_height: op.block_height,
2✔
594
                            op_vtxindex: op.vtxindex,
2✔
595
                            target_block_height: op.block_height - 1,
2✔
596
                            memo: op.memo.clone(),
2✔
597
                        });
2✔
598
                    }
2✔
599
                }
600
                BlockstackOperationType::LeaderBlockCommit(ref op) => {
9✔
601
                    if op.txid == burnchain_tip.block_snapshot.winning_block_txid {
9✔
602
                        last_sortitioned_block = Some(burnchain_tip.clone());
9✔
603
                        if self.block_commits.contains(&op.txid) {
9✔
604
                            won_sortition = true;
9✔
605
                        }
9✔
606
                    }
×
607
                }
608
                _ => {
×
609
                    // no-op, ops are not supported / produced at this point.
×
610
                }
×
611
            }
612
        }
613

614
        // Update the active key so we use the latest registered key.
615
        if new_key.is_some() {
13✔
616
            self.active_registered_key = new_key;
2✔
617
        }
11✔
618

619
        // Update last_sortitioned_block so we keep a reference to the latest
620
        // block including a sortition.
621
        if last_sortitioned_block.is_some() {
13✔
622
            self.last_sortitioned_block = last_sortitioned_block;
9✔
623
        }
9✔
624

625
        // Keep a pointer of the burnchain's chain tip.
626
        self.burnchain_tip = Some(burnchain_tip.clone());
13✔
627

628
        (self.last_sortitioned_block.clone(), won_sortition)
13✔
629
    }
13✔
630

631
    /// Prepares the node to run a tenure consisting in bootstraping the chain.
632
    ///
633
    /// Will internally call initiate_new_tenure().
634
    pub fn initiate_genesis_tenure(&mut self, burnchain_tip: &BurnchainTip) -> Option<Tenure> {
2✔
635
        // Set the `bootstraping_chain` flag, that will be unset once the
636
        // bootstraping tenure ran successfully (process_tenure).
637
        self.bootstraping_chain = true;
2✔
638

639
        self.last_sortitioned_block = Some(burnchain_tip.clone());
2✔
640

641
        self.initiate_new_tenure()
2✔
642
    }
2✔
643

644
    /// Constructs and returns an instance of Tenure, that can be run
645
    /// on an isolated thread and discarded or canceled without corrupting the
646
    /// chain state of the node.
647
    pub fn initiate_new_tenure(&mut self) -> Option<Tenure> {
11✔
648
        // Get the latest registered key
649
        let registered_key = match &self.active_registered_key {
11✔
650
            None => {
651
                // We're continuously registering new keys, as such, this branch
652
                // should be unreachable.
653
                unreachable!()
×
654
            }
655
            Some(ref key) => key,
11✔
656
        };
657

658
        let burnchain = self.config.get_burnchain();
11✔
659
        let sortdb = SortitionDB::open(
11✔
660
            &self.config.get_burn_db_file_path(),
11✔
661
            true,
662
            burnchain.pox_constants,
11✔
663
            Some(self.config.node.get_marf_opts()),
11✔
664
        )
665
        .expect("Error while opening sortition db");
11✔
666
        let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
11✔
667
            .expect("FATAL: failed to query canonical burn chain tip");
11✔
668

669
        // Generates a proof out of the sortition hash provided in the params.
670
        let Some(vrf_proof) = self.keychain.generate_proof(
11✔
671
            registered_key.target_block_height,
11✔
672
            tip.sortition_hash.as_bytes(),
11✔
673
        ) else {
11✔
674
            warn!("Failed to generate VRF proof, will be unable to initiate new tenure");
×
675
            return None;
×
676
        };
677

678
        // Generates a new secret key for signing the trail of microblocks
679
        // of the upcoming tenure.
680
        let microblock_secret_key = self.keychain.get_microblock_key(tip.block_height);
11✔
681

682
        // Get the stack's chain tip
683
        let chain_tip = match self.bootstraping_chain {
11✔
684
            true => ChainTip::genesis(&BurnchainHeaderHash::zero(), 0, 0),
2✔
685
            false => match &self.chain_tip {
9✔
686
                Some(chain_tip) => chain_tip.clone(),
9✔
687
                None => unreachable!(),
×
688
            },
689
        };
690

691
        let estimator = self
11✔
692
            .config
11✔
693
            .make_cost_estimator()
11✔
694
            .unwrap_or_else(|| Box::new(UnitEstimator));
11✔
695
        let metric = self
11✔
696
            .config
11✔
697
            .make_cost_metric()
11✔
698
            .unwrap_or_else(|| Box::new(UnitMetric));
11✔
699

700
        let mem_pool = MemPoolDB::open(
11✔
701
            self.config.is_mainnet(),
11✔
702
            self.config.burnchain.chain_id,
11✔
703
            &self.chain_state.root_path,
11✔
704
            estimator,
11✔
705
            metric,
11✔
706
        )
707
        .expect("FATAL: failed to open mempool");
11✔
708

709
        // Construct the coinbase transaction - 1st txn that should be handled and included in
710
        // the upcoming tenure.
711
        let coinbase_tx = self.generate_coinbase_tx(self.config.is_mainnet());
11✔
712

713
        let burn_fee_cap = self.config.burnchain.burn_fee_cap;
11✔
714

715
        let block_to_build_upon = match &self.last_sortitioned_block {
11✔
716
            None => unreachable!(),
×
717
            Some(block) => block.clone(),
11✔
718
        };
719

720
        // Construct the upcoming tenure
721
        let tenure = Tenure::new(
11✔
722
            chain_tip,
11✔
723
            coinbase_tx,
11✔
724
            self.config.clone(),
11✔
725
            mem_pool,
11✔
726
            microblock_secret_key,
11✔
727
            block_to_build_upon,
11✔
728
            vrf_proof,
11✔
729
            burn_fee_cap,
11✔
730
        );
731

732
        Some(tenure)
11✔
733
    }
11✔
734

735
    pub fn commit_artifacts(
9✔
736
        &mut self,
9✔
737
        anchored_block_from_ongoing_tenure: &StacksBlock,
9✔
738
        burnchain_tip: &BurnchainTip,
9✔
739
        burnchain_controller: &mut Box<dyn BurnchainController>,
9✔
740
        burn_fee: u64,
9✔
741
    ) {
9✔
742
        if self.active_registered_key.is_some() {
9✔
743
            let registered_key = self.active_registered_key.clone().unwrap();
9✔
744

745
            let Some(vrf_proof) = self.keychain.generate_proof(
9✔
746
                registered_key.target_block_height,
9✔
747
                burnchain_tip.block_snapshot.sortition_hash.as_bytes(),
9✔
748
            ) else {
9✔
749
                warn!("Failed to generate VRF proof, will be unable to mine commits");
×
750
                return;
×
751
            };
752

753
            let op = self.generate_block_commit_op(
9✔
754
                anchored_block_from_ongoing_tenure.header.block_hash(),
9✔
755
                burn_fee,
9✔
756
                &registered_key,
9✔
757
                burnchain_tip,
9✔
758
                VRFSeed::from_proof(&vrf_proof),
9✔
759
            );
760

761
            let burnchain = self.config.get_burnchain();
9✔
762
            let sortdb = SortitionDB::open(
9✔
763
                &self.config.get_burn_db_file_path(),
9✔
764
                true,
765
                burnchain.pox_constants,
9✔
766
                Some(self.config.node.get_marf_opts()),
9✔
767
            )
768
            .expect("Error while opening sortition db");
9✔
769

770
            let cur_epoch = SortitionDB::get_stacks_epoch(
9✔
771
                sortdb.conn(),
9✔
772
                burnchain_tip.block_snapshot.block_height,
9✔
773
            )
774
            .expect("FATAL: failed to read sortition DB")
9✔
775
            .expect("FATAL: no epoch defined");
9✔
776

777
            let mut op_signer = self.keychain.generate_op_signer();
9✔
778
            let txid = burnchain_controller
9✔
779
                .submit_operation(cur_epoch.epoch_id, op, &mut op_signer)
9✔
780
                .expect("FATAL: failed to submit block-commit");
9✔
781

782
            self.block_commits.insert(txid);
9✔
783
        } else {
784
            warn!("No leader key active!");
×
785
        }
786
    }
9✔
787

788
    /// Process artifacts from the tenure.
789
    /// At this point, we're modifying the chainstate, and merging the artifacts from the previous tenure.
790
    pub fn process_tenure(
9✔
791
        &mut self,
9✔
792
        anchored_block: &StacksBlock,
9✔
793
        consensus_hash: &ConsensusHash,
9✔
794
        microblocks: Vec<StacksMicroblock>,
9✔
795
        db: &mut SortitionDB,
9✔
796
        atlas_db: &mut AtlasDB,
9✔
797
    ) -> ChainTip {
9✔
798
        let _parent_consensus_hash = {
9✔
799
            // look up parent consensus hash
800
            let ic = db.index_conn();
9✔
801
            let parent_consensus_hash = StacksChainState::get_parent_consensus_hash(
9✔
802
                &ic,
9✔
803
                &anchored_block.header.parent_block,
9✔
804
                consensus_hash,
9✔
805
            )
806
            .unwrap_or_else(|_| {
9✔
807
                panic!(
×
808
                    "BUG: could not query chainstate to find parent consensus hash of {consensus_hash}/{}",
809
                    &anchored_block.block_hash()
×
810
                )
811
            })
812
            .unwrap_or_else(|| {
9✔
813
                panic!(
×
814
                    "BUG: no such parent of block {consensus_hash}/{}",
815
                    &anchored_block.block_hash()
×
816
                )
817
            });
818

819
            // Preprocess the anchored block
820
            self.chain_state
9✔
821
                .preprocess_anchored_block(
9✔
822
                    &ic,
9✔
823
                    consensus_hash,
9✔
824
                    anchored_block,
9✔
825
                    &parent_consensus_hash,
9✔
826
                    0,
827
                )
828
                .unwrap();
9✔
829

830
            // Preprocess the microblocks
831
            for microblock in microblocks.iter() {
9✔
832
                let res = self
×
833
                    .chain_state
×
834
                    .preprocess_streamed_microblock(
×
835
                        consensus_hash,
×
836
                        &anchored_block.block_hash(),
×
837
                        microblock,
×
838
                    )
839
                    .unwrap();
×
840
                if !res {
×
841
                    warn!(
×
842
                        "Unhandled error while pre-processing microblock {}",
843
                        microblock.header.block_hash()
×
844
                    );
845
                }
×
846
            }
847

848
            parent_consensus_hash
9✔
849
        };
850

851
        let atlas_config = Self::make_atlas_config();
9✔
852
        let mut processed_blocks = vec![];
9✔
853
        loop {
854
            let mut process_blocks_at_tip = {
18✔
855
                let tx = db.tx_begin_at_tip();
18✔
856
                self.chain_state
18✔
857
                    .process_blocks(tx, 1, Some(&self.event_dispatcher))
18✔
858
            };
859
            match process_blocks_at_tip {
18✔
860
                Err(e) => panic!("Error while processing block - {e:?}"),
×
861
                Ok(ref mut blocks) => {
18✔
862
                    if blocks.is_empty() {
18✔
863
                        break;
9✔
864
                    } else {
865
                        for block in blocks.iter() {
9✔
866
                            if let (Some(epoch_receipt), _) = block {
9✔
867
                                let attachments_instances =
9✔
868
                                    self.get_attachment_instances(epoch_receipt, &atlas_config);
9✔
869
                                if !attachments_instances.is_empty() {
9✔
870
                                    for new_attachment in attachments_instances.into_iter() {
×
871
                                        if let Err(e) =
×
872
                                            atlas_db.queue_attachment_instance(&new_attachment)
×
873
                                        {
874
                                            warn!(
×
875
                                                "Atlas: Error writing attachment instance to DB";
876
                                                "err" => ?e,
877
                                                "index_block_hash" => %new_attachment.index_block_hash,
878
                                                "contract_id" => %new_attachment.contract_id,
879
                                                "attachment_index" => %new_attachment.attachment_index,
880
                                            );
881
                                        }
×
882
                                    }
883
                                }
9✔
884
                            }
×
885
                        }
886

887
                        processed_blocks.append(blocks);
9✔
888
                    }
889
                }
890
            }
891
        }
892

893
        // todo(ludo): yikes but good enough in the context of helium:
894
        // we only expect 1 block.
895
        let processed_block = processed_blocks[0].clone().0.unwrap();
9✔
896

897
        let mut cost_estimator = self.config.make_cost_estimator();
9✔
898
        let mut fee_estimator = self.config.make_fee_estimator();
9✔
899

900
        let stacks_epoch =
9✔
901
            SortitionDB::get_stacks_epoch_by_epoch_id(db.conn(), &processed_block.evaluated_epoch)
9✔
902
                .expect("FATAL: could not query sortition DB for epochs")
9✔
903
                .expect("Could not find a stacks epoch.");
9✔
904
        if let Some(estimator) = cost_estimator.as_mut() {
9✔
905
            estimator.notify_block(
9✔
906
                &processed_block.tx_receipts,
9✔
907
                &stacks_epoch.block_limit,
9✔
908
                &stacks_epoch.epoch_id,
9✔
909
            );
9✔
910
        }
9✔
911

912
        if let Some(estimator) = fee_estimator.as_mut() {
9✔
913
            if let Err(e) = estimator.notify_block(&processed_block, &stacks_epoch.block_limit) {
9✔
914
                warn!("FeeEstimator failed to process block receipt";
×
915
                      "stacks_block_hash" => %processed_block.header.anchored_header.block_hash(),
×
916
                      "stacks_height" => %processed_block.header.stacks_block_height,
917
                      "error" => %e);
918
            }
9✔
919
        }
×
920

921
        // Handle events
922
        let receipts = processed_block.tx_receipts;
9✔
923
        let metadata = processed_block.header;
9✔
924
        let block: StacksBlock = {
9✔
925
            let block_path = StacksChainState::get_block_path(
9✔
926
                &self.chain_state.blocks_path,
9✔
927
                &metadata.consensus_hash,
9✔
928
                &metadata.anchored_header.block_hash(),
9✔
929
            )
930
            .unwrap();
9✔
931
            StacksChainState::consensus_load(&block_path).unwrap()
9✔
932
        };
933

934
        let chain_tip = ChainTip {
9✔
935
            metadata,
9✔
936
            block,
9✔
937
            receipts,
9✔
938
        };
9✔
939
        self.chain_tip = Some(chain_tip.clone());
9✔
940

941
        // Unset the `bootstraping_chain` flag.
942
        if self.bootstraping_chain {
9✔
943
            self.bootstraping_chain = false;
2✔
944
        }
7✔
945

946
        chain_tip
9✔
947
    }
9✔
948

949
    pub fn get_attachment_instances(
9✔
950
        &self,
9✔
951
        epoch_receipt: &StacksEpochReceipt,
9✔
952
        atlas_config: &AtlasConfig,
9✔
953
    ) -> HashSet<AttachmentInstance> {
9✔
954
        let mut attachments_instances = HashSet::new();
9✔
955
        for receipt in epoch_receipt.tx_receipts.iter() {
26✔
956
            if let TransactionOrigin::Stacks(ref transaction) = receipt.transaction {
26✔
957
                if let TransactionPayload::ContractCall(ref contract_call) = transaction.payload {
26✔
958
                    let contract_id = contract_call.to_clarity_contract_id();
2✔
959
                    if atlas_config.contracts.contains(&contract_id) {
2✔
960
                        for event in receipt.events.iter() {
×
961
                            if let StacksTransactionEvent::SmartContractEvent(ref event_data) =
×
962
                                event
×
963
                            {
964
                                let res = AttachmentInstance::try_new_from_value(
×
965
                                    &event_data.value,
×
966
                                    &contract_id,
×
967
                                    epoch_receipt.header.index_block_hash(),
×
968
                                    epoch_receipt.header.stacks_block_height,
×
969
                                    receipt.transaction.txid(),
×
970
                                    self.chain_tip
×
971
                                        .as_ref()
×
972
                                        .map(|t| t.metadata.stacks_block_height),
×
973
                                );
974
                                if let Some(attachment_instance) = res {
×
975
                                    attachments_instances.insert(attachment_instance);
×
976
                                }
×
977
                            }
×
978
                        }
979
                    }
2✔
980
                }
24✔
981
            }
×
982
        }
983
        attachments_instances
9✔
984
    }
9✔
985

986
    /// Constructs and returns a LeaderKeyRegisterOp out of the provided params
987
    fn generate_leader_key_register_op(
2✔
988
        &mut self,
2✔
989
        vrf_public_key: VRFPublicKey,
2✔
990
        consensus_hash: ConsensusHash,
2✔
991
    ) -> BlockstackOperationType {
2✔
992
        let mut txid_bytes = [0u8; 32];
2✔
993
        let mut rng = rand::thread_rng();
2✔
994
        rng.fill_bytes(&mut txid_bytes);
2✔
995
        let txid = Txid(txid_bytes);
2✔
996

997
        BlockstackOperationType::LeaderKeyRegister(LeaderKeyRegisterOp {
2✔
998
            public_key: vrf_public_key,
2✔
999
            memo: vec![],
2✔
1000
            consensus_hash,
2✔
1001
            vtxindex: 1,
2✔
1002
            txid,
2✔
1003
            block_height: 0,
2✔
1004
            burn_header_hash: BurnchainHeaderHash::zero(),
2✔
1005
        })
2✔
1006
    }
2✔
1007

1008
    /// Constructs and returns a LeaderBlockCommitOp out of the provided params
1009
    fn generate_block_commit_op(
9✔
1010
        &mut self,
9✔
1011
        block_header_hash: BlockHeaderHash,
9✔
1012
        burn_fee: u64,
9✔
1013
        key: &RegisteredKey,
9✔
1014
        burnchain_tip: &BurnchainTip,
9✔
1015
        vrf_seed: VRFSeed,
9✔
1016
    ) -> BlockstackOperationType {
9✔
1017
        let winning_tx_vtindex = burnchain_tip.get_winning_tx_index().unwrap_or(0);
9✔
1018

1019
        let (parent_block_ptr, parent_vtxindex) = match self.bootstraping_chain {
9✔
1020
            true => (0, 0), // parent_block_ptr and parent_vtxindex should both be 0 on block #1
2✔
1021
            false => (
7✔
1022
                burnchain_tip.block_snapshot.block_height as u32,
7✔
1023
                winning_tx_vtindex as u16,
7✔
1024
            ),
7✔
1025
        };
1026

1027
        let burnchain = self.config.get_burnchain();
9✔
1028
        let commit_outs = if burnchain_tip.block_snapshot.block_height + 1
9✔
1029
            < burnchain.pox_constants.sunset_end
9✔
1030
            && !burnchain.is_in_prepare_phase(burnchain_tip.block_snapshot.block_height + 1)
9✔
1031
        {
1032
            RewardSetInfo::into_commit_outs(None, self.config.is_mainnet())
3✔
1033
        } else {
1034
            vec![PoxAddress::standard_burn_address(self.config.is_mainnet())]
6✔
1035
        };
1036

1037
        let burn_parent_modulus =
9✔
1038
            (burnchain_tip.block_snapshot.block_height % BURN_BLOCK_MINED_AT_MODULUS) as u8;
9✔
1039

1040
        let mut txid_bytes = [0u8; 32];
9✔
1041
        let mut rng = rand::thread_rng();
9✔
1042
        rng.fill_bytes(&mut txid_bytes);
9✔
1043
        let txid = Txid(txid_bytes);
9✔
1044

1045
        BlockstackOperationType::LeaderBlockCommit(LeaderBlockCommitOp {
9✔
1046
            treatment: vec![],
9✔
1047
            sunset_burn: 0,
9✔
1048
            block_header_hash,
9✔
1049
            burn_fee,
9✔
1050
            input: (Txid([0; 32]), 0),
9✔
1051
            apparent_sender: self.keychain.get_burnchain_signer(),
9✔
1052
            key_block_ptr: key.block_height as u32,
9✔
1053
            key_vtxindex: key.op_vtxindex as u16,
9✔
1054
            memo: vec![STACKS_EPOCH_2_1_MARKER],
9✔
1055
            new_seed: vrf_seed,
9✔
1056
            parent_block_ptr,
9✔
1057
            parent_vtxindex,
9✔
1058
            vtxindex: 2,
9✔
1059
            txid,
9✔
1060
            commit_outs,
9✔
1061
            block_height: 0,
9✔
1062
            burn_header_hash: BurnchainHeaderHash::zero(),
9✔
1063
            burn_parent_modulus,
9✔
1064
        })
9✔
1065
    }
9✔
1066

1067
    // Constructs a coinbase transaction
1068
    fn generate_coinbase_tx(&mut self, is_mainnet: bool) -> StacksTransaction {
11✔
1069
        let mut tx_auth = self.keychain.get_transaction_auth().unwrap();
11✔
1070
        tx_auth.set_origin_nonce(self.nonce);
11✔
1071

1072
        let version = if is_mainnet {
11✔
1073
            TransactionVersion::Mainnet
×
1074
        } else {
1075
            TransactionVersion::Testnet
11✔
1076
        };
1077
        let mut tx = StacksTransaction::new(
11✔
1078
            version,
11✔
1079
            tx_auth,
11✔
1080
            TransactionPayload::Coinbase(CoinbasePayload([0u8; 32]), None, None),
11✔
1081
        );
1082
        tx.chain_id = self.config.burnchain.chain_id;
11✔
1083
        tx.anchor_mode = TransactionAnchorMode::OnChainOnly;
11✔
1084
        let mut tx_signer = StacksTransactionSigner::new(&tx);
11✔
1085
        self.keychain.sign_as_origin(&mut tx_signer);
11✔
1086

1087
        // Increment nonce
1088
        self.nonce += 1;
11✔
1089

1090
        tx_signer.get_tx().unwrap()
11✔
1091
    }
11✔
1092
}
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