• 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

84.16
/stackslib/src/burnchains/mod.rs
1
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2
// Copyright (C) 2020 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
use std::collections::HashMap;
18
use std::marker::PhantomData;
19
use std::{error, fmt, io};
20

21
use rusqlite::Error as sqlite_error;
22
use stacks_common::types::chainstate::{BurnchainHeaderHash, ConsensusHash, PoxId};
23
pub use stacks_common::types::{Address, PrivateKey, PublicKey};
24

25
use self::bitcoin::indexer::{
26
    BITCOIN_MAINNET as BITCOIN_NETWORK_ID_MAINNET, BITCOIN_MAINNET_NAME,
27
    BITCOIN_REGTEST as BITCOIN_NETWORK_ID_REGTEST, BITCOIN_REGTEST_NAME,
28
    BITCOIN_TESTNET as BITCOIN_NETWORK_ID_TESTNET, BITCOIN_TESTNET_NAME,
29
};
30
use self::bitcoin::{BitcoinBlock, BitcoinTransaction, Error as btc_error};
31
use crate::chainstate::burn::distribution::BurnSamplePoint;
32
use crate::chainstate::burn::operations::leader_block_commit::{
33
    MissedBlockCommit, OUTPUTS_PER_COMMIT,
34
};
35
use crate::chainstate::burn::operations::{
36
    BlockstackOperationType, Error as op_error, LeaderBlockCommitOp, LeaderKeyRegisterOp,
37
};
38
use crate::chainstate::stacks::address::PoxAddress;
39
use crate::chainstate::stacks::boot::{POX_1_NAME, POX_2_NAME, POX_3_NAME, POX_4_NAME};
40
use crate::chainstate::stacks::index::marf::MARFOpenOpts;
41
use crate::core::*;
42
#[cfg(test)]
43
use crate::net::neighbors::MAX_NEIGHBOR_BLOCK_DELAY;
44
use crate::util_lib::db::Error as db_error;
45

46
/// This module contains drivers and types for all burn chains we support.
47
pub mod bitcoin;
48
pub mod burnchain;
49
pub mod db;
50
pub mod indexer;
51

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

55
pub use stacks_common::types::chainstate::{Txid, TXID_ENCODED_SIZE};
56

57
pub const MAGIC_BYTES_LENGTH: usize = 2;
58

59
#[derive(Debug, Serialize, Deserialize, Default, Copy)]
60
pub struct MagicBytes([u8; MAGIC_BYTES_LENGTH]);
61
impl_array_newtype!(MagicBytes, u8, MAGIC_BYTES_LENGTH);
62
impl MagicBytes {
63
    pub fn default() -> MagicBytes {
×
64
        BLOCKSTACK_MAGIC_MAINNET
×
65
    }
×
66
}
67

68
pub const BLOCKSTACK_MAGIC_MAINNET: MagicBytes = MagicBytes([105, 100]); // 'id'
69

70
#[derive(Debug, PartialEq, Clone)]
71
pub struct BurnchainParameters {
72
    chain_name: String,
73
    network_name: String,
74
    network_id: u32,
75
    stable_confirmations: u32,
76
    consensus_hash_lifetime: u32,
77
    pub first_block_height: u64,
78
    pub first_block_hash: BurnchainHeaderHash,
79
    pub first_block_timestamp: u32,
80
    pub initial_reward_start_block: u64,
81
}
82

83
impl BurnchainParameters {
84
    pub fn from_params(chain: &str, network: &str) -> Option<BurnchainParameters> {
2,060,352✔
85
        match (chain, network) {
2,060,352✔
86
            ("bitcoin", "mainnet") => Some(BurnchainParameters::bitcoin_mainnet()),
2,060,352✔
87
            ("bitcoin", "testnet") => Some(BurnchainParameters::bitcoin_testnet()),
2,060,352✔
88
            ("bitcoin", "regtest") => Some(BurnchainParameters::bitcoin_regtest()),
2,060,352✔
89
            _ => None,
×
90
        }
91
    }
2,060,352✔
92

93
    pub fn bitcoin_mainnet() -> BurnchainParameters {
1,841✔
94
        BurnchainParameters {
1,841✔
95
            chain_name: "bitcoin".to_string(),
1,841✔
96
            network_name: BITCOIN_MAINNET_NAME.to_string(),
1,841✔
97
            network_id: BITCOIN_NETWORK_ID_MAINNET,
1,841✔
98
            stable_confirmations: 7,
1,841✔
99
            consensus_hash_lifetime: 24,
1,841✔
100
            first_block_height: BITCOIN_MAINNET_FIRST_BLOCK_HEIGHT,
1,841✔
101
            first_block_hash: BurnchainHeaderHash::from_hex(BITCOIN_MAINNET_FIRST_BLOCK_HASH)
1,841✔
102
                .unwrap(),
1,841✔
103
            first_block_timestamp: BITCOIN_MAINNET_FIRST_BLOCK_TIMESTAMP,
1,841✔
104
            initial_reward_start_block: BITCOIN_MAINNET_INITIAL_REWARD_START_BLOCK,
1,841✔
105
        }
1,841✔
106
    }
1,841✔
107

108
    pub fn bitcoin_testnet() -> BurnchainParameters {
1✔
109
        BurnchainParameters {
1✔
110
            chain_name: "bitcoin".to_string(),
1✔
111
            network_name: BITCOIN_TESTNET_NAME.to_string(),
1✔
112
            network_id: BITCOIN_NETWORK_ID_TESTNET,
1✔
113
            stable_confirmations: 7,
1✔
114
            consensus_hash_lifetime: 24,
1✔
115
            first_block_height: BITCOIN_TESTNET_FIRST_BLOCK_HEIGHT,
1✔
116
            first_block_hash: BurnchainHeaderHash::from_hex(BITCOIN_TESTNET_FIRST_BLOCK_HASH)
1✔
117
                .unwrap(),
1✔
118
            first_block_timestamp: BITCOIN_TESTNET_FIRST_BLOCK_TIMESTAMP,
1✔
119
            initial_reward_start_block: BITCOIN_TESTNET_FIRST_BLOCK_HEIGHT - 10_000,
1✔
120
        }
1✔
121
    }
1✔
122

123
    pub fn bitcoin_regtest() -> BurnchainParameters {
10,870,321✔
124
        BurnchainParameters {
10,870,321✔
125
            chain_name: "bitcoin".to_string(),
10,870,321✔
126
            network_name: BITCOIN_REGTEST_NAME.to_string(),
10,870,321✔
127
            network_id: BITCOIN_NETWORK_ID_REGTEST,
10,870,321✔
128
            stable_confirmations: 1,
10,870,321✔
129
            consensus_hash_lifetime: 24,
10,870,321✔
130
            first_block_height: BITCOIN_REGTEST_FIRST_BLOCK_HEIGHT,
10,870,321✔
131
            first_block_hash: BurnchainHeaderHash::from_hex(BITCOIN_REGTEST_FIRST_BLOCK_HASH)
10,870,321✔
132
                .unwrap(),
10,870,321✔
133
            first_block_timestamp: BITCOIN_REGTEST_FIRST_BLOCK_TIMESTAMP,
10,870,321✔
134
            initial_reward_start_block: BITCOIN_REGTEST_FIRST_BLOCK_HEIGHT,
10,870,321✔
135
        }
10,870,321✔
136
    }
10,870,321✔
137

138
    pub fn is_testnet(network_id: u32) -> bool {
×
139
        matches!(
×
140
            network_id,
×
141
            BITCOIN_NETWORK_ID_TESTNET | BITCOIN_NETWORK_ID_REGTEST
142
        )
143
    }
×
144
}
145

146
/// This is an opaque representation of the underlying burnchain-specific principal that signed
147
/// this transaction.  It may not even map to an address, given that even in "simple" VMs like
148
/// bitcoin script, the "signer" may be only a part of a complex script.
149
///
150
/// The purpose of this struct is to capture a loggable representation of a principal that signed
151
/// this transaction.  It's not meant for use with consensus-critical code.
152
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153
pub struct BurnchainSigner(pub String);
154

155
impl fmt::Display for BurnchainSigner {
156
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
431,329✔
157
        write!(f, "{}", &self.0)
431,329✔
158
    }
431,329✔
159
}
160

161
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
162
pub struct BurnchainRecipient {
163
    pub address: PoxAddress,
164
    pub amount: u64,
165
}
166

167
#[derive(Debug, PartialEq, Clone)]
168
pub enum BurnchainTransaction {
169
    Bitcoin(BitcoinTransaction),
170
    // TODO: fill in more types as we support them
171
}
172

173
impl BurnchainTransaction {
174
    pub fn txid(&self) -> Txid {
92,220✔
175
        match *self {
92,220✔
176
            BurnchainTransaction::Bitcoin(ref btc) => btc.txid.clone(),
92,220✔
177
        }
178
    }
92,220✔
179

180
    pub fn vtxindex(&self) -> u32 {
91,700✔
181
        match *self {
91,700✔
182
            BurnchainTransaction::Bitcoin(ref btc) => btc.vtxindex,
91,700✔
183
        }
184
    }
91,700✔
185

186
    pub fn opcode(&self) -> u8 {
184,436✔
187
        match *self {
184,436✔
188
            BurnchainTransaction::Bitcoin(ref btc) => btc.opcode,
184,436✔
189
        }
190
    }
184,436✔
191

192
    pub fn data(&self) -> Vec<u8> {
92,631✔
193
        match *self {
92,631✔
194
            BurnchainTransaction::Bitcoin(ref btc) => btc.data.clone(),
92,631✔
195
        }
196
    }
92,631✔
197

198
    pub fn num_signers(&self) -> usize {
92,235✔
199
        match *self {
92,235✔
200
            BurnchainTransaction::Bitcoin(ref btc) => btc.inputs.len(),
92,235✔
201
        }
202
    }
92,235✔
203

204
    pub fn get_input_tx_ref(&self, input: usize) -> Option<&(Txid, u32)> {
88,403✔
205
        match self {
88,403✔
206
            BurnchainTransaction::Bitcoin(ref btc) => {
88,403✔
207
                btc.inputs.get(input).map(|txin| txin.tx_ref())
88,403✔
208
            }
209
        }
210
    }
88,403✔
211

212
    /// Get the BurnchainRecipients we are able to decode.
213
    /// A `None` value at slot `i` means "there is a recipient at slot `i`, but we don't know how
214
    /// to decode it`.
215
    pub fn get_recipients(&self) -> Vec<Option<BurnchainRecipient>> {
89,046✔
216
        match *self {
89,046✔
217
            BurnchainTransaction::Bitcoin(ref btc) => btc
89,046✔
218
                .outputs
89,046✔
219
                .iter()
89,046✔
220
                .map(BurnchainRecipient::try_from_bitcoin_output)
89,046✔
221
                .collect(),
89,046✔
222
        }
223
    }
89,046✔
224

225
    pub fn num_recipients(&self) -> usize {
3,388✔
226
        match *self {
3,388✔
227
            BurnchainTransaction::Bitcoin(ref btc) => btc.outputs.len(),
3,388✔
228
        }
229
    }
3,388✔
230

231
    pub fn get_burn_amount(&self) -> u64 {
88,309✔
232
        match *self {
88,309✔
233
            BurnchainTransaction::Bitcoin(ref btc) => btc.data_amt,
88,309✔
234
        }
235
    }
88,309✔
236
}
237

238
#[derive(Debug, PartialEq, Clone)]
239
pub enum BurnchainBlock {
240
    Bitcoin(BitcoinBlock),
241
    // TODO: fill in some more types as we support them
242
}
243

244
#[derive(Debug, PartialEq, Clone)]
245
pub struct BurnchainBlockHeader {
246
    pub block_height: u64,
247
    pub block_hash: BurnchainHeaderHash,
248
    pub parent_block_hash: BurnchainHeaderHash,
249
    pub num_txs: u64,
250
    pub timestamp: u64,
251
}
252

253
#[derive(Debug, PartialEq, Clone)]
254
pub struct Burnchain {
255
    pub peer_version: u32,
256
    pub network_id: u32,
257
    pub chain_name: String,
258
    pub network_name: String,
259
    pub working_dir: String,
260
    pub consensus_hash_lifetime: u32,
261
    pub stable_confirmations: u32,
262
    pub first_block_height: u64,
263
    pub first_block_hash: BurnchainHeaderHash,
264
    pub first_block_timestamp: u32,
265
    pub pox_constants: PoxConstants,
266
    pub initial_reward_start_block: u64,
267
    pub marf_opts: Option<MARFOpenOpts>,
268
}
269

270
#[derive(Debug, PartialEq, Clone)]
271
pub struct PoxConstants {
272
    /// the length (in burn blocks) of the reward cycle
273
    pub reward_cycle_length: u32,
274
    /// the length (in burn blocks) of the prepare phase
275
    pub prepare_length: u32,
276
    /// the number of confirmations a PoX anchor block must
277
    ///  receive in order to become the anchor. must be at least > prepare_length/2
278
    pub anchor_threshold: u32,
279
    /// fraction of liquid STX that must vote to reject PoX for
280
    /// it to revert to PoB in the next reward cycle
281
    pub pox_rejection_fraction: u64,
282
    /// percentage of liquid STX that must participate for PoX
283
    ///  to occur
284
    pub pox_participation_threshold_pct: u64,
285
    /// last+1 block height of sunset phase
286
    pub sunset_end: u64,
287
    /// first block height of sunset phase
288
    pub sunset_start: u64,
289
    /// The auto unlock height for PoX v1 lockups before transition to PoX v2. This
290
    /// also defines the burn height at which PoX reward sets are calculated using
291
    /// PoX v2 rather than v1
292
    pub v1_unlock_height: u32,
293
    /// The auto unlock height for PoX v2 lockups during Epoch 2.2
294
    pub v2_unlock_height: u32,
295
    /// The auto unlock height for PoX v3 lockups during Epoch 2.5
296
    pub v3_unlock_height: u32,
297
    /// After this burn height, reward cycles use pox-3 for reward set data
298
    pub pox_3_activation_height: u32,
299
    /// After this burn height, reward cycles use pox-4 for reward set data
300
    pub pox_4_activation_height: u32,
301
    _shadow: PhantomData<()>,
302
}
303

304
impl PoxConstants {
305
    pub fn new(
8,827,208✔
306
        reward_cycle_length: u32,
8,827,208✔
307
        prepare_length: u32,
8,827,208✔
308
        anchor_threshold: u32,
8,827,208✔
309
        pox_rejection_fraction: u64,
8,827,208✔
310
        pox_participation_threshold_pct: u64,
8,827,208✔
311
        sunset_start: u64,
8,827,208✔
312
        sunset_end: u64,
8,827,208✔
313
        v1_unlock_height: u32,
8,827,208✔
314
        v2_unlock_height: u32,
8,827,208✔
315
        v3_unlock_height: u32,
8,827,208✔
316
        pox_3_activation_height: u32,
8,827,208✔
317
    ) -> PoxConstants {
8,827,208✔
318
        assert!(anchor_threshold > (prepare_length / 2));
8,827,208✔
319
        assert!(prepare_length < reward_cycle_length);
8,827,208✔
320
        assert!(sunset_start <= sunset_end);
8,827,208✔
321
        assert!(v2_unlock_height >= v1_unlock_height);
8,827,208✔
322
        assert!(v3_unlock_height >= v2_unlock_height);
8,827,208✔
323
        assert!(pox_3_activation_height >= v2_unlock_height);
8,827,208✔
324

325
        PoxConstants {
8,827,208✔
326
            reward_cycle_length,
8,827,208✔
327
            prepare_length,
8,827,208✔
328
            anchor_threshold,
8,827,208✔
329
            pox_rejection_fraction,
8,827,208✔
330
            pox_participation_threshold_pct,
8,827,208✔
331
            sunset_start,
8,827,208✔
332
            sunset_end,
8,827,208✔
333
            v1_unlock_height,
8,827,208✔
334
            v2_unlock_height,
8,827,208✔
335
            v3_unlock_height,
8,827,208✔
336
            pox_3_activation_height,
8,827,208✔
337
            pox_4_activation_height: v3_unlock_height,
8,827,208✔
338
            _shadow: PhantomData,
8,827,208✔
339
        }
8,827,208✔
340
    }
8,827,208✔
341
    #[cfg(test)]
342
    pub fn test_default() -> PoxConstants {
8,669✔
343
        // 20 reward slots; 10 prepare-phase slots
344
        PoxConstants::new(
8,669✔
345
            10,
346
            5,
347
            3,
348
            25,
349
            5,
350
            5000,
351
            10000,
352
            u32::MAX,
353
            u32::MAX,
354
            u32::MAX,
355
            u32::MAX,
356
        )
357
    }
8,669✔
358

359
    #[cfg(test)]
360
    /// Create a PoX constants used in tests with 5-block cycles,
361
    ///  3-block prepare phases, a threshold of 3, rejection fraction of 25%,
362
    ///  a participation threshold of 5% and no sunset or transition to pox-2 or beyond.
363
    pub(crate) fn test_20_no_sunset() -> PoxConstants {
1,702✔
364
        PoxConstants::new(
1,702✔
365
            5,
366
            3,
367
            3,
368
            25,
369
            5,
370
            u64::MAX,
371
            u64::MAX,
372
            u32::MAX,
373
            u32::MAX,
374
            u32::MAX,
375
            u32::MAX,
376
        )
377
    }
1,702✔
378

379
    /// Returns the PoX contract that is "active" at the given burn block height
380
    pub fn static_active_pox_contract(
2,492,193✔
381
        v1_unlock_height: u64,
2,492,193✔
382
        pox_3_activation_height: u64,
2,492,193✔
383
        pox_4_activation_height: u64,
2,492,193✔
384
        burn_height: u64,
2,492,193✔
385
    ) -> &'static str {
2,492,193✔
386
        if burn_height > pox_4_activation_height {
2,492,193✔
387
            POX_4_NAME
603,342✔
388
        } else if burn_height > pox_3_activation_height {
1,888,851✔
389
            POX_3_NAME
1,745,061✔
390
        } else if burn_height > v1_unlock_height {
143,790✔
391
            POX_2_NAME
15,814✔
392
        } else {
393
            POX_1_NAME
127,976✔
394
        }
395
    }
2,492,193✔
396

397
    /// Returns the PoX contract that is "active" at the given burn block height
398
    pub fn active_pox_contract(&self, burn_height: u64) -> &'static str {
2,492,193✔
399
        Self::static_active_pox_contract(
2,492,193✔
400
            u64::from(self.v1_unlock_height),
2,492,193✔
401
            u64::from(self.pox_3_activation_height),
2,492,193✔
402
            u64::from(self.pox_4_activation_height),
2,492,193✔
403
            burn_height,
2,492,193✔
404
        )
405
    }
2,492,193✔
406

407
    pub fn reward_slots(&self) -> u32 {
43,759✔
408
        (self.reward_cycle_length - self.prepare_length)
43,759✔
409
            * u32::try_from(OUTPUTS_PER_COMMIT).expect("FATAL: > 2^32 outputs per commit")
43,759✔
410
    }
43,759✔
411

412
    /// is participating_ustx enough to engage in PoX in the next reward cycle?
413
    pub fn enough_participation(&self, participating_ustx: u128, liquid_ustx: u128) -> bool {
20,412✔
414
        participating_ustx
20,412✔
415
            .checked_mul(100)
20,412✔
416
            .expect("OVERFLOW: uSTX overflowed u128")
20,412✔
417
            > liquid_ustx
20,412✔
418
                .checked_mul(u128::from(self.pox_participation_threshold_pct))
20,412✔
419
                .expect("OVERFLOW: uSTX overflowed u128")
20,412✔
420
    }
20,412✔
421

422
    pub fn mainnet_default() -> PoxConstants {
4,212✔
423
        PoxConstants::new(
4,212✔
424
            POX_REWARD_CYCLE_LENGTH,
425
            POX_PREPARE_WINDOW_LENGTH,
426
            80,
427
            25,
428
            5,
429
            BITCOIN_MAINNET_FIRST_BLOCK_HEIGHT + POX_SUNSET_START,
4,212✔
430
            BITCOIN_MAINNET_FIRST_BLOCK_HEIGHT + POX_SUNSET_END,
4,212✔
431
            POX_V1_MAINNET_EARLY_UNLOCK_HEIGHT,
432
            POX_V2_MAINNET_EARLY_UNLOCK_HEIGHT,
433
            POX_V3_MAINNET_EARLY_UNLOCK_HEIGHT,
434
            BITCOIN_MAINNET_STACKS_24_BURN_HEIGHT
435
                .try_into()
4,212✔
436
                .expect("Epoch transition height must be <= u32::MAX"),
4,212✔
437
        )
438
    }
4,212✔
439

440
    // NOTE: this is the *old* pre-Nakamoto testnet
441
    pub fn testnet_default() -> PoxConstants {
581✔
442
        PoxConstants::new(
581✔
443
            POX_REWARD_CYCLE_LENGTH / 2,   // 1050
581✔
444
            POX_PREPARE_WINDOW_LENGTH / 2, // 50
581✔
445
            40,
446
            12,
447
            2,
448
            BITCOIN_TESTNET_FIRST_BLOCK_HEIGHT + POX_SUNSET_START,
581✔
449
            BITCOIN_TESTNET_FIRST_BLOCK_HEIGHT + POX_SUNSET_END,
581✔
450
            POX_V1_TESTNET_EARLY_UNLOCK_HEIGHT,
451
            POX_V2_TESTNET_EARLY_UNLOCK_HEIGHT,
452
            POX_V3_TESTNET_EARLY_UNLOCK_HEIGHT,
453
            BITCOIN_TESTNET_STACKS_24_BURN_HEIGHT
454
                .try_into()
581✔
455
                .expect("Epoch transition height must be <= u32::MAX"),
581✔
456
        ) // total liquid supply is 40000000000000000 µSTX
457
    }
581✔
458

459
    pub fn nakamoto_testnet_default() -> PoxConstants {
×
460
        PoxConstants::new(900, 100, 51, 100, 0, u64::MAX, u64::MAX, 242, 243, 246, 244)
×
461
    }
×
462

463
    // TODO: add tests from mutation testing results #4838
464
    #[cfg_attr(test, mutants::skip)]
465
    pub fn regtest_default() -> PoxConstants {
8,809,910✔
466
        PoxConstants::new(
8,809,910✔
467
            5,
468
            3,
469
            2,
470
            3333333333333333,
471
            1,
472
            BITCOIN_REGTEST_FIRST_BLOCK_HEIGHT + POX_SUNSET_START,
8,809,910✔
473
            BITCOIN_REGTEST_FIRST_BLOCK_HEIGHT + POX_SUNSET_END,
8,809,910✔
474
            1_000_000,
475
            2_000_000,
476
            4_000_000,
477
            3_000_000,
478
        )
479
    }
8,809,910✔
480

481
    /// Return true if PoX should sunset at all
482
    /// return false if not.
483
    pub fn has_pox_sunset(epoch_id: StacksEpochId) -> bool {
5,081,001✔
484
        epoch_id < StacksEpochId::Epoch21
5,081,001✔
485
    }
5,081,001✔
486

487
    /// Returns true if PoX has been fully disabled by the PoX sunset.
488
    /// Behavior is epoch-specific
489
    pub fn is_after_pox_sunset_end(&self, burn_block_height: u64, epoch_id: StacksEpochId) -> bool {
1,835,103✔
490
        if !Self::has_pox_sunset(epoch_id) {
1,835,103✔
491
            false
1,274,265✔
492
        } else {
493
            burn_block_height >= self.sunset_end
560,838✔
494
        }
495
    }
1,835,103✔
496

497
    /// Returns true if the burn height falls into the PoX sunset period.
498
    /// Returns false if not, or if the sunset isn't active in this epoch
499
    /// (Note that this is true if burn_block_height is beyond the sunset height)
500
    pub fn is_after_pox_sunset_start(
103,130✔
501
        &self,
103,130✔
502
        burn_block_height: u64,
103,130✔
503
        epoch_id: StacksEpochId,
103,130✔
504
    ) -> bool {
103,130✔
505
        if !Self::has_pox_sunset(epoch_id) {
103,130✔
506
            false
×
507
        } else {
508
            self.sunset_start <= burn_block_height
103,130✔
509
        }
510
    }
103,130✔
511

512
    /// The first block of the prepare phase during `reward_cycle`. This is the prepare phase _for the next cycle_.
513
    pub fn prepare_phase_start(&self, first_block_height: u64, reward_cycle: u64) -> u64 {
781,287✔
514
        let reward_cycle_start =
781,287✔
515
            self.reward_cycle_to_block_height(first_block_height, reward_cycle);
781,287✔
516
        let prepare_phase_start = reward_cycle_start + u64::from(self.reward_cycle_length)
781,287✔
517
            - u64::from(self.prepare_length);
781,287✔
518
        prepare_phase_start
781,287✔
519
    }
781,287✔
520

521
    /// Is this the first block to receive rewards in its cycle?
522
    /// This is the mod 1 block. Note: in nakamoto, the signer set for cycle N signs
523
    ///  the mod 0 block.
524
    pub fn is_reward_cycle_start(&self, first_block_height: u64, burn_height: u64) -> bool {
2,289,524✔
525
        let effective_height = burn_height - first_block_height;
2,289,524✔
526
        // first block of the new reward cycle
527
        (effective_height % u64::from(self.reward_cycle_length)) == 1
2,289,524✔
528
    }
2,289,524✔
529

530
    /// Is this the first block to be signed by the signer set in cycle N?
531
    /// This is the mod 0 block.
532
    pub fn is_naka_signing_cycle_start(&self, first_block_height: u64, burn_height: u64) -> bool {
162✔
533
        let effective_height = burn_height - first_block_height;
162✔
534
        // first block of the new reward cycle
535
        (effective_height % u64::from(self.reward_cycle_length)) == 0
162✔
536
    }
162✔
537

538
    /// return the first burn block which receives reward in `reward_cycle`.
539
    /// this is the modulo 1 block
540
    pub fn reward_cycle_to_block_height(&self, first_block_height: u64, reward_cycle: u64) -> u64 {
11,146,232✔
541
        // NOTE: the `+ 1` is because the height of the first block of a reward cycle is mod 1, not
542
        // mod 0.
543
        first_block_height + reward_cycle * u64::from(self.reward_cycle_length) + 1
11,146,232✔
544
    }
11,146,232✔
545

546
    /// the first burn block that must be *signed* by the signer set of `reward_cycle`.
547
    /// this is the modulo 0 block
548
    pub fn nakamoto_first_block_of_cycle(&self, first_block_height: u64, reward_cycle: u64) -> u64 {
3,124,090✔
549
        first_block_height + reward_cycle * u64::from(self.reward_cycle_length)
3,124,090✔
550
    }
3,124,090✔
551

552
    pub fn reward_cycle_index(&self, first_block_height: u64, burn_height: u64) -> Option<u64> {
18✔
553
        let effective_height = burn_height.checked_sub(first_block_height)?;
18✔
554
        Some(effective_height % u64::from(self.reward_cycle_length))
18✔
555
    }
18✔
556

557
    pub fn block_height_to_reward_cycle(
47,994,705✔
558
        &self,
47,994,705✔
559
        first_block_height: u64,
47,994,705✔
560
        block_height: u64,
47,994,705✔
561
    ) -> Option<u64> {
47,994,705✔
562
        Self::static_block_height_to_reward_cycle(
47,994,705✔
563
            block_height,
47,994,705✔
564
            first_block_height,
47,994,705✔
565
            u64::from(self.reward_cycle_length),
47,994,705✔
566
        )
567
    }
47,994,705✔
568

569
    /// Return the reward cycle that the current prepare phase corresponds to if `block_height` is _in_ a prepare
570
    /// phase. If it is not in a prepare phase, return None.
571
    pub fn reward_cycle_of_prepare_phase(
91,661✔
572
        &self,
91,661✔
573
        first_block_height: u64,
91,661✔
574
        block_height: u64,
91,661✔
575
    ) -> Option<u64> {
91,661✔
576
        if !self.is_in_prepare_phase(first_block_height, block_height) {
91,661✔
577
            return None;
2,690✔
578
        }
88,971✔
579
        // the None branches here should be unreachable, because if `first_block_height > block_height`,
580
        //   `is_in_prepare_phase` would have returned false, but no need to be unsafe anyways.
581
        let effective_height = block_height.checked_sub(first_block_height)?;
88,971✔
582
        let current_cycle = self.block_height_to_reward_cycle(first_block_height, block_height)?;
88,971✔
583
        if effective_height % u64::from(self.reward_cycle_length) == 0 {
88,971✔
584
            // if this is the "mod 0" block of a prepare phase, its corresponding reward cycle is the current one
585
            Some(current_cycle)
14,807✔
586
        } else {
587
            // otherwise, the corresponding reward cycle is actually the _next_ reward cycle
588
            Some(current_cycle + 1)
74,164✔
589
        }
590
    }
91,661✔
591

592
    pub fn is_in_prepare_phase(&self, first_block_height: u64, block_height: u64) -> bool {
12,128,833✔
593
        Self::static_is_in_prepare_phase(
12,128,833✔
594
            first_block_height,
12,128,833✔
595
            u64::from(self.reward_cycle_length),
12,128,833✔
596
            u64::from(self.prepare_length),
12,128,833✔
597
            block_height,
12,128,833✔
598
        )
599
    }
12,128,833✔
600

601
    pub fn static_is_in_prepare_phase(
12,866,588✔
602
        first_block_height: u64,
12,866,588✔
603
        reward_cycle_length: u64,
12,866,588✔
604
        prepare_length: u64,
12,866,588✔
605
        block_height: u64,
12,866,588✔
606
    ) -> bool {
12,866,588✔
607
        if block_height <= first_block_height {
12,866,588✔
608
            // not a reward cycle start if we're the first block after genesis.
609
            false
18,646✔
610
        } else {
611
            let effective_height = block_height - first_block_height;
12,847,942✔
612
            let reward_index = effective_height % reward_cycle_length;
12,847,942✔
613

614
            // NOTE: first block in reward cycle is mod 1, so mod 0 is the last block in the
615
            // prepare phase.
616
            // TODO: I *think* the logic of `== 0` here requires some further digging.
617
            //  `mod 0` may not have any rewards, but it does not behave like "prepare phase" blocks:
618
            //  is it already a member of reward cycle "N" where N = block_height / reward_cycle_len
619
            reward_index == 0 || reward_index > reward_cycle_length - prepare_length
12,847,942✔
620
        }
621
    }
12,866,588✔
622

623
    /// The prepare phase is the last prepare_phase_length blocks of the cycle
624
    /// This cannot include the 0 block for nakamoto
625
    pub fn is_in_naka_prepare_phase(&self, first_block_height: u64, block_height: u64) -> bool {
52,922✔
626
        Self::static_is_in_naka_prepare_phase(
52,922✔
627
            first_block_height,
52,922✔
628
            u64::from(self.reward_cycle_length),
52,922✔
629
            u64::from(self.prepare_length),
52,922✔
630
            block_height,
52,922✔
631
        )
632
    }
52,922✔
633

634
    /// The prepare phase is the last prepare_phase_length blocks of the cycle
635
    /// This cannot include the 0 block for nakamoto
636
    pub fn static_is_in_naka_prepare_phase(
52,922✔
637
        first_block_height: u64,
52,922✔
638
        reward_cycle_length: u64,
52,922✔
639
        prepare_length: u64,
52,922✔
640
        block_height: u64,
52,922✔
641
    ) -> bool {
52,922✔
642
        if block_height <= first_block_height {
52,922✔
643
            // not a reward cycle start if we're the first block after genesis.
644
            false
×
645
        } else {
646
            let effective_height = block_height - first_block_height;
52,922✔
647
            let reward_index = effective_height % reward_cycle_length;
52,922✔
648
            reward_index > reward_cycle_length - prepare_length
52,922✔
649
        }
650
    }
52,922✔
651

652
    /// Returns the active reward cycle at the given burn block height
653
    /// * `first_block_ht` - the first burn block height that the Stacks network monitored
654
    /// * `reward_cycle_len` - the length of each reward cycle in the network.
655
    pub fn static_block_height_to_reward_cycle(
48,415,953✔
656
        block_ht: u64,
48,415,953✔
657
        first_block_ht: u64,
48,415,953✔
658
        reward_cycle_len: u64,
48,415,953✔
659
    ) -> Option<u64> {
48,415,953✔
660
        if block_ht < first_block_ht {
48,415,953✔
661
            return None;
×
662
        }
48,415,953✔
663
        Some((block_ht - first_block_ht) / (reward_cycle_len))
48,415,953✔
664
    }
48,415,953✔
665
}
666

667
/// Structure for encoding our view of the network
668
#[derive(Debug, PartialEq, Clone)]
669
pub struct BurnchainView {
670
    /// last-seen block height (at chain tip)
671
    pub burn_block_height: u64,
672
    /// last-seen burn block hash
673
    pub burn_block_hash: BurnchainHeaderHash,
674
    /// latest stable block height (e.g. chain tip minus 7)
675
    pub burn_stable_block_height: u64,
676
    /// latest stable burn block hash
677
    pub burn_stable_block_hash: BurnchainHeaderHash,
678
    /// map all block heights from burn_block_height back to the oldest one we'll take for considering the peer a neighbor
679
    pub last_burn_block_hashes: HashMap<u64, BurnchainHeaderHash>,
680
    /// consensus hash of the current reward cycle's start block
681
    pub rc_consensus_hash: ConsensusHash,
682
}
683

684
/// The burnchain block's encoded state transition:
685
/// -- the new burn distribution
686
/// -- the sequence of valid blockstack operations that went into it
687
/// -- the set of previously-accepted leader VRF keys consumed
688
#[derive(Debug, Clone)]
689
pub struct BurnchainStateTransition {
690
    pub burn_dist: Vec<BurnSamplePoint>,
691
    pub accepted_ops: Vec<BlockstackOperationType>,
692
    pub consumed_leader_keys: Vec<LeaderKeyRegisterOp>,
693
    pub windowed_block_commits: Vec<Vec<LeaderBlockCommitOp>>,
694
    pub windowed_missed_commits: Vec<Vec<MissedBlockCommit>>,
695
}
696

697
/// The burnchain block's state transition's ops:
698
/// -- the new burn distribution
699
/// -- the sequence of valid blockstack operations that went into it
700
/// -- the set of previously-accepted leader VRF keys consumed
701
#[derive(Debug, Clone)]
702
pub struct BurnchainStateTransitionOps {
703
    pub accepted_ops: Vec<BlockstackOperationType>,
704
    pub consumed_leader_keys: Vec<LeaderKeyRegisterOp>,
705
}
706

707
#[derive(Debug)]
708
pub enum Error {
709
    /// Unsupported burn chain
710
    UnsupportedBurnchain,
711
    /// Bitcoin-related error
712
    Bitcoin(btc_error),
713
    /// burn database error
714
    DBError(db_error),
715
    /// Download error
716
    DownloadError(btc_error),
717
    /// Parse error
718
    ParseError,
719
    /// Thread channel error
720
    ThreadChannelError,
721
    /// Missing headers
722
    MissingHeaders,
723
    /// Missing parent block
724
    MissingParentBlock,
725
    /// Remote burnchain peer has misbehaved
726
    BurnchainPeerBroken,
727
    /// filesystem error
728
    FSError(io::Error),
729
    /// Operation processing error
730
    OpError(op_error),
731
    /// Try again error
732
    TrySyncAgain,
733
    UnknownBlock(BurnchainHeaderHash),
734
    NonCanonicalPoxId(PoxId, PoxId),
735
    CoordinatorClosed,
736
    /// Graceful shutdown error
737
    ShutdownInitiated,
738
    /// No epoch defined at that height
739
    NoStacksEpoch,
740
    /// Node error processing the operation
741
    ProcessorError,
742
}
743

744
impl fmt::Display for Error {
745
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
993✔
746
        match self {
993✔
747
            Error::UnsupportedBurnchain => write!(f, "Unsupported burnchain"),
9✔
748
            Error::Bitcoin(ref btce) => fmt::Display::fmt(btce, f),
498✔
749
            Error::DBError(ref dbe) => fmt::Display::fmt(dbe, f),
×
750
            Error::DownloadError(ref btce) => fmt::Display::fmt(btce, f),
×
751
            Error::ParseError => write!(f, "Parse error"),
×
752
            Error::MissingHeaders => write!(f, "Missing block headers"),
×
753
            Error::MissingParentBlock => write!(f, "Missing parent block"),
×
754
            Error::ThreadChannelError => write!(f, "Error in thread channel"),
×
755
            Error::BurnchainPeerBroken => write!(f, "Remote burnchain peer has misbehaved"),
×
756
            Error::FSError(ref e) => fmt::Display::fmt(e, f),
×
757
            Error::OpError(ref e) => fmt::Display::fmt(e, f),
×
758
            Error::TrySyncAgain => write!(f, "Try synchronizing again"),
477✔
759
            Error::UnknownBlock(block) => write!(f, "Unknown burnchain block {}", block),
×
760
            Error::NonCanonicalPoxId(parent, child) => write!(
×
761
                f,
×
762
                "{} is not a descendant of the canonical parent PoXId: {}",
763
                parent, child
764
            ),
765
            Error::CoordinatorClosed => write!(f, "ChainsCoordinator channel hung up"),
9✔
766
            Error::ShutdownInitiated => write!(f, "Graceful shutdown was initiated"),
×
767
            Error::NoStacksEpoch => write!(
×
768
                f,
×
769
                "No Stacks epoch is defined at the height being evaluated"
770
            ),
771
            Error::ProcessorError => write!(f, "Failure processing burn state"),
×
772
        }
773
    }
993✔
774
}
775

776
impl error::Error for Error {
777
    fn cause(&self) -> Option<&dyn error::Error> {
×
778
        match *self {
×
779
            Error::UnsupportedBurnchain => None,
×
780
            Error::Bitcoin(ref e) => Some(e),
×
781
            Error::DBError(ref e) => Some(e),
×
782
            Error::DownloadError(ref e) => Some(e),
×
783
            Error::ParseError => None,
×
784
            Error::MissingHeaders => None,
×
785
            Error::MissingParentBlock => None,
×
786
            Error::ThreadChannelError => None,
×
787
            Error::BurnchainPeerBroken => None,
×
788
            Error::FSError(ref e) => Some(e),
×
789
            Error::OpError(ref e) => Some(e),
×
790
            Error::TrySyncAgain => None,
×
791
            Error::UnknownBlock(_) => None,
×
792
            Error::NonCanonicalPoxId(_, _) => None,
×
793
            Error::CoordinatorClosed => None,
×
794
            Error::ShutdownInitiated => None,
×
795
            Error::NoStacksEpoch => None,
×
796
            Error::ProcessorError => None,
×
797
        }
798
    }
×
799
}
800

801
impl From<db_error> for Error {
802
    fn from(e: db_error) -> Error {
×
803
        Error::DBError(e)
×
804
    }
×
805
}
806

807
impl From<sqlite_error> for Error {
808
    fn from(e: sqlite_error) -> Error {
×
809
        Error::DBError(db_error::SqliteError(e))
×
810
    }
×
811
}
812

813
impl From<btc_error> for Error {
814
    fn from(e: btc_error) -> Error {
×
815
        Error::Bitcoin(e)
×
816
    }
×
817
}
818

819
impl BurnchainView {
820
    #[cfg(test)]
821
    pub fn make_test_data(&mut self) {
26✔
822
        let oldest_height = if self.burn_stable_block_height < MAX_NEIGHBOR_BLOCK_DELAY {
26✔
823
            0
×
824
        } else {
825
            self.burn_stable_block_height - MAX_NEIGHBOR_BLOCK_DELAY
26✔
826
        };
827

828
        let mut ret = HashMap::new();
26✔
829
        for i in oldest_height..self.burn_block_height + 1 {
856✔
830
            if i == self.burn_stable_block_height {
856✔
831
                ret.insert(i, self.burn_stable_block_hash.clone());
26✔
832
            } else if i == self.burn_block_height {
830✔
833
                ret.insert(i, self.burn_block_hash.clone());
26✔
834
            } else {
26✔
835
                let data = {
804✔
836
                    use sha2::{Digest, Sha256};
837
                    let mut hasher = Sha256::new();
804✔
838
                    hasher.update(&i.to_le_bytes());
804✔
839
                    hasher.finalize()
804✔
840
                };
841
                let mut data_32 = [0x00; 32];
804✔
842
                data_32.copy_from_slice(&data[0..32]);
804✔
843
                ret.insert(i, BurnchainHeaderHash(data_32));
804✔
844
            }
845
        }
846
        self.last_burn_block_hashes = ret;
26✔
847
    }
26✔
848
}
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