• 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

80.82
/stacks-node/src/nakamoto_node/miner.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
use std::collections::HashSet;
17
use std::sync::atomic::{AtomicBool, Ordering};
18
use std::sync::Arc;
19
#[cfg(test)]
20
use std::sync::LazyLock;
21
use std::thread;
22
use std::time::{Duration, Instant};
23

24
use clarity::boot_util::boot_code_id;
25
use clarity::vm::costs::ExecutionCost;
26
use clarity::vm::types::PrincipalData;
27
use libsigner::v0::messages::{MinerSlotID, SignerMessage};
28
use libsigner::StackerDBSession;
29
use rand::{thread_rng, Rng};
30
use stacks::burnchains::{Burnchain, Txid};
31
use stacks::chainstate::burn::db::sortdb::SortitionDB;
32
use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash};
33
use stacks::chainstate::coordinator::OnChainRewardSetProvider;
34
use stacks::chainstate::nakamoto::coordinator::load_nakamoto_reward_set;
35
use stacks::chainstate::nakamoto::miner::{NakamotoBlockBuilder, NakamotoTenureInfo};
36
use stacks::chainstate::nakamoto::staging_blocks::NakamotoBlockObtainMethod;
37
use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState};
38
use stacks::chainstate::stacks::boot::{RewardSet, MINERS_NAME};
39
use stacks::chainstate::stacks::db::{StacksChainState, StacksHeaderInfo};
40
use stacks::chainstate::stacks::{
41
    CoinbasePayload, Error as ChainstateError, StacksTransaction, StacksTransactionSigner,
42
    TenureChangeCause, TenureChangePayload, TransactionAnchorMode, TransactionPayload,
43
    TransactionVersion,
44
};
45
use stacks::core::mempool::MemPoolWalkStrategy;
46
use stacks::net::api::poststackerdbchunk::StackerDBErrorCodes;
47
use stacks::net::p2p::NetworkHandle;
48
use stacks::net::stackerdb::StackerDBs;
49
use stacks::net::{NakamotoBlocksData, StacksMessageType};
50
use stacks::types::chainstate::BlockHeaderHash;
51
use stacks::types::{MinerDiagnosticData, MiningReason};
52
use stacks::util::get_epoch_time_secs;
53
use stacks::util::secp256k1::MessageSignature;
54
#[cfg(test)]
55
use stacks::util::secp256k1::Secp256k1PublicKey;
56
use stacks_common::types::chainstate::{StacksAddress, StacksBlockId};
57
#[cfg(test)]
58
use stacks_common::types::PublicKey;
59
use stacks_common::types::{PrivateKey, StacksEpochId};
60
#[cfg(test)]
61
use stacks_common::util::tests::TestFlag;
62
use stacks_common::util::vrf::VRFProof;
63
#[cfg(test)]
64
use tempfile::tempdir;
65

66
use super::miner_db::MinerDB;
67
use super::relayer::{MinerStopHandle, RelayerThread};
68
use super::{Config, Error as NakamotoNodeError, EventDispatcher, Keychain};
69
use crate::nakamoto_node::signer_coordinator::SignerCoordinator;
70
use crate::nakamoto_node::VRF_MOCK_MINER_KEY;
71
use crate::neon_node;
72
use crate::run_loop::nakamoto::Globals;
73
use crate::run_loop::RegisteredKey;
74

75
#[cfg(test)]
76
#[derive(Default, Clone, PartialEq)]
77
enum TestMineStall {
78
    #[default]
79
    NotStalled,
80
    Pending,
81
    Stalled,
82
}
83

84
#[cfg(test)]
85
/// Test flag to stall the miner thread
86
static TEST_MINE_STALL: LazyLock<TestFlag<TestMineStall>> = LazyLock::new(TestFlag::default);
87
#[cfg(test)]
88
/// Test flag to stall block proposal broadcasting for the specified miner keys
89
pub static TEST_BROADCAST_PROPOSAL_STALL: LazyLock<TestFlag<Vec<Secp256k1PublicKey>>> =
90
    LazyLock::new(TestFlag::default);
91
#[cfg(test)]
92
/// Test flag to make miner skip mining a block
93
pub static TEST_MINE_SKIP: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
94
#[cfg(test)]
95
// Test flag to stall the miner from announcing a block while this flag is true
96
pub static TEST_BLOCK_ANNOUNCE_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
97
#[cfg(test)]
98
// Test flag to skip broadcasting blocks over the p2p network
99
pub static TEST_P2P_BROADCAST_SKIP: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
100
#[cfg(test)]
101
// Test flag to stall broadcasting blocks over the p2p network
102
pub static TEST_P2P_BROADCAST_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
103
#[cfg(test)]
104
// Test flag to skip pushing blocks to the signers
105
pub static TEST_BLOCK_PUSH_SKIP: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
106
#[cfg(test)]
107
// Test flag to indicate the block that the miner most recently tried to broadcast
108
pub static TEST_MINER_BROADCASTING_BLOCK: LazyLock<TestFlag<NakamotoBlock>> =
109
    LazyLock::new(TestFlag::default);
110

111
#[cfg(test)]
112
/// Set the `TEST_MINE_STALL` flag to `Pending` and block until the miner is stalled.
113
pub fn fault_injection_stall_miner() {
81✔
114
    if TEST_MINE_STALL.get() == TestMineStall::NotStalled {
81✔
115
        TEST_MINE_STALL.set(TestMineStall::Pending);
74✔
116
    }
74✔
117
    while TEST_MINE_STALL.get() != TestMineStall::Stalled {
14,567✔
118
        std::thread::sleep(std::time::Duration::from_millis(10));
14,486✔
119
    }
14,486✔
120
}
81✔
121

122
#[cfg(test)]
123
/// Set the `TEST_MINE_STALL` flag to `Pending` and do not block.
124
pub fn fault_injection_try_stall_miner() {
3✔
125
    let mut stall_lock = TEST_MINE_STALL.0.lock().unwrap();
3✔
126
    if stall_lock.as_ref().is_none() || stall_lock.as_ref() == Some(&TestMineStall::NotStalled) {
3✔
127
        *stall_lock = Some(TestMineStall::Pending);
3✔
128
    }
3✔
129
}
3✔
130

131
#[cfg(test)]
132
/// Unstall the miner by setting the `TEST_MINE_STALL` flag to `NotStalled`.
133
pub fn fault_injection_unstall_miner() {
75✔
134
    TEST_MINE_STALL.set(TestMineStall::NotStalled);
75✔
135
}
75✔
136

137
/// If the miner was interrupted while mining a block, how long should the
138
///  miner thread sleep before trying again?
139
const ABORT_TRY_AGAIN_MS: u64 = 200;
140

141
#[allow(clippy::large_enum_variant)]
142
#[derive(Debug)]
143
pub enum MinerDirective {
144
    /// The miner won sortition so they should begin a new tenure
145
    BeginTenure {
146
        /// This is the block ID of the first block in the parent tenure
147
        parent_tenure_start: StacksBlockId,
148
        /// This is the snapshot that this miner won, and will produce a tenure for
149
        election_block: BlockSnapshot,
150
        /// This is the snapshot that caused the relayer to initiate this event (may be different
151
        ///  than the election block in the case where the miner is trying to mine a late block).
152
        burnchain_tip: BlockSnapshot,
153
        /// This is `true` if the snapshot above is known not to be the the latest burnchain tip,
154
        /// but an ancestor of it (for example, the burnchain tip could be an empty flash block, but the
155
        /// miner may nevertheless need to produce a Stacks block with a BlockFound tenure-change
156
        /// transaction for the tenure began by winning `burnchain_tip`'s sortition).
157
        late: bool,
158
    },
159
    /// The miner should try to continue their tenure if they are the active miner
160
    ContinueTenure { new_burn_view: ConsensusHash },
161
    /// The miner did not win sortition
162
    StopTenure,
163
}
164

165
#[derive(PartialEq, Debug, Clone)]
166
/// Tenure info needed to construct a tenure change or tenure extend transaction
167
struct ParentTenureInfo {
168
    /// The number of blocks in the parent tenure
169
    parent_tenure_blocks: u64,
170
    /// The consensus hash of the parent tenure
171
    parent_tenure_consensus_hash: ConsensusHash,
172
}
173

174
/// Metadata required for beginning a new tenure
175
struct ParentStacksBlockInfo {
176
    /// Header metadata for the Stacks block we're going to build on top of
177
    stacks_parent_header: StacksHeaderInfo,
178
    /// nonce to use for this new block's coinbase transaction
179
    coinbase_nonce: u64,
180
    parent_tenure: Option<ParentTenureInfo>,
181
}
182

183
/// The reason the miner thread was spawned
184
#[derive(PartialEq, Clone, Debug)]
185
pub enum MinerReason {
186
    /// The miner thread was spawned to begin a new tenure
187
    BlockFound {
188
        /// `late` indicates whether or not the tenure that is about to be started corresponds to
189
        /// an ancestor of the canonical tip.  This can happen if this miner won the highest
190
        /// sortition, but that sortition's snapshot is not the canonical tip (e.g. the canonical
191
        /// tip may have no sortition, but its parent (or Nth ancestor) would have had a sortition
192
        /// that this miner won, and it would be the latest non-empty sortition ancestor of the
193
        /// tip).  This indication is important because the miner would issue a BlockFound
194
        /// tenure-change, and then issue an Extended tenure-change right afterwards in order to
195
        /// update the burnchain view exposed to Clarity for the highest sortition.
196
        late: bool,
197
    },
198
    /// The miner thread was spawned to extend an existing tenure
199
    Extended {
200
        /// Current consensus hash on the underlying burnchain.  Corresponds to the last-seen
201
        /// sortition.
202
        burn_view_consensus_hash: ConsensusHash,
203
    },
204
    /// Issue a read-count only extension
205
    ReadCountExtend {
206
        /// Current consensus hash on the underlying burnchain.  Corresponds to the last-seen
207
        /// sortition.
208
        burn_view_consensus_hash: ConsensusHash,
209
    },
210
}
211

212
impl MinerReason {
213
    pub fn is_late_block(&self) -> bool {
13,575✔
214
        match self {
13,575✔
215
            Self::BlockFound { ref late } => *late,
11,941✔
216
            Self::Extended { .. } => false,
1,626✔
217
            Self::ReadCountExtend { .. } => false,
8✔
218
        }
219
    }
13,575✔
220
}
221

222
impl std::fmt::Display for MinerReason {
223
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
224
        match self {
×
225
            MinerReason::BlockFound { late } => {
×
226
                write!(f, "BlockFound({})", if *late { "late" } else { "current" })
×
227
            }
228
            MinerReason::Extended {
229
                burn_view_consensus_hash,
×
230
            } => write!(
×
231
                f,
×
232
                "Extended: burn_view_consensus_hash = {burn_view_consensus_hash:?}",
233
            ),
234
            MinerReason::ReadCountExtend {
235
                burn_view_consensus_hash,
×
236
            } => write!(
×
237
                f,
×
238
                "Read-Count Extend: burn_view_consensus_hash = {burn_view_consensus_hash:?}",
239
            ),
240
        }
241
    }
×
242
}
243

244
impl Into<MiningReason> for MinerReason {
245
    fn into(self) -> MiningReason {
2,432✔
246
        match self {
2,432✔
247
            Self::BlockFound { .. } => MiningReason::BlockFound,
2,220✔
248
            Self::Extended { .. } => MiningReason::Extended,
207✔
249
            Self::ReadCountExtend { .. } => MiningReason::ReadCountExtend,
5✔
250
        }
251
    }
2,432✔
252
}
253

254
pub struct BlockMinerThread {
255
    /// node config struct
256
    config: Config,
257
    /// handle to global state
258
    globals: Globals,
259
    /// copy of the node's keychain
260
    keychain: Keychain,
261
    /// burnchain configuration
262
    burnchain: Burnchain,
263
    /// Consensus hash and header hash of the last block mined
264
    last_block_mined: Option<(ConsensusHash, BlockHeaderHash)>,
265
    /// Number of blocks mined since a tenure change/extend was attempted
266
    mined_blocks: u64,
267
    /// Cost consumed by the current tenure
268
    tenure_cost: ExecutionCost,
269
    /// Cost budget for the current tenure
270
    tenure_budget: ExecutionCost,
271
    /// Copy of the node's registered VRF key
272
    registered_key: RegisteredKey,
273
    /// Burnchain block snapshot which elected this miner
274
    burn_election_block: BlockSnapshot,
275
    /// Current burnchain tip as of the last TenureChange
276
    /// * if the last tenure-change was a BlockFound, then this is the same as the
277
    /// `burn_election_block` (and it is also the `burn_view`)
278
    /// * otherwise, if the last tenure-change is an Extend, then this is the sortition of the burn
279
    /// view consensus hash in the TenureChange
280
    burn_block: BlockSnapshot,
281
    /// The start of the parent tenure for this tenure
282
    parent_tenure_id: StacksBlockId,
283
    /// Handle to the node's event dispatcher
284
    event_dispatcher: EventDispatcher,
285
    /// The reason the miner thread was spawned
286
    reason: MinerReason,
287
    /// Handle to the p2p thread for block broadcast
288
    p2p_handle: NetworkHandle,
289
    signer_set_cache: Option<RewardSet>,
290
    /// The time at which tenure change/extend was attempted
291
    tenure_change_time: Instant,
292
    /// The current tip when this miner thread was started.
293
    /// This *should not* be passed into any block building code, as it
294
    ///  is not necessarily the burn view for the block being constructed.
295
    /// Rather, this burn block is used to determine whether or not a new
296
    ///  burn block has arrived since this thread started.
297
    burn_tip_at_start: ConsensusHash,
298
    /// flag to indicate an abort driven from the relayer
299
    abort_flag: Arc<AtomicBool>,
300
    /// Should the nonce and considered transactions cache be reset before mining the next block?
301
    reset_mempool_caches: bool,
302
    /// Storage for persisting non-confidential miner information
303
    miner_db: MinerDB,
304
    /// Transaction IDs to exclude from the next block proposal only.
305
    /// Replaced (not accumulated) on each signer rejection.
306
    temporarily_excluded_txids: HashSet<Txid>,
307
    /// Transaction IDs to permanently ban from the mempool.
308
    /// Drained and blacklisted from the mempool in mine_block().
309
    permanently_excluded_txids: HashSet<Txid>,
310
}
311

312
/// Trait for the coordinator's read count extend timestamp check.
313
/// This trait is used so that we can unit test the function more easily.
314
trait ReadCountCheck {
315
    /// Get the timestamp at which at least 70% of the signing power should be
316
    /// willing to accept a time-based read-count extension.
317
    fn get_read_count_extend_timestamp(&self) -> u64;
318
}
319

320
impl ReadCountCheck for SignerCoordinator {
321
    fn get_read_count_extend_timestamp(&self) -> u64 {
52✔
322
        SignerCoordinator::get_read_count_extend_timestamp(self)
52✔
323
    }
52✔
324
}
325

326
impl BlockMinerThread {
327
    /// Instantiate the miner thread
328
    pub fn new(
1,405✔
329
        rt: &RelayerThread,
1,405✔
330
        registered_key: RegisteredKey,
1,405✔
331
        burn_election_block: BlockSnapshot,
1,405✔
332
        burn_block: BlockSnapshot,
1,405✔
333
        parent_tenure_id: StacksBlockId,
1,405✔
334
        burn_tip_at_start: &ConsensusHash,
1,405✔
335
        reason: MinerReason,
1,405✔
336
    ) -> Result<BlockMinerThread, NakamotoNodeError> {
1,405✔
337
        Ok(BlockMinerThread {
338
            config: rt.config.clone(),
1,405✔
339
            globals: rt.globals.clone(),
1,405✔
340
            keychain: rt.keychain.clone(),
1,405✔
341
            burnchain: rt.burnchain.clone(),
1,405✔
342
            last_block_mined: None,
1,405✔
343
            mined_blocks: 0,
344
            registered_key,
1,405✔
345
            burn_election_block,
1,405✔
346
            burn_block,
1,405✔
347
            event_dispatcher: rt.event_dispatcher.clone(),
1,405✔
348
            parent_tenure_id,
1,405✔
349
            reason,
1,405✔
350
            p2p_handle: rt.get_p2p_handle(),
1,405✔
351
            signer_set_cache: None,
1,405✔
352
            burn_tip_at_start: burn_tip_at_start.clone(),
1,405✔
353
            tenure_change_time: Instant::now(),
1,405✔
354
            abort_flag: Arc::new(AtomicBool::new(false)),
1,405✔
355
            tenure_cost: ExecutionCost::ZERO,
356
            tenure_budget: ExecutionCost::ZERO,
357
            reset_mempool_caches: true,
358
            miner_db: MinerDB::open_with_config(&rt.config)?,
1,405✔
359
            temporarily_excluded_txids: HashSet::new(),
1,405✔
360
            permanently_excluded_txids: HashSet::new(),
1,405✔
361
        })
362
    }
1,405✔
363

364
    pub fn get_abort_flag(&self) -> Arc<AtomicBool> {
1,405✔
365
        self.abort_flag.clone()
1,405✔
366
    }
1,405✔
367

368
    #[cfg(test)]
369
    fn fault_injection_block_proposal_stall(new_block: &NakamotoBlock) {
2,473✔
370
        if TEST_BROADCAST_PROPOSAL_STALL.get().iter().any(|key| {
2,473✔
371
            key.verify(
30✔
372
                new_block.header.miner_signature_hash().as_bytes(),
30✔
373
                &new_block.header.miner_signature,
30✔
374
            )
375
            .unwrap_or_default()
30✔
376
        }) {
30✔
377
            warn!("Fault injection: Block proposal broadcast is stalled due to testing directive.";
20✔
378
                        "stacks_block_id" => %new_block.block_id(),
20✔
379
                        "stacks_block_hash" => %new_block.header.block_hash(),
20✔
380
                        "height" => new_block.header.chain_length,
20✔
381
                        "consensus_hash" => %new_block.header.consensus_hash
382
            );
383
            while TEST_BROADCAST_PROPOSAL_STALL.get().iter().any(|key| {
35,607✔
384
                key.verify(
35,591✔
385
                    new_block.header.miner_signature_hash().as_bytes(),
35,591✔
386
                    &new_block.header.miner_signature,
35,591✔
387
                )
388
                .unwrap_or_default()
35,591✔
389
            }) {
35,591✔
390
                std::thread::sleep(std::time::Duration::from_millis(10));
33,459✔
391
            }
33,459✔
392
            info!("Fault injection: Block proposal broadcast is no longer stalled due to testing directive.";
20✔
393
                    "block_id" => %new_block.block_id(),
19✔
394
                    "height" => new_block.header.chain_length,
19✔
395
                    "consensus_hash" => %new_block.header.consensus_hash
396
            );
397
        }
2,453✔
398
    }
2,473✔
399

400
    #[cfg(not(test))]
401
    fn fault_injection_block_proposal_stall(_ignored: &NakamotoBlock) {}
402

403
    #[cfg(test)]
404
    fn fault_injection_block_mining_skip() -> bool {
9,181✔
405
        if TEST_MINE_SKIP.get() {
9,181✔
406
            warn!("Fault injection: Block mining is skipped due to testing directive.");
1,795✔
407
            true
1,795✔
408
        } else {
409
            false
7,386✔
410
        }
411
    }
9,181✔
412

413
    #[cfg(not(test))]
414
    fn fault_injection_block_mining_skip() -> bool {
415
        false
416
    }
417

418
    #[cfg(test)]
419
    fn fault_injection_block_announce_stall(new_block: &NakamotoBlock) {
2,303✔
420
        if TEST_BLOCK_ANNOUNCE_STALL.get() {
2,303✔
421
            // Do an extra check just so we don't log EVERY time.
422
            warn!("Fault injection: Block announcement is stalled due to testing directive.";
4✔
423
                      "stacks_block_id" => %new_block.block_id(),
4✔
424
                      "stacks_block_hash" => %new_block.header.block_hash(),
4✔
425
                      "height" => new_block.header.chain_length,
4✔
426
                      "consensus_hash" => %new_block.header.consensus_hash
427
            );
428
            while TEST_BLOCK_ANNOUNCE_STALL.get() {
3,036✔
429
                std::thread::sleep(std::time::Duration::from_millis(10));
3,032✔
430
            }
3,032✔
431
            info!("Fault injection: Block announcement is no longer stalled due to testing directive.";
4✔
432
                  "block_id" => %new_block.block_id(),
4✔
433
                  "height" => new_block.header.chain_length,
4✔
434
                  "consensus_hash" => %new_block.header.consensus_hash
435
            );
436
        }
2,299✔
437
    }
2,303✔
438

439
    #[cfg(not(test))]
440
    fn fault_injection_block_announce_stall(_ignored: &NakamotoBlock) {}
441

442
    #[cfg(test)]
443
    fn fault_injection_skip_block_broadcast() -> bool {
2,264✔
444
        TEST_P2P_BROADCAST_SKIP.get()
2,264✔
445
    }
2,264✔
446

447
    #[cfg(not(test))]
448
    fn fault_injection_skip_block_broadcast() -> bool {
449
        false
450
    }
451

452
    #[cfg(test)]
453
    fn fault_injection_block_broadcast_stall(new_block: &NakamotoBlock) {
2,259✔
454
        if TEST_P2P_BROADCAST_STALL.get() {
2,259✔
455
            // Do an extra check just so we don't log EVERY time.
456
            warn!("Fault injection: P2P block broadcast is stalled due to testing directive.";
3✔
457
                      "stacks_block_id" => %new_block.block_id(),
3✔
458
                      "stacks_block_hash" => %new_block.header.block_hash(),
3✔
459
                      "height" => new_block.header.chain_length,
3✔
460
                      "consensus_hash" => %new_block.header.consensus_hash
461
            );
462
            while TEST_P2P_BROADCAST_STALL.get() {
578✔
463
                std::thread::sleep(std::time::Duration::from_millis(10));
575✔
464
            }
575✔
465
            info!("Fault injection: P2P block broadcast is no longer stalled due to testing directive.";
3✔
466
                  "block_id" => %new_block.block_id(),
3✔
467
                  "height" => new_block.header.chain_length,
3✔
468
                  "consensus_hash" => %new_block.header.consensus_hash
469
            );
470
        }
2,256✔
471
    }
2,259✔
472

473
    #[cfg(not(test))]
474
    fn fault_injection_block_broadcast_stall(_ignored: &NakamotoBlock) {}
475

476
    #[cfg(test)]
477
    fn fault_injection_skip_block_push() -> bool {
2,264✔
478
        TEST_BLOCK_PUSH_SKIP.get()
2,264✔
479
    }
2,264✔
480

481
    #[cfg(not(test))]
482
    fn fault_injection_skip_block_push() -> bool {
483
        false
484
    }
485

486
    /// Stall the miner based on the `TEST_MINE_STALL` flag.
487
    /// Block the miner until the test flag is set to `NotStalled`.
488
    #[cfg(test)]
489
    fn fault_injection_miner_stall() {
9,326✔
490
        if TEST_MINE_STALL.get() != TestMineStall::NotStalled {
9,326✔
491
            // Do an extra check just so we don't log EVERY time.
492
            warn!("Mining is stalled due to testing directive");
79✔
493
            TEST_MINE_STALL.set(TestMineStall::Stalled);
79✔
494
            while TEST_MINE_STALL.get() != TestMineStall::NotStalled {
96,408✔
495
                std::thread::sleep(std::time::Duration::from_millis(10));
96,329✔
496
            }
96,329✔
497
            warn!("Mining is no longer stalled due to testing directive. Continuing...");
79✔
498
        }
9,247✔
499
    }
9,326✔
500

501
    #[cfg(not(test))]
502
    fn fault_injection_miner_stall() {}
503

504
    pub fn run_miner(
1,405✔
505
        mut self,
1,405✔
506
        prior_miner: Option<MinerStopHandle>,
1,405✔
507
    ) -> Result<(), NakamotoNodeError> {
1,405✔
508
        // when starting a new tenure, block the mining thread if its currently running.
509
        // the new mining thread will join it (so that the new mining thread stalls, not the relayer)
510
        debug!(
1,405✔
511
            "New miner thread starting";
512
            "had_prior_miner" => prior_miner.is_some(),
×
513
            "parent_tenure_id" => %self.parent_tenure_id,
514
            "thread_id" => ?thread::current().id(),
×
515
            "burn_block_consensus_hash" => %self.burn_block.consensus_hash,
516
            "burn_election_block_consensus_hash" => %self.burn_election_block.consensus_hash,
517
            "reason" => %self.reason,
518
        );
519
        if let Some(prior_miner) = prior_miner {
1,405✔
520
            debug!(
1,177✔
521
                "Miner thread {:?}: will try and stop prior miner {:?}",
522
                thread::current().id(),
×
523
                prior_miner.inner_thread().id()
×
524
            );
525
            prior_miner.stop(&self.globals)?;
1,177✔
526
        }
228✔
527
        let mut stackerdbs = StackerDBs::connect(&self.config.get_stacker_db_file_path(), true)?;
1,405✔
528
        let mut last_block_rejected = false;
1,405✔
529

530
        let reward_set = self.load_signer_set()?;
1,405✔
531
        let Some(miner_privkey) = self.config.miner.mining_key.clone() else {
1,405✔
532
            return Err(NakamotoNodeError::MinerConfigurationFailed(
×
533
                "No mining key configured, cannot mine",
×
534
            ));
×
535
        };
536
        let sortdb = SortitionDB::open(
1,405✔
537
            &self.config.get_burn_db_file_path(),
1,405✔
538
            true,
539
            self.burnchain.pox_constants.clone(),
1,405✔
540
            Some(self.config.node.get_marf_opts()),
1,405✔
541
        )
542
        .expect("FATAL: could not open sortition DB");
1,405✔
543

544
        // Start the signer coordinator
545
        let mut coordinator = SignerCoordinator::new(
1,405✔
546
            self.event_dispatcher.stackerdb_channel.clone(),
1,405✔
547
            self.globals.should_keep_running.clone(),
1,405✔
548
            &reward_set,
1,405✔
549
            &self.burn_election_block,
1,405✔
550
            &self.burnchain,
1,405✔
551
            miner_privkey,
1,405✔
552
            &self.config,
1,405✔
553
            &self.burn_tip_at_start,
1,405✔
554
        )
555
        .map_err(|e| {
1,405✔
556
            NakamotoNodeError::SigningCoordinatorFailure(format!(
×
557
                "Failed to initialize the signing coordinator. Cannot mine! {e:?}"
×
558
            ))
×
559
        })?;
×
560

561
        // now, actually run this tenure
562
        loop {
563
            if let Err(e) = self.attempt_mine_and_propose_block(
9,326✔
564
                &mut coordinator,
9,326✔
565
                &sortdb,
9,326✔
566
                &mut stackerdbs,
9,326✔
567
                &mut last_block_rejected,
9,326✔
568
                &reward_set,
9,326✔
569
            ) {
9,326✔
570
                // Before stopping this miner, shutdown the coordinator thread.
571
                coordinator.shutdown();
1,405✔
572
                return Err(e);
1,405✔
573
            }
7,921✔
574
        }
575
    }
1,405✔
576

577
    /// Pause the miner thread and retry to mine
578
    fn pause_and_retry(
124✔
579
        &self,
124✔
580
        new_block: &NakamotoBlock,
124✔
581
        last_block_rejected: &mut bool,
124✔
582
        e: &NakamotoNodeError,
124✔
583
    ) {
124✔
584
        // Sleep for a bit to allow signers to catch up
585
        let pause_ms = if *last_block_rejected {
124✔
586
            self.config.miner.subsequent_rejection_pause_ms
34✔
587
        } else {
588
            self.config.miner.first_rejection_pause_ms
90✔
589
        };
590

591
        error!("Error while gathering signatures: {e:?}. Will try mining again in {pause_ms}.";
124✔
592
            "signer_signature_hash" => %new_block.header.signer_signature_hash(),
124✔
593
            "block_height" => new_block.header.chain_length,
124✔
594
            "consensus_hash" => %new_block.header.consensus_hash,
595
        );
596
        thread::sleep(Duration::from_millis(pause_ms));
124✔
597
        *last_block_rejected = true;
124✔
598
    }
124✔
599

600
    /// Attempts to mine a block, propose it, and broadcast it if successful.
601
    ///
602
    /// Note: `Ok(())` does not guarantee that a block was mined, only that the
603
    /// mining attempt completed and a subsequent attempt should be tried
604
    ///
605
    /// Returns `Ok(())` if mining completes successfully or should be retried.
606
    /// Returns `Err` if the mining thread should exit (e.g., due to tenure changes or shutdown).
607
    fn attempt_mine_and_propose_block(
9,326✔
608
        &mut self,
9,326✔
609
        coordinator: &mut SignerCoordinator,
9,326✔
610
        sortdb: &SortitionDB,
9,326✔
611
        stackerdbs: &mut StackerDBs,
9,326✔
612
        last_block_rejected: &mut bool,
9,326✔
613
        reward_set: &RewardSet,
9,326✔
614
    ) -> Result<(), NakamotoNodeError> {
9,326✔
615
        Self::fault_injection_miner_stall();
9,326✔
616
        let mut chain_state =
9,326✔
617
            neon_node::open_chainstate_with_faults(&self.config).map_err(|e| {
9,326✔
618
                NakamotoNodeError::SigningCoordinatorFailure(format!(
×
619
                    "Failed to open chainstate DB. Cannot mine! {e:?}"
×
620
                ))
×
621
            })?;
×
622
        // Late block tenures are initiated only to issue the BlockFound
623
        //  tenure change tx (because they can be immediately extended to
624
        //  the next burn view). This checks whether or not we're in such a
625
        //  tenure and have produced a block already. If so, it exits the
626
        //  mining thread to allow the tenure extension thread to take over.
627
        if self.last_block_mined.is_some() && self.reason.is_late_block() {
9,326✔
628
            info!("Miner: finished mining a late tenure");
17✔
629
            return Err(NakamotoNodeError::StacksTipChanged);
17✔
630
        }
9,309✔
631
        // If we're mock mining, we may not have processed the block that the
632
        // actual tenure winner committed to yet. So, before attempting to
633
        // mock mine, check if the parent is processed.
634
        if self.config.get_node_config(false).mock_mining
9,309✔
635
            && !self.is_parent_processed(&mut chain_state)?
178✔
636
        {
637
            info!("Mock miner has not processed parent block yet, sleeping and trying again");
17✔
638
            thread::sleep(Duration::from_millis(ABORT_TRY_AGAIN_MS));
17✔
639
            return Ok(());
17✔
640
        }
9,289✔
641

642
        // Reset the mempool caches if needed. When mock-mining, we always
643
        // reset the caches, because the blocks we mine are not actually
644
        // processed, so the mempool caches are not valid.
645
        if self.reset_mempool_caches
9,289✔
646
            || self.config.miner.mempool_walk_strategy
3,902✔
647
                == MemPoolWalkStrategy::NextNonceWithHighestFeeRate
3,902✔
648
            || self.config.node.mock_mining
90✔
649
        {
650
            let mut mem_pool = self
9,194✔
651
                .config
9,194✔
652
                .connect_mempool_db()
9,194✔
653
                .expect("Database failure opening mempool");
9,194✔
654

655
            if self.reset_mempool_caches || self.config.node.mock_mining {
9,194✔
656
                mem_pool.reset_mempool_caches()?;
5,433✔
657
            } else {
658
                // Even if the nonce cache is still valid, NextNonceWithHighestFeeRate strategy
659
                // needs to reset this cache after each block. This prevents skipping transactions
660
                // that were previously considered, but not included in previous blocks.
661
                mem_pool.reset_considered_txs_cache()?;
3,761✔
662
            }
663
        }
95✔
664

665
        let Some(new_block) = self.mine_block_and_handle_result(coordinator)? else {
9,289✔
666
            // We should reattempt to mine
667
            return Ok(());
5,591✔
668
        };
669

670
        if !self.propose_new_block_and_broadcast(
2,614✔
671
            coordinator,
2,614✔
672
            sortdb,
2,614✔
673
            stackerdbs,
2,614✔
674
            last_block_rejected,
2,614✔
675
            reward_set,
2,614✔
676
            new_block,
2,614✔
677
        )? {
20✔
678
            // We should reattempt to mine
679
            return Ok(());
291✔
680
        }
2,303✔
681

682
        // Wait until the last block has been mined and processed
683
        self.wait_for_last_block_mined_and_processed(&mut chain_state)?;
2,303✔
684

685
        Ok(())
2,226✔
686
    }
9,326✔
687

688
    /// Check if the parent block has been processed
689
    fn is_parent_processed(
178✔
690
        &mut self,
178✔
691
        chain_state: &mut StacksChainState,
178✔
692
    ) -> Result<bool, NakamotoNodeError> {
178✔
693
        let burn_db_path = self.config.get_burn_db_file_path();
178✔
694
        let mut burn_db = SortitionDB::open(
178✔
695
            &burn_db_path,
178✔
696
            true,
697
            self.burnchain.pox_constants.clone(),
178✔
698
            Some(self.config.node.get_marf_opts()),
178✔
699
        )
700
        .expect("FATAL: could not open sortition DB");
178✔
701
        self.check_burn_tip_changed(&burn_db)?;
178✔
702
        match self.load_block_parent_info(&mut burn_db, chain_state) {
175✔
703
            Ok(..) => Ok(true),
158✔
704
            Err(NakamotoNodeError::ParentNotFound) => Ok(false),
17✔
705
            Err(e) => {
×
706
                warn!("Failed to load parent info: {e:?}");
×
707
                Err(e)
×
708
            }
709
        }
710
    }
178✔
711

712
    /// Attempts to mine a block and handle the result.
713
    ///
714
    /// - Returns `Ok(Some(NakamotoBlock))` if a block is successfully mined and passes timestamp validation.
715
    /// - Returns `Ok(None)` if mining should be retried (e.g. due to early block timestamp or no transactions).
716
    /// - Returns `Err(NakamotoNodeError)` if mining should be aborted (e.g. shutdown signal or unexpected error).
717
    fn mine_block_and_handle_result(
9,283✔
718
        &mut self,
9,283✔
719
        coordinator: &mut SignerCoordinator,
9,283✔
720
    ) -> Result<Option<NakamotoBlock>, NakamotoNodeError> {
9,283✔
721
        match self.mine_block(coordinator) {
9,283✔
722
            Ok(x) => {
2,487✔
723
                if !self.validate_timestamp(&x)? {
2,487✔
724
                    info!("Block mined too quickly. Will try again.";
14✔
725
                        "block_timestamp" => x.header.timestamp,
×
726
                    );
727
                    return Ok(None);
14✔
728
                }
2,473✔
729
                Ok(Some(x))
2,473✔
730
            }
731
            Err(NakamotoNodeError::MiningFailure(ChainstateError::MinerAborted)) => {
732
                if self.abort_flag.load(Ordering::SeqCst) {
4,198✔
733
                    info!("Miner interrupted while mining in order to shut down");
×
734
                    self.globals
×
735
                        .raise_initiative(format!("MiningFailure: aborted by node"));
×
736
                    return Err(ChainstateError::MinerAborted.into());
×
737
                }
4,198✔
738

739
                info!("Miner interrupted while mining, will try again");
4,198✔
740

741
                // sleep, and try again. if the miner was interrupted because the burnchain
742
                // view changed, the next `mine_block()` invocation will error
743
                thread::sleep(Duration::from_millis(ABORT_TRY_AGAIN_MS));
4,198✔
744
                Ok(None)
4,198✔
745
            }
746
            Err(NakamotoNodeError::MiningFailure(ChainstateError::NoTransactionsToMine)) => {
747
                debug!(
2,495✔
748
                    "Miner did not find any transactions to mine, sleeping for {:?}",
749
                    self.config.miner.empty_mempool_sleep_time
750
                );
751
                self.reset_mempool_caches = false;
2,495✔
752

753
                // Pause the miner to wait for transactions to arrive
754
                let now = Instant::now();
2,495✔
755
                while now.elapsed() < self.config.miner.empty_mempool_sleep_time {
28,076✔
756
                    if self.abort_flag.load(Ordering::SeqCst) {
26,597✔
757
                        info!("Miner interrupted while mining in order to shut down");
24✔
758
                        self.globals
24✔
759
                            .raise_initiative(format!("MiningFailure: aborted by node"));
24✔
760
                        return Err(ChainstateError::MinerAborted.into());
24✔
761
                    }
26,573✔
762

763
                    // Check if the burnchain tip has changed
764
                    let Ok(sort_db) = SortitionDB::open(
26,573✔
765
                        &self.config.get_burn_db_file_path(),
26,573✔
766
                        false,
26,573✔
767
                        self.burnchain.pox_constants.clone(),
26,573✔
768
                        Some(self.config.node.get_marf_opts()),
26,573✔
769
                    ) else {
26,573✔
770
                        error!("Failed to open sortition DB. Will try mining again.");
×
771
                        return Ok(None);
×
772
                    };
773
                    if self.check_burn_tip_changed(&sort_db).is_err() {
26,573✔
774
                        return Err(NakamotoNodeError::BurnchainTipChanged);
992✔
775
                    }
25,581✔
776

777
                    thread::sleep(Duration::from_millis(ABORT_TRY_AGAIN_MS));
25,581✔
778
                }
779
                Ok(None)
1,479✔
780
            }
781
            Err(NakamotoNodeError::ParentNotFound) if self.config.node.mock_mining => {
×
782
                info!(
×
783
                    "Mock miner could not load parent tenure info yet. Will try again.";
784
                );
785
                thread::sleep(Duration::from_millis(ABORT_TRY_AGAIN_MS));
×
786
                Ok(None)
×
787
            }
788
            Err(e) => {
103✔
789
                warn!("Failed to mine block: {e:?}");
103✔
790

791
                // try again, in case a new sortition is pending
792
                self.globals
103✔
793
                    .raise_initiative(format!("MiningFailure: {e:?}"));
103✔
794
                Err(ChainstateError::MinerAborted.into())
103✔
795
            }
796
        }
797
    }
9,283✔
798

799
    /// Attempts to propose a new block and broadcast it upon success.
800
    ///
801
    /// - Returns `Ok(true)` if the block was successfully proposed and broadcasted.
802
    /// - Returns `Ok(false)` if the proposal failed but the miner should retry (e.g. due to tip change or recoverable upload error).
803
    /// - Returns `Err(NakamotoNodeError)` if the operation should be aborted (e.g. unrecoverable error during proposal or broadcasting).
804
    fn propose_new_block_and_broadcast(
2,473✔
805
        &mut self,
2,473✔
806
        coordinator: &mut SignerCoordinator,
2,473✔
807
        sortdb: &SortitionDB,
2,473✔
808
        stackerdbs: &mut StackerDBs,
2,473✔
809
        last_block_rejected: &mut bool,
2,473✔
810
        reward_set: &RewardSet,
2,473✔
811
        mut new_block: NakamotoBlock,
2,473✔
812
    ) -> Result<bool, NakamotoNodeError> {
2,473✔
813
        Self::fault_injection_block_proposal_stall(&new_block);
2,473✔
814

815
        let signer_signature = match self.propose_block(
2,473✔
816
            coordinator,
2,473✔
817
            &mut new_block,
2,473✔
818
            sortdb,
2,473✔
819
            stackerdbs,
2,473✔
820
        ) {
2,473✔
821
            Ok(x) => x,
2,325✔
822
            Err(e) => match e {
148✔
823
                NakamotoNodeError::StacksTipChanged => {
824
                    info!("Stacks tip changed while waiting for signatures";
4✔
825
                        "signer_signature_hash" => %new_block.header.signer_signature_hash(),
4✔
826
                        "block_height" => new_block.header.chain_length,
4✔
827
                        "consensus_hash" => %new_block.header.consensus_hash,
828
                    );
829
                    return Ok(false);
4✔
830
                }
831
                NakamotoNodeError::BurnchainTipChanged => {
832
                    info!("Burnchain tip changed while waiting for signatures";
20✔
833
                        "signer_signature_hash" => %new_block.header.signer_signature_hash(),
20✔
834
                        "block_height" => new_block.header.chain_length,
20✔
835
                        "consensus_hash" => %new_block.header.consensus_hash,
836
                    );
837
                    return Err(e);
20✔
838
                }
839
                NakamotoNodeError::StackerDBUploadError(ref ack) => {
×
840
                    if ack.code == Some(StackerDBErrorCodes::BadSigner.code()) {
×
841
                        error!("Error while gathering signatures: failed to upload miner StackerDB data: {ack:?}. Giving up.";
×
842
                            "signer_signature_hash" => %new_block.header.signer_signature_hash(),
×
843
                            "block_height" => new_block.header.chain_length,
×
844
                            "consensus_hash" => %new_block.header.consensus_hash,
845
                        );
846
                        return Err(e);
×
847
                    }
×
848
                    self.pause_and_retry(&new_block, last_block_rejected, &e);
×
849
                    return Ok(false);
×
850
                }
851
                NakamotoNodeError::SignersRejected {
852
                    ref temporarily_excluded_txids,
111✔
853
                    ref permanently_excluded_txids,
111✔
854
                } => {
855
                    // Replace (not extend) temporarily_excluded_txids so the ban
856
                    // only applies to the next block proposal — transient failures
857
                    // may resolve after one block.
858
                    self.temporarily_excluded_txids = temporarily_excluded_txids.clone();
111✔
859
                    // Permanently excluded txids will be blacklisted from the
860
                    // mempool when mine_block opens the mempool connection.
861
                    self.permanently_excluded_txids
111✔
862
                        .extend(permanently_excluded_txids.iter().cloned());
111✔
863
                    self.pause_and_retry(&new_block, last_block_rejected, &e);
111✔
864
                    return Ok(false);
111✔
865
                }
866
                _ => {
867
                    self.pause_and_retry(&new_block, last_block_rejected, &e);
13✔
868
                    return Ok(false);
13✔
869
                }
870
            },
871
        };
872
        *last_block_rejected = false;
2,325✔
873

874
        new_block.header.signer_signature = signer_signature;
2,325✔
875
        if let Err(e) = self.broadcast(new_block.clone(), reward_set, stackerdbs) {
2,325✔
876
            warn!("Error accepting own block: {e:?}. Will try mining again.");
22✔
877
            return Ok(false);
22✔
878
        } else {
879
            info!(
2,303✔
880
                "Miner: Block signed by signer set and broadcasted";
881
                "signer_signature_hash" => %new_block.header.signer_signature_hash(),
2,303✔
882
                "stacks_block_hash" => %new_block.header.block_hash(),
2,303✔
883
                "stacks_block_id" => %new_block.header.block_id(),
2,303✔
884
                "block_height" => new_block.header.chain_length,
2,303✔
885
                "consensus_hash" => %new_block.header.consensus_hash,
886
            );
887

888
            // We successfully mined, so the mempool caches are valid.
889
            self.reset_mempool_caches = false;
2,303✔
890
            // Block was accepted — clear any single-block exclusions
891
            self.temporarily_excluded_txids.clear();
2,303✔
892
        }
893

894
        // update mined-block counters and mined-tenure counters
895
        self.globals.counters.bump_naka_mined_blocks();
2,303✔
896
        if self.last_block_mined.is_none() {
2,303✔
897
            // this is the first block of the tenure, bump tenure counter
1,345✔
898
            self.globals.counters.bump_naka_mined_tenures();
1,345✔
899
        }
1,881✔
900

901
        // wake up chains coordinator
902
        Self::fault_injection_block_announce_stall(&new_block);
2,303✔
903
        self.globals.coord().announce_new_stacks_block();
2,303✔
904

905
        self.last_block_mined = Some((
2,303✔
906
            new_block.header.consensus_hash.clone(),
2,303✔
907
            new_block.header.block_hash(),
2,303✔
908
        ));
2,303✔
909
        self.mined_blocks += 1;
2,303✔
910
        Ok(true)
2,303✔
911
    }
2,473✔
912

913
    /// Blocks until the most recently mined block has been fully processed by the chainstate
914
    /// and the miner is unblocked.
915
    ///
916
    /// - Returns `Ok(())` when the block is processed and the miner is ready to continue.
917
    /// - Returns `Err(NakamotoNodeError)` if mining is aborted or the chainstate is inconsistent.
918
    fn wait_for_last_block_mined_and_processed(
2,303✔
919
        &mut self,
2,303✔
920
        chain_state: &mut StacksChainState,
2,303✔
921
    ) -> Result<(), NakamotoNodeError> {
2,303✔
922
        let Some((last_consensus_hash, last_bhh)) = &self.last_block_mined else {
2,303✔
923
            return Ok(());
×
924
        };
925

926
        // If mock-mining, we don't need to wait for the last block to be
927
        // processed (because it will never be). Instead just wait
928
        // `min_time_between_blocks_ms`, then resume mining.
929
        if self.config.node.mock_mining {
2,303✔
930
            thread::sleep(Duration::from_millis(
40✔
931
                self.config.miner.min_time_between_blocks_ms,
40✔
932
            ));
933
            return Ok(());
40✔
934
        }
2,263✔
935

936
        loop {
937
            let (_, processed, _, _) = chain_state
5,980✔
938
                .nakamoto_blocks_db()
5,980✔
939
                .get_block_processed_and_signed_weight(last_consensus_hash, &last_bhh)?
5,980✔
940
                .ok_or_else(|| NakamotoNodeError::UnexpectedChainState)?;
5,980✔
941

942
            // Once the block has been processed and the miner is no longer
943
            // blocked, we can continue mining.
944
            if processed
5,976✔
945
                && !(*self
2,975✔
946
                    .globals
2,975✔
947
                    .get_miner_status()
2,975✔
948
                    .lock()
2,975✔
949
                    .expect("FATAL: mutex poisoned"))
2,975✔
950
                .is_blocked()
2,975✔
951
            {
952
                return Ok(());
2,169✔
953
            }
3,807✔
954

955
            thread::sleep(Duration::from_millis(ABORT_TRY_AGAIN_MS));
3,807✔
956

957
            if self.abort_flag.load(Ordering::SeqCst) {
3,807✔
958
                info!("Miner interrupted while mining in order to shut down");
71✔
959
                self.globals
71✔
960
                    .raise_initiative(format!("MiningFailure: aborted by node"));
71✔
961
                return Err(ChainstateError::MinerAborted.into());
71✔
962
            }
3,736✔
963

964
            // Check if the burnchain tip has changed
965
            let Ok(sort_db) = SortitionDB::open(
3,736✔
966
                &self.config.get_burn_db_file_path(),
3,736✔
967
                false,
3,736✔
968
                self.burnchain.pox_constants.clone(),
3,736✔
969
                Some(self.config.node.get_marf_opts()),
3,736✔
970
            ) else {
3,736✔
971
                error!("Failed to open sortition DB. Will try mining again.");
×
972
                return Ok(());
×
973
            };
974
            if self.check_burn_tip_changed(&sort_db).is_err() {
3,736✔
975
                return Err(NakamotoNodeError::BurnchainTipChanged);
19✔
976
            }
3,717✔
977
        }
978
    }
2,303✔
979

980
    fn propose_block(
2,472✔
981
        &self,
2,472✔
982
        coordinator: &mut SignerCoordinator,
2,472✔
983
        new_block: &mut NakamotoBlock,
2,472✔
984
        sortdb: &SortitionDB,
2,472✔
985
        stackerdbs: &mut StackerDBs,
2,472✔
986
    ) -> Result<Vec<MessageSignature>, NakamotoNodeError> {
2,472✔
987
        if self.config.get_node_config(false).mock_mining {
2,472✔
988
            // If we're mock mining, we don't actually propose the block.
989
            return Ok(Vec::new());
40✔
990
        }
2,432✔
991

992
        let mut chain_state =
2,432✔
993
            neon_node::open_chainstate_with_faults(&self.config).map_err(|e| {
2,432✔
994
                NakamotoNodeError::SigningCoordinatorFailure(format!(
×
995
                    "Failed to open chainstate DB. Cannot mine! {e:?}"
×
996
                ))
×
997
            })?;
×
998

999
        let burn_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).map_err(|e| {
2,432✔
1000
            NakamotoNodeError::SigningCoordinatorFailure(format!(
×
1001
                "Failed to open sortition DB. Cannot mine! {e:?}"
×
1002
            ))
×
1003
        })?;
×
1004

1005
        let diagnostics = MinerDiagnosticData {
2,432✔
1006
            burnchain_tip_height: burn_tip.block_height,
2,432✔
1007
            burnchain_tip_consensus_hash: burn_tip.consensus_hash,
2,432✔
1008
            burnchain_tip_header_hash: burn_tip.burn_header_hash,
2,432✔
1009
            tenure_extend_time_stamp: coordinator.get_tenure_extend_timestamp(),
2,432✔
1010
            read_count_extend_timestamp: coordinator.get_read_count_extend_timestamp(),
2,432✔
1011
            mining_reason: self.reason.clone().into(),
2,432✔
1012
        };
2,432✔
1013

1014
        coordinator.propose_block(
2,432✔
1015
            new_block,
2,432✔
1016
            &self.burnchain,
2,432✔
1017
            sortdb,
2,432✔
1018
            &mut chain_state,
2,432✔
1019
            stackerdbs,
2,432✔
1020
            &self.globals.counters,
2,432✔
1021
            &self.burn_election_block,
2,432✔
1022
            &self.miner_db,
2,432✔
1023
            diagnostics,
2,432✔
1024
        )
1025
    }
2,472✔
1026

1027
    /// Load the signer set active for this miner's blocks. This is the
1028
    ///  active reward set during `self.burn_election_block`. The miner
1029
    ///  thread caches this information, and this method will consult
1030
    ///  that cache (or populate it if necessary).
1031
    fn load_signer_set(&mut self) -> Result<RewardSet, NakamotoNodeError> {
10,683✔
1032
        if let Some(set) = self.signer_set_cache.as_ref() {
10,683✔
1033
            return Ok(set.clone());
9,283✔
1034
        }
1,400✔
1035
        let sort_db = SortitionDB::open(
1,400✔
1036
            &self.config.get_burn_db_file_path(),
1,400✔
1037
            true,
1038
            self.burnchain.pox_constants.clone(),
1,400✔
1039
            Some(self.config.node.get_marf_opts()),
1,400✔
1040
        )
1041
        .map_err(|e| {
1,400✔
1042
            NakamotoNodeError::SigningCoordinatorFailure(format!(
×
1043
                "Failed to open sortition DB. Cannot mine! {e:?}"
×
1044
            ))
×
1045
        })?;
×
1046

1047
        let mut chain_state =
1,400✔
1048
            neon_node::open_chainstate_with_faults(&self.config).map_err(|e| {
1,400✔
1049
                NakamotoNodeError::SigningCoordinatorFailure(format!(
×
1050
                    "Failed to open chainstate DB. Cannot mine! {e:?}"
×
1051
                ))
×
1052
            })?;
×
1053

1054
        let burn_election_height = self.burn_election_block.block_height;
1,400✔
1055

1056
        let reward_cycle = self
1,400✔
1057
            .burnchain
1,400✔
1058
            .block_height_to_reward_cycle(burn_election_height)
1,400✔
1059
            .expect("FATAL: no reward cycle for sortition");
1,400✔
1060

1061
        let reward_info = match load_nakamoto_reward_set(
1,400✔
1062
            reward_cycle,
1,400✔
1063
            &self.burn_election_block.sortition_id,
1,400✔
1064
            &self.burnchain,
1,400✔
1065
            &mut chain_state,
1,400✔
1066
            &self.parent_tenure_id,
1,400✔
1067
            &sort_db,
1,400✔
1068
            &OnChainRewardSetProvider::new(),
1,400✔
1069
        ) {
1070
            Ok(Some((reward_info, _))) => reward_info,
1,400✔
1071
            Ok(None) => {
1072
                return Err(NakamotoNodeError::SigningCoordinatorFailure(
×
1073
                    "No reward set stored yet. Cannot mine!".into(),
×
1074
                ));
×
1075
            }
1076
            Err(e) => {
×
1077
                return Err(NakamotoNodeError::SigningCoordinatorFailure(format!(
×
1078
                    "Failure while fetching reward set. Cannot initialize miner coordinator. {e:?}"
×
1079
                )));
×
1080
            }
1081
        };
1082

1083
        let Some(reward_set) = reward_info.known_selected_anchor_block_owned() else {
1,400✔
1084
            return Err(NakamotoNodeError::SigningCoordinatorFailure(
×
1085
                "Current reward cycle did not select a reward set. Cannot mine!".into(),
×
1086
            ));
×
1087
        };
1088

1089
        self.signer_set_cache = Some(reward_set.clone());
1,400✔
1090
        Ok(reward_set)
1,400✔
1091
    }
10,683✔
1092

1093
    /// Fault injection -- possibly fail to broadcast
1094
    /// Return true to drop the block
1095
    fn fault_injection_broadcast_fail(&self) -> bool {
2,240✔
1096
        let drop_prob = self
2,240✔
1097
            .config
2,240✔
1098
            .node
2,240✔
1099
            .fault_injection_block_push_fail_probability
2,240✔
1100
            .unwrap_or(0)
2,240✔
1101
            .min(100);
2,240✔
1102
        if drop_prob > 0 {
2,240✔
1103
            let throw: u8 = thread_rng().gen_range(0..100);
×
1104
            throw < drop_prob
×
1105
        } else {
1106
            false
2,240✔
1107
        }
1108
    }
2,240✔
1109

1110
    /// Store a block to the chainstate, and if successful (it should be since we mined it),
1111
    /// broadcast it via the p2p network.
1112
    fn broadcast_p2p(
2,264✔
1113
        &mut self,
2,264✔
1114
        sort_db: &SortitionDB,
2,264✔
1115
        chain_state: &mut StacksChainState,
2,264✔
1116
        block: &NakamotoBlock,
2,264✔
1117
        reward_set: &RewardSet,
2,264✔
1118
    ) -> Result<(), ChainstateError> {
2,264✔
1119
        if Self::fault_injection_skip_block_broadcast() {
2,264✔
1120
            warn!(
5✔
1121
                "Fault injection: Skipping block broadcast for {}",
1122
                block.block_id()
5✔
1123
            );
1124
            return Ok(());
5✔
1125
        }
2,259✔
1126
        #[cfg(test)]
1127
        TEST_MINER_BROADCASTING_BLOCK.set(block.clone());
2,259✔
1128

1129
        Self::fault_injection_block_broadcast_stall(block);
2,259✔
1130

1131
        let parent_block_info =
2,259✔
1132
            NakamotoChainState::get_block_header(chain_state.db(), &block.header.parent_block_id)?
2,259✔
1133
                .ok_or_else(|| ChainstateError::NoSuchBlockError)?;
2,259✔
1134
        let burn_view_ch =
2,259✔
1135
            NakamotoChainState::get_block_burn_view(sort_db, block, &parent_block_info)?;
2,259✔
1136
        let mut sortition_handle = sort_db.index_handle_at_ch(&burn_view_ch)?;
2,259✔
1137
        let accepted = NakamotoChainState::accept_block(
2,259✔
1138
            chain_state,
2,259✔
1139
            block,
2,259✔
1140
            &mut sortition_handle,
2,259✔
1141
            reward_set,
2,259✔
1142
            NakamotoBlockObtainMethod::Mined,
2,259✔
1143
        )?;
×
1144

1145
        if !accepted {
2,259✔
1146
            // this can happen if the p2p network and relayer manage to receive this block prior to
1147
            // the thread reaching this point -- this can happen because the signers broadcast the
1148
            // signed block to the nodes independent of the miner, so the miner itself can receive
1149
            // and store its own block outside of this thread.
1150
            debug!("Did NOT accept block {} we mined", &block.block_id());
19✔
1151

1152
            // not much we can do here, but try and mine again and hope we produce a valid one.
1153
            return Ok(());
19✔
1154
        }
2,240✔
1155

1156
        // forward to p2p thread, but do fault injection
1157
        if self.fault_injection_broadcast_fail() {
2,240✔
1158
            info!("Fault injection: drop block {}", &block.block_id());
×
1159
            return Ok(());
×
1160
        }
2,240✔
1161

1162
        let block_id = block.block_id();
2,240✔
1163
        debug!("Broadcasting block {block_id}");
2,240✔
1164
        if let Err(e) = self.p2p_handle.broadcast_message(
2,240✔
1165
            vec![],
2,240✔
1166
            StacksMessageType::NakamotoBlocks(NakamotoBlocksData {
2,240✔
1167
                blocks: vec![block.clone()],
2,240✔
1168
            }),
2,240✔
1169
        ) {
2,240✔
1170
            warn!("Failed to broadcast block {block_id}: {e:?}");
×
1171
        }
2,240✔
1172
        Ok(())
2,240✔
1173
    }
2,264✔
1174

1175
    fn broadcast(
2,304✔
1176
        &mut self,
2,304✔
1177
        block: NakamotoBlock,
2,304✔
1178
        reward_set: &RewardSet,
2,304✔
1179
        stackerdbs: &StackerDBs,
2,304✔
1180
    ) -> Result<(), NakamotoNodeError> {
2,304✔
1181
        if self.config.get_node_config(false).mock_mining {
2,304✔
1182
            // If we're mock mining, we don't actually broadcast the block.
1183
            return Ok(());
40✔
1184
        }
2,264✔
1185

1186
        if self.config.miner.mining_key.is_none() {
2,264✔
1187
            return Err(NakamotoNodeError::MinerConfigurationFailed(
×
1188
                "No mining key configured, cannot mine",
×
1189
            ));
×
1190
        };
2,264✔
1191

1192
        let mut chain_state = neon_node::open_chainstate_with_faults(&self.config)
2,264✔
1193
            .expect("FATAL: could not open chainstate DB");
2,264✔
1194
        let sort_db = SortitionDB::open(
2,264✔
1195
            &self.config.get_burn_db_file_path(),
2,264✔
1196
            true,
1197
            self.burnchain.pox_constants.clone(),
2,264✔
1198
            Some(self.config.node.get_marf_opts()),
2,264✔
1199
        )
1200
        .expect("FATAL: could not open sortition DB");
2,264✔
1201

1202
        // push block via p2p block push
1203
        self.broadcast_p2p(&sort_db, &mut chain_state, &block, reward_set)
2,264✔
1204
            .map_err(NakamotoNodeError::AcceptFailure)?;
2,264✔
1205

1206
        let Some(ref miner_privkey) = self.config.miner.mining_key else {
2,264✔
1207
            // should be unreachable, but we can't borrow this above broadcast_p2p() since it's
1208
            // mutable
1209
            return Err(NakamotoNodeError::MinerConfigurationFailed(
×
1210
                "No mining key configured, cannot mine",
×
1211
            ));
×
1212
        };
1213

1214
        // also, push block via stackerdb to make sure stackers get it
1215
        let rpc_socket = self.config.node.get_rpc_loopback().ok_or_else(|| {
2,264✔
1216
            NakamotoNodeError::MinerConfigurationFailed("Failed to get RPC loopback socket")
×
1217
        })?;
×
1218
        let miners_contract_id = boot_code_id(MINERS_NAME, chain_state.mainnet);
2,264✔
1219
        let mut miners_session = StackerDBSession::new(
2,264✔
1220
            &rpc_socket.to_string(),
2,264✔
1221
            miners_contract_id,
2,264✔
1222
            self.config.miner.stackerdb_timeout,
2,264✔
1223
        );
1224

1225
        if Self::fault_injection_skip_block_push() {
2,264✔
1226
            warn!(
×
1227
                "Fault injection: Skipping block push for {}",
1228
                block.block_id()
×
1229
            );
1230
            return Ok(());
×
1231
        }
2,264✔
1232

1233
        SignerCoordinator::send_miners_message(
2,264✔
1234
            miner_privkey,
2,264✔
1235
            &sort_db,
2,264✔
1236
            &self.burn_block,
2,264✔
1237
            stackerdbs,
2,264✔
1238
            SignerMessage::BlockPushed(block),
2,264✔
1239
            MinerSlotID::BlockPushed,
2,264✔
1240
            chain_state.mainnet,
2,264✔
1241
            &mut miners_session,
2,264✔
1242
            &self.burn_election_block.consensus_hash,
2,264✔
1243
            &self.miner_db,
2,264✔
1244
        )
1245
    }
2,304✔
1246

1247
    /// Get the coinbase recipient address, if set in the config and if allowed in this epoch
1248
    fn get_coinbase_recipient(&self, epoch_id: StacksEpochId) -> Option<PrincipalData> {
1,346✔
1249
        if epoch_id < StacksEpochId::Epoch21 && self.config.miner.block_reward_recipient.is_some() {
1,346✔
1250
            warn!("Coinbase pay-to-contract is not supported in the current epoch");
×
1251
            None
×
1252
        } else {
1253
            self.config.miner.block_reward_recipient.clone()
1,346✔
1254
        }
1255
    }
1,346✔
1256

1257
    fn generate_tenure_change_tx(
1,607✔
1258
        &self,
1,607✔
1259
        nonce: u64,
1,607✔
1260
        payload: TenureChangePayload,
1,607✔
1261
    ) -> StacksTransaction {
1,607✔
1262
        let is_mainnet = self.config.is_mainnet();
1,607✔
1263
        let chain_id = self.config.burnchain.chain_id;
1,607✔
1264
        let tenure_change_tx_payload = TransactionPayload::TenureChange(payload);
1,607✔
1265

1266
        let mut tx_auth = self.keychain.get_transaction_auth().unwrap();
1,607✔
1267
        tx_auth.set_origin_nonce(nonce);
1,607✔
1268

1269
        let version = if is_mainnet {
1,607✔
1270
            TransactionVersion::Mainnet
×
1271
        } else {
1272
            TransactionVersion::Testnet
1,607✔
1273
        };
1274

1275
        let mut tx = StacksTransaction::new(version, tx_auth, tenure_change_tx_payload);
1,607✔
1276

1277
        tx.chain_id = chain_id;
1,607✔
1278
        tx.anchor_mode = TransactionAnchorMode::OnChainOnly;
1,607✔
1279
        let mut tx_signer = StacksTransactionSigner::new(&tx);
1,607✔
1280
        self.keychain.sign_as_origin(&mut tx_signer);
1,607✔
1281

1282
        tx_signer.get_tx().unwrap()
1,607✔
1283
    }
1,607✔
1284

1285
    /// Create a coinbase transaction.
1286
    fn generate_coinbase_tx(
1,346✔
1287
        &self,
1,346✔
1288
        nonce: u64,
1,346✔
1289
        epoch_id: StacksEpochId,
1,346✔
1290
        vrf_proof: VRFProof,
1,346✔
1291
    ) -> StacksTransaction {
1,346✔
1292
        let is_mainnet = self.config.is_mainnet();
1,346✔
1293
        let chain_id = self.config.burnchain.chain_id;
1,346✔
1294
        let mut tx_auth = self.keychain.get_transaction_auth().unwrap();
1,346✔
1295
        tx_auth.set_origin_nonce(nonce);
1,346✔
1296

1297
        let version = if is_mainnet {
1,346✔
1298
            TransactionVersion::Mainnet
×
1299
        } else {
1300
            TransactionVersion::Testnet
1,346✔
1301
        };
1302

1303
        let recipient_opt = self.get_coinbase_recipient(epoch_id);
1,346✔
1304

1305
        let mut tx = StacksTransaction::new(
1,346✔
1306
            version,
1,346✔
1307
            tx_auth,
1,346✔
1308
            TransactionPayload::Coinbase(
1,346✔
1309
                CoinbasePayload([0u8; 32]),
1,346✔
1310
                recipient_opt,
1,346✔
1311
                Some(vrf_proof),
1,346✔
1312
            ),
1,346✔
1313
        );
1314
        tx.chain_id = chain_id;
1,346✔
1315
        tx.anchor_mode = TransactionAnchorMode::OnChainOnly;
1,346✔
1316
        let mut tx_signer = StacksTransactionSigner::new(&tx);
1,346✔
1317
        self.keychain.sign_as_origin(&mut tx_signer);
1,346✔
1318

1319
        tx_signer.get_tx().unwrap()
1,346✔
1320
    }
1,346✔
1321

1322
    #[cfg_attr(test, mutants::skip)]
1323
    /// Load up the parent block header for mining the next block.
1324
    /// If we can't find the parent in the DB but we expect one, return Err(ParentNotFound).
1325
    ///
1326
    /// The nakamoto miner must always build off of a chain tip that is either:
1327
    /// 1. The highest block in our own tenure
1328
    /// 2. The highest block in our tenure's parent tenure (i.e., `self.parent_tenure_id`)
1329
    ///
1330
    /// `self.parent_tenure_id` is the tenure start block which was
1331
    /// committed to by our tenure's associated block commit.
1332
    fn load_block_parent_header(
7,558✔
1333
        &self,
7,558✔
1334
        burn_db: &mut SortitionDB,
7,558✔
1335
        chain_state: &mut StacksChainState,
7,558✔
1336
    ) -> Result<StacksHeaderInfo, NakamotoNodeError> {
7,558✔
1337
        let my_tenure_tip = self
7,558✔
1338
            .find_highest_known_block_in_my_tenure(&burn_db, &chain_state)
7,558✔
1339
            .map_err(|e| {
7,558✔
1340
                error!(
×
1341
                    "Could not find highest header info for miner's tenure {}: {e:?}",
1342
                    &self.burn_election_block.consensus_hash
×
1343
                );
1344
                NakamotoNodeError::ParentNotFound
×
1345
            })?;
×
1346
        if let Some(my_tenure_tip) = my_tenure_tip {
7,558✔
1347
            debug!(
6,182✔
1348
                "Stacks block parent ID is last block in tenure ID {}",
1349
                &my_tenure_tip.consensus_hash
×
1350
            );
1351
            return Ok(my_tenure_tip);
6,182✔
1352
        }
1,376✔
1353

1354
        // My tenure is empty on the canonical fork, so our parent should be the highest block in
1355
        //   self.parent_tenure_id
1356
        debug!(
1,376✔
1357
            "Stacks block parent ID is last block in parent tenure tipped by {}",
1358
            &self.parent_tenure_id
×
1359
        );
1360

1361
        // find the last block in the parent tenure, since this is the tip we'll build atop
1362
        let parent_tenure_header =
1,359✔
1363
            NakamotoChainState::get_block_header(chain_state.db(), &self.parent_tenure_id)
1,376✔
1364
                .map_err(|e| {
1,376✔
1365
                    error!(
×
1366
                        "Could not query header for parent tenure ID {}: {e:?}",
1367
                        &self.parent_tenure_id
×
1368
                    );
1369
                    NakamotoNodeError::ParentNotFound
×
1370
                })?
×
1371
                .ok_or_else(|| {
1,376✔
1372
                    error!("No header for parent tenure ID {}", &self.parent_tenure_id);
17✔
1373
                    NakamotoNodeError::ParentNotFound
17✔
1374
                })?;
17✔
1375

1376
        let header_opt = NakamotoChainState::find_highest_known_block_header_in_tenure(
1,359✔
1377
            &chain_state,
1,359✔
1378
            burn_db,
1,359✔
1379
            &parent_tenure_header.consensus_hash,
1,359✔
1380
        )
1381
        .map_err(|e| {
1,359✔
1382
            error!("Could not query parent tenure finish block: {e:?}");
×
1383
            NakamotoNodeError::ParentNotFound
×
1384
        })?;
×
1385

1386
        if let Some(parent_tenure_tip) = header_opt {
1,359✔
1387
            return Ok(parent_tenure_tip);
1,359✔
1388
        }
×
1389

1390
        // this is an epoch2 block
1391
        debug!(
×
1392
            "Stacks block parent ID is an epoch2x block: {}",
1393
            &self.parent_tenure_id
×
1394
        );
1395

1396
        Ok(parent_tenure_header)
×
1397
    }
7,558✔
1398

1399
    // TODO: add tests from mutation testing results #4869
1400
    #[cfg_attr(test, mutants::skip)]
1401
    /// Load up the parent block info for mining.
1402
    /// If we can't find the parent in the DB but we expect one, return Err(ParentNotFound).
1403
    fn load_block_parent_info(
7,558✔
1404
        &self,
7,558✔
1405
        burn_db: &mut SortitionDB,
7,558✔
1406
        chain_state: &mut StacksChainState,
7,558✔
1407
    ) -> Result<ParentStacksBlockInfo, NakamotoNodeError> {
7,558✔
1408
        let stacks_tip_header = self.load_block_parent_header(burn_db, chain_state)?;
7,558✔
1409

1410
        debug!(
7,541✔
1411
            "Miner: stacks tip parent header is {} {stacks_tip_header:?}",
1412
            &stacks_tip_header.index_block_hash()
×
1413
        );
1414
        let miner_address = self
7,541✔
1415
            .keychain
7,541✔
1416
            .origin_address(self.config.is_mainnet())
7,541✔
1417
            .unwrap();
7,541✔
1418
        ParentStacksBlockInfo::lookup(
7,541✔
1419
            chain_state,
7,541✔
1420
            burn_db,
7,541✔
1421
            &self.burn_block,
7,541✔
1422
            miner_address,
7,541✔
1423
            &self.parent_tenure_id,
7,541✔
1424
            stacks_tip_header,
7,541✔
1425
            &self.reason,
7,541✔
1426
        )
1427
        .inspect_err(|e| {
7,541✔
1428
            if matches!(e, NakamotoNodeError::BurnchainTipChanged) {
×
1429
                self.globals.counters.bump_missed_tenures();
×
1430
            }
×
1431
        })
×
1432
    }
7,558✔
1433

1434
    /// Generate the VRF proof for the block we're going to build.
1435
    /// Returns Some(proof) if we could make the proof
1436
    /// Return None if we could not make the proof
1437
    fn make_vrf_proof(&mut self) -> Option<VRFProof> {
7,383✔
1438
        // if we're a mock miner, then make sure that the keychain has a keypair for the mocked VRF
1439
        // key
1440
        let vrf_proof = if self.config.get_node_config(false).mock_mining {
7,383✔
1441
            self.keychain.generate_proof(
158✔
1442
                VRF_MOCK_MINER_KEY,
1443
                self.burn_election_block.sortition_hash.as_bytes(),
158✔
1444
            )
1445
        } else {
1446
            self.keychain.generate_proof(
7,225✔
1447
                self.registered_key.target_block_height,
7,225✔
1448
                self.burn_election_block.sortition_hash.as_bytes(),
7,225✔
1449
            )
1450
        };
1451

1452
        let Some(vrf_proof) = vrf_proof else {
7,383✔
1453
            error!(
×
1454
                "Unable to generate VRF proof, will be unable to mine";
1455
                "burn_block_sortition_hash" => %self.burn_election_block.sortition_hash,
1456
                "burn_block_block_height" => %self.burn_block.block_height,
1457
                "burn_block_hash" => %self.burn_block.burn_header_hash,
1458
                "vrf_pubkey" => &self.registered_key.vrf_public_key.to_hex()
×
1459
            );
1460
            return None;
×
1461
        };
1462

1463
        debug!(
7,383✔
1464
            "Generated VRF Proof: {} over {} ({},{}) with key {}",
1465
            vrf_proof.to_hex(),
×
1466
            &self.burn_election_block.sortition_hash,
×
1467
            &self.burn_block.block_height,
×
1468
            &self.burn_block.burn_header_hash,
×
1469
            &self.registered_key.vrf_public_key.to_hex()
×
1470
        );
1471
        Some(vrf_proof)
7,383✔
1472
    }
7,383✔
1473

1474
    fn validate_timestamp_info(
9,848✔
1475
        &self,
9,848✔
1476
        current_timestamp_secs: u64,
9,848✔
1477
        stacks_parent_header: &StacksHeaderInfo,
9,848✔
1478
    ) -> bool {
9,848✔
1479
        let parent_timestamp = match stacks_parent_header.anchored_header.as_stacks_nakamoto() {
9,848✔
1480
            Some(naka_header) => naka_header.timestamp,
9,444✔
1481
            None => stacks_parent_header.burn_header_timestamp,
404✔
1482
        };
1483
        let time_since_parent_ms = current_timestamp_secs.saturating_sub(parent_timestamp) * 1000;
9,848✔
1484
        if time_since_parent_ms < self.config.miner.min_time_between_blocks_ms {
9,848✔
1485
            debug!("Parent block mined {time_since_parent_ms} ms ago. Required minimum gap between blocks is {} ms", self.config.miner.min_time_between_blocks_ms;
235✔
1486
                "current_timestamp" => current_timestamp_secs,
×
1487
                "parent_block_id" => %stacks_parent_header.index_block_hash(),
×
1488
                "parent_block_height" => stacks_parent_header.stacks_block_height,
×
1489
                "parent_block_timestamp" => stacks_parent_header.burn_header_timestamp,
×
1490
            );
1491
            false
235✔
1492
        } else {
1493
            true
9,613✔
1494
        }
1495
    }
9,848✔
1496

1497
    /// Check that the provided block is not mined too quickly after the parent block.
1498
    /// This is to ensure that the signers do not reject the block due to the block being mined within the same second as the parent block.
1499
    fn validate_timestamp(&self, x: &NakamotoBlock) -> Result<bool, NakamotoNodeError> {
2,473✔
1500
        let chain_state = neon_node::open_chainstate_with_faults(&self.config)
2,473✔
1501
            .expect("FATAL: could not open chainstate DB");
2,473✔
1502
        let stacks_parent_header =
2,473✔
1503
            NakamotoChainState::get_block_header(chain_state.db(), &x.header.parent_block_id)
2,473✔
1504
                .map_err(|e| {
2,473✔
1505
                    error!(
×
1506
                        "Could not query header info for parent block ID {}: {e:?}",
1507
                        &x.header.parent_block_id
×
1508
                    );
1509
                    NakamotoNodeError::ParentNotFound
×
1510
                })?
×
1511
                .ok_or_else(|| {
2,473✔
1512
                    error!(
×
1513
                        "No header info for parent block ID {}",
1514
                        &x.header.parent_block_id
×
1515
                    );
1516
                    NakamotoNodeError::ParentNotFound
×
1517
                })?;
×
1518
        Ok(self.validate_timestamp_info(x.header.timestamp, &stacks_parent_header))
2,473✔
1519
    }
2,473✔
1520

1521
    // TODO: add tests from mutation testing results #4869
1522
    #[cfg_attr(test, mutants::skip)]
1523
    /// Try to mine a Stacks block by assembling one from mempool transactions and sending a
1524
    /// burnchain block-commit transaction.  If we succeed, then return the assembled block.
1525
    fn mine_block(
9,283✔
1526
        &mut self,
9,283✔
1527
        coordinator: &mut SignerCoordinator,
9,283✔
1528
    ) -> Result<NakamotoBlock, NakamotoNodeError> {
9,283✔
1529
        debug!("block miner thread ID is {:?}", thread::current().id());
9,283✔
1530
        info!("Miner: Mining block");
9,283✔
1531

1532
        let burn_db_path = self.config.get_burn_db_file_path();
9,283✔
1533
        let reward_set = self.load_signer_set()?;
9,283✔
1534

1535
        // NOTE: read-write access is needed in order to be able to query the recipient set.
1536
        // This is an artifact of the way the MARF is built (see #1449)
1537
        let mut burn_db = SortitionDB::open(
9,283✔
1538
            &burn_db_path,
9,283✔
1539
            true,
1540
            self.burnchain.pox_constants.clone(),
9,283✔
1541
            Some(self.config.node.get_marf_opts()),
9,283✔
1542
        )
1543
        .expect("FATAL: could not open sortition DB");
9,283✔
1544

1545
        let mut chain_state = neon_node::open_chainstate_with_faults(&self.config)
9,283✔
1546
            .expect("FATAL: could not open chainstate DB");
9,283✔
1547

1548
        self.check_burn_tip_changed(&burn_db)?;
9,283✔
1549
        if Self::fault_injection_block_mining_skip() {
9,181✔
1550
            return Err(ChainstateError::MinerAborted.into());
1,795✔
1551
        }
7,386✔
1552
        neon_node::fault_injection_long_tenure();
7,386✔
1553

1554
        let mut mem_pool = self
7,386✔
1555
            .config
7,386✔
1556
            .connect_mempool_db()
7,386✔
1557
            .expect("Database failure opening mempool");
7,386✔
1558

1559
        // Blacklist permanently excluded transactions reported by signers
1560
        if !self.permanently_excluded_txids.is_empty() {
7,386✔
1561
            let txids: Vec<Txid> = self.permanently_excluded_txids.drain().collect();
4✔
1562
            info!("Miner: blacklisting permanently excluded transaction(s) from mempool";
4✔
1563
                "count" => txids.len(),
1✔
1564
            );
1565
            if let Err(e) = mem_pool.drop_and_blacklist_txs(&txids) {
4✔
1566
                warn!("Miner: failed to blacklist permanently excluded transactions: {e:?}");
3✔
1567
            }
1✔
1568
        }
7,382✔
1569

1570
        let target_epoch_id =
7,386✔
1571
            SortitionDB::get_stacks_epoch(burn_db.conn(), self.burn_block.block_height + 1)
7,386✔
1572
                .map_err(|_| NakamotoNodeError::SnapshotNotFoundForChainTip)?
7,386✔
1573
                .expect("FATAL: no epoch defined")
7,386✔
1574
                .epoch_id;
1575
        let mut parent_block_info = self.load_block_parent_info(&mut burn_db, &mut chain_state)?;
7,386✔
1576
        let vrf_proof = self
7,386✔
1577
            .make_vrf_proof()
7,386✔
1578
            .ok_or_else(|| NakamotoNodeError::BadVrfConstruction)?;
7,386✔
1579

1580
        if self.config.node.mock_mining {
7,386✔
1581
            if let Some((last_block_consensus_hash, _)) = &self.last_block_mined {
158✔
1582
                // If we're mock mining, we need to manipulate the `last_block_mined`
1583
                // to match what it should be based on the actual chainstate.
1584
                if last_block_consensus_hash
60✔
1585
                    == &parent_block_info.stacks_parent_header.consensus_hash
60✔
1586
                {
52✔
1587
                    // If the parent block is in the same tenure, then we should
52✔
1588
                    // pretend that we mined it.
52✔
1589
                    self.last_block_mined = Some((
52✔
1590
                        parent_block_info
52✔
1591
                            .stacks_parent_header
52✔
1592
                            .consensus_hash
52✔
1593
                            .clone(),
52✔
1594
                        parent_block_info
52✔
1595
                            .stacks_parent_header
52✔
1596
                            .anchored_header
52✔
1597
                            .block_hash(),
52✔
1598
                    ));
52✔
1599
                } else {
52✔
1600
                    // If the parent block is not in the same tenure, then we
8✔
1601
                    // should act as though we haven't mined anything yet.
8✔
1602
                    self.last_block_mined = None;
8✔
1603
                }
8✔
1604
            }
98✔
1605
        }
7,228✔
1606

1607
        if self.last_block_mined.is_none() && parent_block_info.parent_tenure.is_none() {
7,386✔
1608
            if self.config.node.mock_mining {
94✔
1609
                info!("Mock miner will follow canonical tip within an ongoing tenure; no parent tenure info loaded yet");
93✔
1610
            } else {
1611
                warn!(
1✔
1612
                    "Miner should be starting a new tenure, but failed to load parent tenure info"
1613
                );
1614
                return Err(NakamotoNodeError::ParentNotFound);
1✔
1615
            }
1616
        };
7,292✔
1617

1618
        // create our coinbase if this is the first block we've mined this tenure
1619
        let tenure_start_info = self.make_tenure_start_info(
7,385✔
1620
            &chain_state,
7,385✔
1621
            &parent_block_info,
7,385✔
1622
            vrf_proof,
7,385✔
1623
            target_epoch_id,
7,385✔
1624
            coordinator,
7,385✔
1625
        )?;
×
1626

1627
        parent_block_info.stacks_parent_header.microblock_tail = None;
7,385✔
1628

1629
        // Length of the per-block `pox_treatment` BitVec.
1630
        // Must be `> 0`:
1631
        //   the BitVec codec rejects zero-length bitvecs at deserialization.
1632
        let signer_bitvec_len = reward_set.pox_treatment_bitvec_len();
7,385✔
1633

1634
        if !self.validate_timestamp_info(
7,385✔
1635
            get_epoch_time_secs(),
7,385✔
1636
            &parent_block_info.stacks_parent_header,
7,385✔
1637
        ) {
7,385✔
1638
            // treat a too-soon-to-mine block as an interrupt: this will let the caller sleep and then re-evaluate
1639
            //  all the pre-mining checks (burnchain tip changes, signal interrupts, etc.)
1640
            return Err(ChainstateError::MinerAborted.into());
245✔
1641
        }
7,140✔
1642

1643
        // If we attempt to build a block, we should reset the nonce cache.
1644
        // In the special case where no transactions are found, this flag will
1645
        // be reset to false.
1646
        self.reset_mempool_caches = true;
7,140✔
1647

1648
        let replay_transactions = if self.config.miner.replay_transactions {
7,140✔
1649
            coordinator
546✔
1650
                .get_signer_global_state()
546✔
1651
                .map(|state| state.tx_replay_set.unwrap_or_default())
546✔
1652
                .unwrap_or_default()
546✔
1653
        } else {
1654
            vec![]
6,594✔
1655
        };
1656
        // build the block itself
1657
        let mining_burn_handle = burn_db
7,140✔
1658
            .index_handle_at_ch(&self.burn_block.consensus_hash)
7,140✔
1659
            .map_err(|_| NakamotoNodeError::UnexpectedChainState)?;
7,140✔
1660
        if let Some(parent_burn_view) = &parent_block_info.stacks_parent_header.burn_view {
7,140✔
1661
            if !mining_burn_handle.processed_block(parent_burn_view)? {
6,947✔
1662
                error!(
×
1663
                    "Cannot mine block: calculated parent has burn view which is incompatible with the canonical burn fork";
1664
                    "parent_burn_view" => %parent_burn_view,
1665
                    "my_burn_view" => %self.burn_block.consensus_hash,
1666
                );
1667
                return Err(NakamotoNodeError::BurnchainTipChanged);
×
1668
            }
6,947✔
1669
        }
193✔
1670

1671
        let mut block_metadata = NakamotoBlockBuilder::build_nakamoto_block(
7,140✔
1672
            &chain_state,
7,140✔
1673
            &mining_burn_handle,
7,140✔
1674
            &mut mem_pool,
7,140✔
1675
            &parent_block_info.stacks_parent_header,
7,140✔
1676
            &self.burn_election_block.consensus_hash,
7,140✔
1677
            self.burn_block.total_burn,
7,140✔
1678
            tenure_start_info,
7,140✔
1679
            {
1680
                let mut settings = self
7,140✔
1681
                    .config
7,140✔
1682
                    .make_nakamoto_block_builder_settings(self.globals.get_miner_status());
7,140✔
1683
                if !self.temporarily_excluded_txids.is_empty() {
7,140✔
1684
                    info!("Miner: excluding signer-rejected transaction(s) from block building";
1✔
1685
                        "count" => self.temporarily_excluded_txids.len(),
1✔
1686
                    );
1687
                    settings.temporarily_excluded_txids = self.temporarily_excluded_txids.clone();
1✔
1688
                }
7,139✔
1689
                settings
7,140✔
1690
            },
1691
            // we'll invoke the event dispatcher ourselves so that it calculates the
1692
            //  correct signer_signature_hash for `process_mined_nakamoto_block_event`
1693
            Some(&self.event_dispatcher),
7,140✔
1694
            signer_bitvec_len,
7,140✔
1695
            &replay_transactions,
7,140✔
1696
        )
1697
        .map_err(|e| {
7,140✔
1698
            if !matches!(
×
1699
                e,
4,663✔
1700
                ChainstateError::MinerAborted | ChainstateError::NoTransactionsToMine
1701
            ) {
1702
                error!("Relayer: Failure mining anchored block: {e}");
×
1703
            }
4,663✔
1704
            e
4,663✔
1705
        })?;
4,663✔
1706

1707
        if block_metadata.block.txs.is_empty() {
2,477✔
1708
            return Err(ChainstateError::NoTransactionsToMine.into());
×
1709
        }
2,477✔
1710
        let mining_key = self.keychain.get_nakamoto_sk();
2,477✔
1711
        let miner_signature = mining_key
2,477✔
1712
            .sign(
2,477✔
1713
                block_metadata
2,477✔
1714
                    .block
2,477✔
1715
                    .header
2,477✔
1716
                    .miner_signature_hash()
2,477✔
1717
                    .as_bytes(),
2,477✔
1718
            )
1719
            .map_err(NakamotoNodeError::MinerSignatureError)?;
2,477✔
1720
        block_metadata.block.header.miner_signature = miner_signature;
2,477✔
1721

1722
        info!(
2,477✔
1723
            "Miner: Assembled block #{} for signer set proposal: {}, with {} txs",
1724
            block_metadata.block.header.chain_length,
1725
            block_metadata.block.header.block_hash(),
2,473✔
1726
            block_metadata.block.txs.len();
2,473✔
1727
            "signer_signature_hash" => %block_metadata.block.header.signer_signature_hash(),
2,473✔
1728
            "consensus_hash" => %block_metadata.block.header.consensus_hash,
1729
            "parent_block_id" => %block_metadata.block.header.parent_block_id,
1730
            "timestamp" => block_metadata.block.header.timestamp,
2,473✔
1731
        );
1732

1733
        self.event_dispatcher.process_mined_nakamoto_block_event(
2,477✔
1734
            self.burn_block.block_height,
2,477✔
1735
            &block_metadata.block,
2,477✔
1736
            block_metadata.tenure_size,
2,477✔
1737
            &block_metadata.tenure_consumed,
2,477✔
1738
            block_metadata.tx_events,
2,477✔
1739
        );
1740

1741
        self.tenure_cost = block_metadata.tenure_consumed;
2,477✔
1742
        self.tenure_budget = block_metadata.tenure_budget;
2,477✔
1743

1744
        // last chance -- confirm that the stacks tip is unchanged (since it could have taken long
1745
        // enough to build this block that another block could have arrived), and confirm that all
1746
        // Stacks blocks with heights higher than the canonical tip are processed.
1747
        self.check_burn_tip_changed(&burn_db)?;
2,477✔
1748
        Ok(block_metadata.block)
2,477✔
1749
    }
9,283✔
1750

1751
    fn find_highest_known_block_in_my_tenure(
7,558✔
1752
        &self,
7,558✔
1753
        burn_db: &SortitionDB,
7,558✔
1754
        chainstate: &StacksChainState,
7,558✔
1755
    ) -> Result<Option<StacksHeaderInfo>, NakamotoNodeError> {
7,558✔
1756
        NakamotoChainState::find_highest_known_block_header_in_tenure(
7,558✔
1757
            chainstate,
7,558✔
1758
            burn_db,
7,558✔
1759
            &self.burn_election_block.consensus_hash,
7,558✔
1760
        )
1761
        .map_err(NakamotoNodeError::from)
7,558✔
1762
    }
7,558✔
1763

1764
    fn should_full_tenure_extend(
5,797✔
1765
        &self,
5,797✔
1766
        coordinator: &mut SignerCoordinator,
5,797✔
1767
    ) -> Result<bool, NakamotoNodeError> {
5,797✔
1768
        if self.last_block_mined.is_none() {
5,797✔
1769
            // if we haven't mined blocks yet, no tenure extends needed
1770
            return Ok(false);
×
1771
        }
5,797✔
1772
        let is_replay = self.config.miner.replay_transactions
5,797✔
1773
            && coordinator
412✔
1774
                .get_signer_global_state()
412✔
1775
                .map(|state| state.tx_replay_set.is_some())
412✔
1776
                .unwrap_or(false);
412✔
1777
        if is_replay {
5,797✔
1778
            // we're in replay, we should always TenureExtend
1779
            info!("Tenure extend: In replay, always extending tenure");
18✔
1780
            return Ok(true);
18✔
1781
        }
5,779✔
1782

1783
        // Do not extend if we have spent a threshold amount of the
1784
        // budget, since it is not necessary.
1785
        let usage = self
5,779✔
1786
            .tenure_budget
5,779✔
1787
            .proportion_largest_dimension(&self.tenure_cost);
5,779✔
1788
        if usage < self.config.miner.tenure_extend_cost_threshold {
5,779✔
1789
            return Ok(false);
5,496✔
1790
        }
283✔
1791

1792
        let tenure_extend_timestamp = coordinator.get_tenure_extend_timestamp();
283✔
1793
        if get_epoch_time_secs() <= tenure_extend_timestamp
283✔
1794
            && self.tenure_change_time.elapsed() <= self.config.miner.tenure_timeout
191✔
1795
        {
1796
            return Ok(false);
184✔
1797
        }
99✔
1798

1799
        info!("Miner: Time-based tenure extend";
99✔
1800
              "current_timestamp" => get_epoch_time_secs(),
99✔
1801
              "tenure_extend_timestamp" => tenure_extend_timestamp,
99✔
1802
              "tenure_change_time_elapsed" => self.tenure_change_time.elapsed().as_secs(),
99✔
1803
              "tenure_timeout_secs" => self.config.miner.tenure_timeout.as_secs(),
99✔
1804
        );
1805
        Ok(true)
99✔
1806
    }
5,797✔
1807

1808
    fn should_read_count_extend<C: ReadCountCheck>(
5,680✔
1809
        &self,
5,680✔
1810
        coordinator: &C,
5,680✔
1811
    ) -> Result<bool, NakamotoNodeError> {
5,680✔
1812
        if self.last_block_mined.is_none() {
5,680✔
1813
            // if we haven't mined blocks yet, no tenure extends needed
1814
            return Ok(false);
×
1815
        }
5,680✔
1816

1817
        // Do not extend if we have spent a threshold amount of the
1818
        // read-count budget, since it is not necessary.
1819
        let usage =
5,680✔
1820
            self.tenure_cost.read_count / std::cmp::max(1, self.tenure_budget.read_count / 100);
5,680✔
1821

1822
        if usage < self.config.miner.read_count_extend_cost_threshold {
5,680✔
1823
            info!(
5,628✔
1824
                "Miner: not read-count extending because threshold not reached";
1825
                "threshold" => self.config.miner.read_count_extend_cost_threshold,
5,628✔
1826
                "usage" => usage
5,628✔
1827
            );
1828
            return Ok(false);
5,628✔
1829
        }
52✔
1830

1831
        let tenure_extend_timestamp = coordinator.get_read_count_extend_timestamp();
52✔
1832
        if get_epoch_time_secs() <= tenure_extend_timestamp {
52✔
1833
            info!(
47✔
1834
                "Miner: not read-count extending because idle timestamp not reached";
1835
                "now" => get_epoch_time_secs(),
47✔
1836
                "extend_ts" => tenure_extend_timestamp,
47✔
1837
            );
1838
            return Ok(false);
47✔
1839
        }
5✔
1840

1841
        info!("Miner: Time-based read-count extend";
5✔
1842
              "current_timestamp" => get_epoch_time_secs(),
5✔
1843
              "tenure_extend_timestamp" => tenure_extend_timestamp,
5✔
1844
              "tenure_change_time_elapsed" => self.tenure_change_time.elapsed().as_secs(),
5✔
1845
              "tenure_timeout_secs" => self.config.miner.tenure_timeout.as_secs(),
5✔
1846
        );
1847
        Ok(true)
5✔
1848
    }
5,680✔
1849

1850
    #[cfg_attr(test, mutants::skip)]
1851
    /// Create the tenure start info for the block we're going to build
1852
    fn make_tenure_start_info(
7,375✔
1853
        &mut self,
7,375✔
1854
        chainstate: &StacksChainState,
7,375✔
1855
        parent_block_info: &ParentStacksBlockInfo,
7,375✔
1856
        vrf_proof: VRFProof,
7,375✔
1857
        target_epoch_id: StacksEpochId,
7,375✔
1858
        coordinator: &mut SignerCoordinator,
7,375✔
1859
    ) -> Result<NakamotoTenureInfo, NakamotoNodeError> {
7,375✔
1860
        let current_miner_nonce = parent_block_info.coinbase_nonce;
7,375✔
1861
        let parent_tenure_info = match &parent_block_info.parent_tenure {
7,375✔
1862
            Some(info) => info.clone(),
2,089✔
1863
            None => {
1864
                // We may be able to extend the current tenure
1865
                if self.last_block_mined.is_none() {
5,286✔
1866
                    debug!("Miner: No parent tenure and no last block mined");
93✔
1867
                    return Ok(NakamotoTenureInfo {
93✔
1868
                        coinbase_tx: None,
93✔
1869
                        tenure_change_tx: None,
93✔
1870
                    });
93✔
1871
                }
5,193✔
1872
                ParentTenureInfo {
5,193✔
1873
                    parent_tenure_blocks: self.mined_blocks,
5,193✔
1874
                    parent_tenure_consensus_hash: self.burn_election_block.consensus_hash.clone(),
5,193✔
1875
                }
5,193✔
1876
            }
1877
        };
1878
        if self.last_block_mined.is_some() {
7,282✔
1879
            // if we've already mined blocks, we only issue tenure_change_txs in the case of an extend,
1880
            //  so check if that's necessary and otherwise return None.
1881
            if self.should_full_tenure_extend(coordinator)? {
5,797✔
1882
                self.set_full_tenure_extend();
117✔
1883
            } else if self.should_read_count_extend(coordinator)? {
5,714✔
1884
                self.set_read_count_tenure_extend();
5✔
1885
            } else {
5✔
1886
                return Ok(NakamotoTenureInfo {
5,675✔
1887
                    coinbase_tx: None,
5,675✔
1888
                    tenure_change_tx: None,
5,675✔
1889
                });
5,675✔
1890
            }
1891
        }
1,485✔
1892

1893
        let parent_block_id = parent_block_info.stacks_parent_header.index_block_hash();
1,607✔
1894
        let mut payload = TenureChangePayload {
1,607✔
1895
            tenure_consensus_hash: self.burn_election_block.consensus_hash.clone(),
1,607✔
1896
            prev_tenure_consensus_hash: parent_tenure_info.parent_tenure_consensus_hash.clone(),
1,607✔
1897
            burn_view_consensus_hash: self.burn_election_block.consensus_hash.clone(),
1,607✔
1898
            previous_tenure_end: parent_block_id.clone(),
1,607✔
1899
            previous_tenure_blocks: u32::try_from(parent_tenure_info.parent_tenure_blocks)
1,607✔
1900
                .expect("FATAL: more than u32 blocks in a tenure"),
1,607✔
1901
            cause: TenureChangeCause::BlockFound,
1,607✔
1902
            pubkey_hash: self.keychain.get_nakamoto_pkh(),
1,607✔
1903
        };
1,607✔
1904

1905
        let (tenure_change_tx, coinbase_tx) = match &self.reason {
1,607✔
1906
            MinerReason::BlockFound { .. } => {
1907
                let tenure_change_tx = self.generate_tenure_change_tx(current_miner_nonce, payload);
1,346✔
1908
                let coinbase_tx =
1,346✔
1909
                    self.generate_coinbase_tx(current_miner_nonce + 1, target_epoch_id, vrf_proof);
1,346✔
1910
                (Some(tenure_change_tx), Some(coinbase_tx))
1,346✔
1911
            }
1912
            MinerReason::Extended {
1913
                burn_view_consensus_hash,
256✔
1914
            } => {
1915
                let num_blocks_so_far = NakamotoChainState::get_nakamoto_tenure_length(
256✔
1916
                    chainstate.db(),
256✔
1917
                    &parent_block_id,
256✔
1918
                )
1919
                .map_err(NakamotoNodeError::MiningFailure)?;
256✔
1920
                info!("Miner: Extending tenure";
256✔
1921
                      "burn_view_consensus_hash" => %burn_view_consensus_hash,
1922
                      "parent_block_id" => %parent_block_id,
1923
                      "num_blocks_so_far" => num_blocks_so_far,
256✔
1924
                );
1925

1926
                // NOTE: this switches payload.cause to TenureChangeCause::Extend
1927
                payload = payload.extend(
256✔
1928
                    burn_view_consensus_hash.clone(),
256✔
1929
                    parent_block_id,
256✔
1930
                    num_blocks_so_far,
256✔
1931
                );
256✔
1932
                let tenure_change_tx = self.generate_tenure_change_tx(current_miner_nonce, payload);
256✔
1933
                (Some(tenure_change_tx), None)
256✔
1934
            }
1935
            MinerReason::ReadCountExtend {
1936
                burn_view_consensus_hash,
5✔
1937
            } => {
1938
                let num_blocks_so_far = NakamotoChainState::get_nakamoto_tenure_length(
5✔
1939
                    chainstate.db(),
5✔
1940
                    &parent_block_id,
5✔
1941
                )
1942
                .map_err(NakamotoNodeError::MiningFailure)?;
5✔
1943
                info!("Miner: Extending read-count";
5✔
1944
                      "burn_view_consensus_hash" => %burn_view_consensus_hash,
1945
                      "parent_block_id" => %parent_block_id,
1946
                      "num_blocks_so_far" => num_blocks_so_far,
5✔
1947
                );
1948

1949
                // NOTE: this switches payload.cause to TenureChangeCause::Extend
1950
                payload = payload.extend_with_cause(
5✔
1951
                    burn_view_consensus_hash.clone(),
5✔
1952
                    parent_block_id,
5✔
1953
                    num_blocks_so_far,
5✔
1954
                    TenureChangeCause::ExtendedReadCount,
5✔
1955
                );
5✔
1956
                let tenure_change_tx = self.generate_tenure_change_tx(current_miner_nonce, payload);
5✔
1957
                (Some(tenure_change_tx), None)
5✔
1958
            }
1959
        };
1960

1961
        debug!(
1,607✔
1962
            "make_tenure_start_info: reason = {:?}, burn_view = {:?}, tenure_change_tx = {:?}",
1963
            &self.reason, &self.burn_block.consensus_hash, &tenure_change_tx
×
1964
        );
1965

1966
        Ok(NakamotoTenureInfo {
1,607✔
1967
            coinbase_tx,
1,607✔
1968
            tenure_change_tx,
1,607✔
1969
        })
1,607✔
1970
    }
7,375✔
1971

1972
    /// Check if the tenure needs to change -- if so, return a BurnchainTipChanged error
1973
    /// The tenure should change if there is a new burnchain tip with a valid sortition,
1974
    /// or if the stacks chain state's burn view has advanced beyond our burn view.
1975
    fn check_burn_tip_changed(&self, sortdb: &SortitionDB) -> Result<(), NakamotoNodeError> {
42,226✔
1976
        let cur_burn_chain_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
42,226✔
1977
            .expect("FATAL: failed to query sortition DB for canonical burn chain tip");
42,226✔
1978

1979
        if cur_burn_chain_tip.consensus_hash != self.burn_tip_at_start {
42,226✔
1980
            info!("Miner: Cancel block assembly; burnchain tip has changed";
1,064✔
1981
                "new_tip" => %cur_burn_chain_tip.consensus_hash,
1982
                "local_tip" => %self.burn_tip_at_start);
1983
            self.globals.counters.bump_missed_tenures();
1,064✔
1984
            Err(NakamotoNodeError::BurnchainTipChanged)
1,064✔
1985
        } else {
1986
            Ok(())
41,162✔
1987
        }
1988
    }
42,226✔
1989

1990
    /// Set up the miner to try to issue a full tenure extend
1991
    fn set_full_tenure_extend(&mut self) {
117✔
1992
        self.tenure_change_time = Instant::now();
117✔
1993
        self.reason = MinerReason::Extended {
117✔
1994
            burn_view_consensus_hash: self.burn_block.consensus_hash.clone(),
117✔
1995
        };
117✔
1996
        self.mined_blocks = 0;
117✔
1997
    }
117✔
1998

1999
    /// Set up the miner to try to issue a full tenure extend
2000
    fn set_read_count_tenure_extend(&mut self) {
5✔
2001
        self.tenure_change_time = Instant::now();
5✔
2002
        self.reason = MinerReason::ReadCountExtend {
5✔
2003
            burn_view_consensus_hash: self.burn_block.consensus_hash.clone(),
5✔
2004
        };
5✔
2005
        self.mined_blocks = 0;
5✔
2006
    }
5✔
2007
}
2008

2009
impl ParentStacksBlockInfo {
2010
    // TODO: add tests from mutation testing results #4869
2011
    #[cfg_attr(test, mutants::skip)]
2012
    /// Determine where in the set of forks to attempt to mine the next anchored block.
2013
    /// `parent_tenure_id` and `stacks_tip_header` identify the parent block on top of which to mine.
2014
    /// `check_burn_block` identifies what we believe to be the burn chain's sortition history tip.
2015
    /// This is used to mitigate (but not eliminate) a TOCTTOU issue with mining: the caller's
2016
    /// conception of the sortition history tip may have become stale by the time they call this
2017
    /// method, in which case, mining should *not* happen (since the block will be invalid).
2018
    pub fn lookup(
7,541✔
2019
        chain_state: &mut StacksChainState,
7,541✔
2020
        burn_db: &mut SortitionDB,
7,541✔
2021
        check_burn_block: &BlockSnapshot,
7,541✔
2022
        miner_address: StacksAddress,
7,541✔
2023
        parent_tenure_id: &StacksBlockId,
7,541✔
2024
        stacks_tip_header: StacksHeaderInfo,
7,541✔
2025
        reason: &MinerReason,
7,541✔
2026
    ) -> Result<ParentStacksBlockInfo, NakamotoNodeError> {
7,541✔
2027
        // the stacks block I'm mining off of's burn header hash and vtxindex:
2028
        let parent_snapshot = SortitionDB::get_block_snapshot_consensus(
7,541✔
2029
            burn_db.conn(),
7,541✔
2030
            &stacks_tip_header.consensus_hash,
7,541✔
2031
        )
2032
        .expect("Failed to look up block's parent snapshot")
7,541✔
2033
        .expect("Failed to look up block's parent snapshot");
7,541✔
2034

2035
        // don't mine off of an old burnchain block, unless we're late
2036
        let burn_chain_tip = SortitionDB::get_canonical_burn_chain_tip(burn_db.conn())
7,541✔
2037
            .expect("FATAL: failed to query sortition DB for canonical burn chain tip");
7,541✔
2038

2039
        // if we're mining a tenure that we were late to initialize, allow the burn tipped
2040
        //  to be slightly stale
2041
        if !reason.is_late_block()
7,541✔
2042
            && burn_chain_tip.consensus_hash != check_burn_block.consensus_hash
7,515✔
2043
        {
2044
            info!(
×
2045
                "New canonical burn chain tip detected. Will not try to mine.";
2046
                "new_consensus_hash" => %burn_chain_tip.consensus_hash,
2047
                "old_consensus_hash" => %check_burn_block.consensus_hash,
2048
                "new_burn_height" => burn_chain_tip.block_height,
×
2049
                "old_burn_height" => check_burn_block.block_height
×
2050
            );
2051
            return Err(NakamotoNodeError::BurnchainTipChanged);
×
2052
        }
7,541✔
2053

2054
        let Ok(Some(parent_tenure_header)) =
7,541✔
2055
            NakamotoChainState::get_block_header(chain_state.db(), parent_tenure_id)
7,541✔
2056
        else {
2057
            warn!("Failed loading parent tenure ID"; "parent_tenure_id" => %parent_tenure_id);
×
2058
            return Err(NakamotoNodeError::ParentNotFound);
×
2059
        };
2060

2061
        // check if we're mining a first tenure block (by checking if our parent block is in the tenure of parent_tenure_id)
2062
        //  and if so, figure out how many blocks there were in the parent tenure
2063
        let parent_tenure_info = if stacks_tip_header.consensus_hash
7,541✔
2064
            == parent_tenure_header.consensus_hash
7,541✔
2065
        {
2066
            // in the same tenure
2067
            let parent_tenure_blocks = if parent_tenure_header
2,102✔
2068
                .anchored_header
2,102✔
2069
                .as_stacks_nakamoto()
2,102✔
2070
                .is_some()
2,102✔
2071
            {
2072
                let Ok(Some(last_parent_tenure_header)) =
1,888✔
2073
                    NakamotoChainState::get_highest_block_header_in_tenure(
1,888✔
2074
                        &mut chain_state.index_conn(),
1,888✔
2075
                        &stacks_tip_header.index_block_hash(),
1,888✔
2076
                        &parent_tenure_header.consensus_hash,
1,888✔
2077
                    )
2078
                else {
2079
                    warn!("Failed loading last block of parent tenure"; "parent_tenure_id" => %parent_tenure_id);
×
2080
                    return Err(NakamotoNodeError::ParentNotFound);
×
2081
                };
2082
                // the last known tenure block of our parent should be the stacks_tip. if not, error.
2083
                if stacks_tip_header.index_block_hash()
1,888✔
2084
                    != last_parent_tenure_header.index_block_hash()
1,888✔
2085
                {
2086
                    warn!("Last known tenure block of parent tenure should be the stacks tip";
×
2087
                          "stacks_tip_header" => %stacks_tip_header.index_block_hash(),
×
2088
                          "last_parent_tenure_header" => %last_parent_tenure_header.index_block_hash());
×
2089
                    return Err(NakamotoNodeError::NewParentDiscovered);
×
2090
                }
1,888✔
2091
                1 + last_parent_tenure_header.stacks_block_height
1,888✔
2092
                    - parent_tenure_header.stacks_block_height
1,888✔
2093
            } else {
2094
                1
214✔
2095
            };
2096
            let parent_tenure_consensus_hash = parent_tenure_header.consensus_hash.clone();
2,102✔
2097
            Some(ParentTenureInfo {
2,102✔
2098
                parent_tenure_blocks,
2,102✔
2099
                parent_tenure_consensus_hash,
2,102✔
2100
            })
2,102✔
2101
        } else {
2102
            None
5,439✔
2103
        };
2104

2105
        debug!(
7,541✔
2106
            "Looked up parent information";
2107
            "parent_tenure_id" => %parent_tenure_id,
2108
            "parent_tenure_consensus_hash" => %parent_tenure_header.consensus_hash,
2109
            "parent_tenure_burn_hash" => %parent_tenure_header.burn_header_hash,
2110
            "parent_tenure_burn_height" => parent_tenure_header.burn_header_height,
×
2111
            "mining_consensus_hash" => %check_burn_block.consensus_hash,
2112
            "mining_burn_hash" => %check_burn_block.burn_header_hash,
2113
            "mining_burn_height" => check_burn_block.block_height,
×
2114
            "stacks_tip_consensus_hash" => %parent_snapshot.consensus_hash,
2115
            "stacks_tip_burn_hash" => %parent_snapshot.burn_header_hash,
2116
            "stacks_tip_burn_height" => parent_snapshot.block_height,
×
2117
            "parent_tenure_info" => ?parent_tenure_info,
2118
            "stacks_tip_header.consensus_hash" => %stacks_tip_header.consensus_hash,
2119
            "parent_tenure_header.consensus_hash" => %parent_tenure_header.consensus_hash,
2120
            "reason" => %reason
2121
        );
2122

2123
        let coinbase_nonce = {
7,541✔
2124
            let principal = miner_address.into();
7,541✔
2125
            let account = chain_state
7,541✔
2126
                .with_read_only_clarity_tx(
7,541✔
2127
                    &burn_db
7,541✔
2128
                        .index_handle_at_block(chain_state, &stacks_tip_header.index_block_hash())
7,541✔
2129
                        .map_err(|_| NakamotoNodeError::UnexpectedChainState)?,
7,541✔
2130
                    &stacks_tip_header.index_block_hash(),
7,541✔
2131
                    |conn| StacksChainState::get_account(conn, &principal),
7,541✔
2132
                )
2133
                .unwrap_or_else(|| {
7,541✔
2134
                    panic!(
×
2135
                        "BUG: stacks tip block {} no longer exists after we queried it",
2136
                        &stacks_tip_header.index_block_hash()
×
2137
                    )
2138
                });
2139
            account.nonce
7,541✔
2140
        };
2141

2142
        Ok(ParentStacksBlockInfo {
7,541✔
2143
            stacks_parent_header: stacks_tip_header,
7,541✔
2144
            coinbase_nonce,
7,541✔
2145
            parent_tenure: parent_tenure_info,
7,541✔
2146
        })
7,541✔
2147
    }
7,541✔
2148
}
2149

2150
#[cfg(test)]
2151
impl ReadCountCheck for () {
2152
    fn get_read_count_extend_timestamp(&self) -> u64 {
×
2153
        // always allow the read count extend
2154
        0
×
2155
    }
×
2156
}
2157

2158
#[test]
2159
fn should_read_count_extend_units() {
×
2160
    let (sync_sender, _rcv_1) = std::sync::mpsc::sync_channel(1);
×
2161
    let (relay_sender, _rcv_2) = std::sync::mpsc::sync_channel(1);
×
2162
    let (_coord_rcv, coord_comms) =
×
2163
        stacks::chainstate::coordinator::comm::CoordinatorCommunication::instantiate();
×
2164
    let working_dir = tempdir().unwrap();
×
2165

2166
    let mut miner = BlockMinerThread {
×
2167
        config: Config::default(),
×
2168
        globals: Globals::new(
×
2169
            coord_comms,
×
2170
            Arc::new(std::sync::Mutex::new(
×
2171
                stacks::chainstate::stacks::miner::MinerStatus::make_ready(10),
×
2172
            )),
×
2173
            relay_sender,
×
2174
            crate::neon::Counters::new(),
×
2175
            crate::syncctl::PoxSyncWatchdogComms::new(Arc::new(AtomicBool::new(true))),
×
2176
            Arc::new(AtomicBool::new(true)),
×
2177
            0,
×
2178
            neon_node::LeaderKeyRegistrationState::Inactive,
×
2179
        ),
×
2180
        keychain: Keychain::default(vec![]),
×
2181
        burnchain: Burnchain::regtest("/dev/null"),
×
2182
        last_block_mined: Some((ConsensusHash([0; 20]), BlockHeaderHash([0; 32]))),
×
2183
        mined_blocks: 1,
×
2184
        tenure_cost: ExecutionCost::ZERO,
×
2185
        tenure_budget: ExecutionCost::ZERO,
×
2186
        registered_key: RegisteredKey {
×
2187
            target_block_height: 0,
×
2188
            block_height: 0,
×
2189
            op_vtxindex: 0,
×
2190
            vrf_public_key: stacks::util::vrf::VRFPublicKey::from_private(
×
2191
                &stacks::util::vrf::VRFPrivateKey::new(),
×
2192
            ),
×
2193
            memo: vec![],
×
2194
        },
×
2195
        burn_election_block: BlockSnapshot::empty(),
×
2196
        burn_block: BlockSnapshot::empty(),
×
2197
        parent_tenure_id: StacksBlockId([0; 32]),
×
2198
        event_dispatcher: EventDispatcher::new(working_dir.path().to_path_buf()),
×
2199
        reason: MinerReason::Extended {
×
2200
            burn_view_consensus_hash: ConsensusHash([0; 20]),
×
2201
        },
×
2202
        p2p_handle: NetworkHandle::new(sync_sender),
×
2203
        signer_set_cache: None,
×
2204
        tenure_change_time: Instant::now(),
×
2205
        burn_tip_at_start: ConsensusHash([0; 20]),
×
2206
        abort_flag: Arc::new(AtomicBool::new(false)),
×
2207
        reset_mempool_caches: false,
×
2208
        miner_db: MinerDB::open("/tmp/should_read_count_extend_units.db").unwrap(),
×
2209
        temporarily_excluded_txids: HashSet::new(),
×
2210
        permanently_excluded_txids: HashSet::new(),
×
2211
    };
×
2212
    miner.config.miner.read_count_extend_cost_threshold = 20;
×
2213

2214
    miner.tenure_cost = ExecutionCost {
×
2215
        write_length: 1000,
×
2216
        write_count: 1000,
×
2217
        read_length: 1000,
×
2218
        read_count: 199,
×
2219
        runtime: 1000,
×
2220
    };
×
2221

2222
    miner.tenure_budget = ExecutionCost {
×
2223
        write_length: 1000,
×
2224
        write_count: 1000,
×
2225
        read_length: 1000,
×
2226
        read_count: 1000,
×
2227
        runtime: 1000,
×
2228
    };
×
2229

2230
    assert_eq!(
×
2231
        miner.should_read_count_extend(&()).unwrap(),
×
2232
        false,
2233
        "When read_count is below the configured threshold, we shouldn't try to extend"
2234
    );
2235

2236
    miner.tenure_cost = ExecutionCost {
×
2237
        write_length: 1000,
×
2238
        write_count: 1000,
×
2239
        read_length: 1000,
×
2240
        read_count: 200,
×
2241
        runtime: 1000,
×
2242
    };
×
2243

2244
    miner.tenure_budget = ExecutionCost {
×
2245
        write_length: 1000,
×
2246
        write_count: 1000,
×
2247
        read_length: 1000,
×
2248
        read_count: 1000,
×
2249
        runtime: 1000,
×
2250
    };
×
2251

2252
    assert_eq!(
×
2253
        miner.should_read_count_extend(&()).unwrap(),
×
2254
        true,
2255
        "When read_count is at the configured threshhold, we should try to extend"
2256
    );
2257
}
×
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