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

stacks-network / stacks-core / 26250451051-1

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

Pull #7215

github

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

188844 of 220651 relevant lines covered (85.58%)

18975267.44 hits per line

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

87.42
/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,646✔
118
        std::thread::sleep(std::time::Duration::from_millis(10));
14,565✔
119
    }
14,565✔
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 {
14,006✔
214
        match self {
14,006✔
215
            Self::BlockFound { ref late } => *late,
12,427✔
216
            Self::Extended { .. } => false,
1,565✔
217
            Self::ReadCountExtend { .. } => false,
14✔
218
        }
219
    }
14,006✔
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,346✔
246
        match self {
2,346✔
247
            Self::BlockFound { .. } => MiningReason::BlockFound,
2,130✔
248
            Self::Extended { .. } => MiningReason::Extended,
211✔
249
            Self::ReadCountExtend { .. } => MiningReason::ReadCountExtend,
5✔
250
        }
251
    }
2,346✔
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 {
90✔
322
        SignerCoordinator::get_read_count_extend_timestamp(self)
90✔
323
    }
90✔
324
}
325

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

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

368
    #[cfg(test)]
369
    fn fault_injection_block_proposal_stall(new_block: &NakamotoBlock) {
2,390✔
370
        if TEST_BROADCAST_PROPOSAL_STALL.get().iter().any(|key| {
2,390✔
371
            key.verify(
31✔
372
                new_block.header.miner_signature_hash().as_bytes(),
31✔
373
                &new_block.header.miner_signature,
31✔
374
            )
375
            .unwrap_or_default()
31✔
376
        }) {
31✔
377
            warn!("Fault injection: Block proposal broadcast is stalled due to testing directive.";
19✔
378
                        "stacks_block_id" => %new_block.block_id(),
19✔
379
                        "stacks_block_hash" => %new_block.header.block_hash(),
19✔
380
                        "height" => new_block.header.chain_length,
19✔
381
                        "consensus_hash" => %new_block.header.consensus_hash
382
            );
383
            while TEST_BROADCAST_PROPOSAL_STALL.get().iter().any(|key| {
37,047✔
384
                key.verify(
37,031✔
385
                    new_block.header.miner_signature_hash().as_bytes(),
37,031✔
386
                    &new_block.header.miner_signature,
37,031✔
387
                )
388
                .unwrap_or_default()
37,031✔
389
            }) {
37,031✔
390
                std::thread::sleep(std::time::Duration::from_millis(10));
34,956✔
391
            }
34,956✔
392
            info!("Fault injection: Block proposal broadcast is no longer stalled due to testing directive.";
19✔
393
                    "block_id" => %new_block.block_id(),
18✔
394
                    "height" => new_block.header.chain_length,
18✔
395
                    "consensus_hash" => %new_block.header.consensus_hash
396
            );
397
        }
2,371✔
398
    }
2,390✔
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 {
8,981✔
405
        if TEST_MINE_SKIP.get() {
8,981✔
406
            warn!("Fault injection: Block mining is skipped due to testing directive.");
1,503✔
407
            true
1,503✔
408
        } else {
409
            false
7,478✔
410
        }
411
    }
8,981✔
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,241✔
420
        if TEST_BLOCK_ANNOUNCE_STALL.get() {
2,241✔
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() {
2,937✔
429
                std::thread::sleep(std::time::Duration::from_millis(10));
2,933✔
430
            }
2,933✔
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,237✔
437
    }
2,241✔
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,201✔
444
        TEST_P2P_BROADCAST_SKIP.get()
2,201✔
445
    }
2,201✔
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,196✔
454
        if TEST_P2P_BROADCAST_STALL.get() {
2,196✔
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.";
5✔
457
                      "stacks_block_id" => %new_block.block_id(),
5✔
458
                      "stacks_block_hash" => %new_block.header.block_hash(),
5✔
459
                      "height" => new_block.header.chain_length,
5✔
460
                      "consensus_hash" => %new_block.header.consensus_hash
461
            );
462
            while TEST_P2P_BROADCAST_STALL.get() {
773✔
463
                std::thread::sleep(std::time::Duration::from_millis(10));
768✔
464
            }
768✔
465
            info!("Fault injection: P2P block broadcast is no longer stalled due to testing directive.";
5✔
466
                  "block_id" => %new_block.block_id(),
5✔
467
                  "height" => new_block.header.chain_length,
5✔
468
                  "consensus_hash" => %new_block.header.consensus_hash
469
            );
470
        }
2,191✔
471
    }
2,196✔
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,201✔
478
        TEST_BLOCK_PUSH_SKIP.get()
2,201✔
479
    }
2,201✔
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,134✔
490
        if TEST_MINE_STALL.get() != TestMineStall::NotStalled {
9,134✔
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 {
107,348✔
495
                std::thread::sleep(std::time::Duration::from_millis(10));
107,269✔
496
            }
107,269✔
497
            warn!("Mining is no longer stalled due to testing directive. Continuing...");
79✔
498
        }
9,055✔
499
    }
9,134✔
500

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

504
    pub fn run_miner(
1,351✔
505
        mut self,
1,351✔
506
        prior_miner: Option<MinerStopHandle>,
1,351✔
507
    ) -> Result<(), NakamotoNodeError> {
1,351✔
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,351✔
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,351✔
520
            debug!(
1,123✔
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,123✔
526
        }
228✔
527
        let mut stackerdbs = StackerDBs::connect(&self.config.get_stacker_db_file_path(), true)?;
1,351✔
528
        let mut last_block_rejected = false;
1,351✔
529

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

544
        // Start the signer coordinator
545
        let mut coordinator = SignerCoordinator::new(
1,351✔
546
            self.event_dispatcher.stackerdb_channel.clone(),
1,351✔
547
            self.globals.should_keep_running.clone(),
1,351✔
548
            &reward_set,
1,351✔
549
            &self.burn_election_block,
1,351✔
550
            &self.burnchain,
1,351✔
551
            miner_privkey,
1,351✔
552
            &self.config,
1,351✔
553
            &self.burn_tip_at_start,
1,351✔
554
        )
555
        .map_err(|e| {
1,351✔
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,134✔
564
                &mut coordinator,
9,134✔
565
                &sortdb,
9,134✔
566
                &mut stackerdbs,
9,134✔
567
                &mut last_block_rejected,
9,134✔
568
                &reward_set,
9,134✔
569
            ) {
9,134✔
570
                // Before stopping this miner, shutdown the coordinator thread.
571
                coordinator.shutdown();
1,351✔
572
                return Err(e);
1,351✔
573
            }
7,783✔
574
        }
575
    }
1,351✔
576

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

591
        error!("Error while gathering signatures: {e:?}. Will try mining again in {pause_ms}.";
106✔
592
            "signer_signature_hash" => %new_block.header.signer_signature_hash(),
106✔
593
            "block_height" => new_block.header.chain_length,
106✔
594
            "consensus_hash" => %new_block.header.consensus_hash,
595
        );
596
        thread::sleep(Duration::from_millis(pause_ms));
106✔
597
        *last_block_rejected = true;
106✔
598
    }
106✔
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,134✔
608
        &mut self,
9,134✔
609
        coordinator: &mut SignerCoordinator,
9,134✔
610
        sortdb: &SortitionDB,
9,134✔
611
        stackerdbs: &mut StackerDBs,
9,134✔
612
        last_block_rejected: &mut bool,
9,134✔
613
        reward_set: &RewardSet,
9,134✔
614
    ) -> Result<(), NakamotoNodeError> {
9,134✔
615
        Self::fault_injection_miner_stall();
9,134✔
616
        let mut chain_state =
9,134✔
617
            neon_node::open_chainstate_with_faults(&self.config).map_err(|e| {
9,134✔
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,134✔
628
            info!("Miner: finished mining a late tenure");
17✔
629
            return Err(NakamotoNodeError::StacksTipChanged);
17✔
630
        }
9,117✔
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,117✔
635
            && !self.is_parent_processed(&mut chain_state)?
223✔
636
        {
637
            info!("Mock miner has not processed parent block yet, sleeping and trying again");
16✔
638
            thread::sleep(Duration::from_millis(ABORT_TRY_AGAIN_MS));
16✔
639
            return Ok(());
16✔
640
        }
9,098✔
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,098✔
646
            || self.config.miner.mempool_walk_strategy
4,059✔
647
                == MemPoolWalkStrategy::NextNonceWithHighestFeeRate
4,059✔
648
            || self.config.node.mock_mining
90✔
649
        {
650
            let mut mem_pool = self
9,005✔
651
                .config
9,005✔
652
                .connect_mempool_db()
9,005✔
653
                .expect("Database failure opening mempool");
9,005✔
654

655
            if self.reset_mempool_caches || self.config.node.mock_mining {
9,005✔
656
                mem_pool.reset_mempool_caches()?;
5,090✔
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,915✔
662
            }
663
        }
93✔
664

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

670
        if !self.propose_new_block_and_broadcast(
2,535✔
671
            coordinator,
2,535✔
672
            sortdb,
2,535✔
673
            stackerdbs,
2,535✔
674
            last_block_rejected,
2,535✔
675
            reward_set,
2,535✔
676
            new_block,
2,535✔
677
        )? {
17✔
678
            // We should reattempt to mine
679
            return Ok(());
277✔
680
        }
2,241✔
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,241✔
684

685
        Ok(())
2,166✔
686
    }
9,134✔
687

688
    /// Check if the parent block has been processed
689
    fn is_parent_processed(
223✔
690
        &mut self,
223✔
691
        chain_state: &mut StacksChainState,
223✔
692
    ) -> Result<bool, NakamotoNodeError> {
223✔
693
        let burn_db_path = self.config.get_burn_db_file_path();
223✔
694
        let mut burn_db = SortitionDB::open(
223✔
695
            &burn_db_path,
223✔
696
            true,
697
            self.burnchain.pox_constants.clone(),
223✔
698
            Some(self.config.node.get_marf_opts()),
223✔
699
        )
700
        .expect("FATAL: could not open sortition DB");
223✔
701
        self.check_burn_tip_changed(&burn_db)?;
223✔
702
        match self.load_block_parent_info(&mut burn_db, chain_state) {
220✔
703
            Ok(..) => Ok(true),
204✔
704
            Err(NakamotoNodeError::ParentNotFound) => Ok(false),
16✔
705
            Err(e) => {
×
706
                warn!("Failed to load parent info: {e:?}");
×
707
                Err(e)
×
708
            }
709
        }
710
    }
223✔
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,092✔
718
        &mut self,
9,092✔
719
        coordinator: &mut SignerCoordinator,
9,092✔
720
    ) -> Result<Option<NakamotoBlock>, NakamotoNodeError> {
9,092✔
721
        match self.mine_block(coordinator) {
9,092✔
722
            Ok(x) => {
2,409✔
723
                if !self.validate_timestamp(&x)? {
2,409✔
724
                    info!("Block mined too quickly. Will try again.";
19✔
725
                        "block_timestamp" => x.header.timestamp,
×
726
                    );
727
                    return Ok(None);
19✔
728
                }
2,390✔
729
                Ok(Some(x))
2,390✔
730
            }
731
            Err(NakamotoNodeError::MiningFailure(ChainstateError::MinerAborted)) => {
732
                if self.abort_flag.load(Ordering::SeqCst) {
3,916✔
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
                }
3,916✔
738

739
                info!("Miner interrupted while mining, will try again");
3,916✔
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));
3,916✔
744
                Ok(None)
3,916✔
745
            }
746
            Err(NakamotoNodeError::MiningFailure(ChainstateError::NoTransactionsToMine)) => {
747
                debug!(
2,656✔
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,656✔
752

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

763
                    // Check if the burnchain tip has changed
764
                    let Ok(sort_db) = SortitionDB::open(
29,340✔
765
                        &self.config.get_burn_db_file_path(),
29,340✔
766
                        false,
29,340✔
767
                        self.burnchain.pox_constants.clone(),
29,340✔
768
                        Some(self.config.node.get_marf_opts()),
29,340✔
769
                    ) else {
29,340✔
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() {
29,340✔
774
                        return Err(NakamotoNodeError::BurnchainTipChanged);
936✔
775
                    }
28,404✔
776

777
                    thread::sleep(Duration::from_millis(ABORT_TRY_AGAIN_MS));
28,404✔
778
                }
779
                Ok(None)
1,695✔
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) => {
111✔
789
                warn!("Failed to mine block: {e:?}");
111✔
790

791
                // try again, in case a new sortition is pending
792
                self.globals
111✔
793
                    .raise_initiative(format!("MiningFailure: {e:?}"));
111✔
794
                Err(ChainstateError::MinerAborted.into())
111✔
795
            }
796
        }
797
    }
9,092✔
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,390✔
805
        &mut self,
2,390✔
806
        coordinator: &mut SignerCoordinator,
2,390✔
807
        sortdb: &SortitionDB,
2,390✔
808
        stackerdbs: &mut StackerDBs,
2,390✔
809
        last_block_rejected: &mut bool,
2,390✔
810
        reward_set: &RewardSet,
2,390✔
811
        mut new_block: NakamotoBlock,
2,390✔
812
    ) -> Result<bool, NakamotoNodeError> {
2,390✔
813
        Self::fault_injection_block_proposal_stall(&new_block);
2,390✔
814

815
        let signer_signature = match self.propose_block(
2,390✔
816
            coordinator,
2,390✔
817
            &mut new_block,
2,390✔
818
            sortdb,
2,390✔
819
            stackerdbs,
2,390✔
820
        ) {
2,390✔
821
            Ok(x) => x,
2,264✔
822
            Err(e) => match e {
126✔
823
                NakamotoNodeError::StacksTipChanged => {
824
                    info!("Stacks tip changed while waiting for signatures";
3✔
825
                        "signer_signature_hash" => %new_block.header.signer_signature_hash(),
3✔
826
                        "block_height" => new_block.header.chain_length,
3✔
827
                        "consensus_hash" => %new_block.header.consensus_hash,
828
                    );
829
                    return Ok(false);
3✔
830
                }
831
                NakamotoNodeError::BurnchainTipChanged => {
832
                    info!("Burnchain tip changed while waiting for signatures";
17✔
833
                        "signer_signature_hash" => %new_block.header.signer_signature_hash(),
17✔
834
                        "block_height" => new_block.header.chain_length,
17✔
835
                        "consensus_hash" => %new_block.header.consensus_hash,
836
                    );
837
                    return Err(e);
17✔
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,
90✔
853
                    ref permanently_excluded_txids,
90✔
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();
90✔
859
                    // Permanently excluded txids will be blacklisted from the
860
                    // mempool when mine_block opens the mempool connection.
861
                    self.permanently_excluded_txids
90✔
862
                        .extend(permanently_excluded_txids.iter().cloned());
90✔
863
                    self.pause_and_retry(&new_block, last_block_rejected, &e);
90✔
864
                    return Ok(false);
90✔
865
                }
866
                _ => {
867
                    self.pause_and_retry(&new_block, last_block_rejected, &e);
16✔
868
                    return Ok(false);
16✔
869
                }
870
            },
871
        };
872
        *last_block_rejected = false;
2,264✔
873

874
        new_block.header.signer_signature = signer_signature;
2,264✔
875
        if let Err(e) = self.broadcast(new_block.clone(), reward_set, stackerdbs) {
2,264✔
876
            warn!("Error accepting own block: {e:?}. Will try mining again.");
23✔
877
            return Ok(false);
23✔
878
        } else {
879
            info!(
2,241✔
880
                "Miner: Block signed by signer set and broadcasted";
881
                "signer_signature_hash" => %new_block.header.signer_signature_hash(),
2,241✔
882
                "stacks_block_hash" => %new_block.header.block_hash(),
2,241✔
883
                "stacks_block_id" => %new_block.header.block_id(),
2,241✔
884
                "block_height" => new_block.header.chain_length,
2,241✔
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,241✔
890
            // Block was accepted — clear any single-block exclusions
891
            self.temporarily_excluded_txids.clear();
2,241✔
892
        }
893

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

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

905
        self.last_block_mined = Some((
2,241✔
906
            new_block.header.consensus_hash.clone(),
2,241✔
907
            new_block.header.block_hash(),
2,241✔
908
        ));
2,241✔
909
        self.mined_blocks += 1;
2,241✔
910
        Ok(true)
2,241✔
911
    }
2,390✔
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,241✔
919
        &mut self,
2,241✔
920
        chain_state: &mut StacksChainState,
2,241✔
921
    ) -> Result<(), NakamotoNodeError> {
2,241✔
922
        let Some((last_consensus_hash, last_bhh)) = &self.last_block_mined else {
2,241✔
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,241✔
930
            thread::sleep(Duration::from_millis(
43✔
931
                self.config.miner.min_time_between_blocks_ms,
43✔
932
            ));
933
            return Ok(());
43✔
934
        }
2,198✔
935

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

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

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

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

964
            // Check if the burnchain tip has changed
965
            let Ok(sort_db) = SortitionDB::open(
3,445✔
966
                &self.config.get_burn_db_file_path(),
3,445✔
967
                false,
3,445✔
968
                self.burnchain.pox_constants.clone(),
3,445✔
969
                Some(self.config.node.get_marf_opts()),
3,445✔
970
            ) else {
3,445✔
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,445✔
975
                return Err(NakamotoNodeError::BurnchainTipChanged);
17✔
976
            }
3,428✔
977
        }
978
    }
2,241✔
979

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

992
        let mut chain_state =
2,346✔
993
            neon_node::open_chainstate_with_faults(&self.config).map_err(|e| {
2,346✔
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,346✔
1000
            NakamotoNodeError::SigningCoordinatorFailure(format!(
×
1001
                "Failed to open sortition DB. Cannot mine! {e:?}"
×
1002
            ))
×
1003
        })?;
×
1004

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

1014
        coordinator.propose_block(
2,346✔
1015
            new_block,
2,346✔
1016
            &self.burnchain,
2,346✔
1017
            sortdb,
2,346✔
1018
            &mut chain_state,
2,346✔
1019
            stackerdbs,
2,346✔
1020
            &self.globals.counters,
2,346✔
1021
            &self.burn_election_block,
2,346✔
1022
            &self.miner_db,
2,346✔
1023
            diagnostics,
2,346✔
1024
        )
1025
    }
2,389✔
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,436✔
1032
        if let Some(set) = self.signer_set_cache.as_ref() {
10,436✔
1033
            return Ok(set.clone());
9,092✔
1034
        }
1,344✔
1035
        let sort_db = SortitionDB::open(
1,344✔
1036
            &self.config.get_burn_db_file_path(),
1,344✔
1037
            true,
1038
            self.burnchain.pox_constants.clone(),
1,344✔
1039
            Some(self.config.node.get_marf_opts()),
1,344✔
1040
        )
1041
        .map_err(|e| {
1,344✔
1042
            NakamotoNodeError::SigningCoordinatorFailure(format!(
×
1043
                "Failed to open sortition DB. Cannot mine! {e:?}"
×
1044
            ))
×
1045
        })?;
×
1046

1047
        let mut chain_state =
1,344✔
1048
            neon_node::open_chainstate_with_faults(&self.config).map_err(|e| {
1,344✔
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,344✔
1055

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

1061
        let reward_info = match load_nakamoto_reward_set(
1,344✔
1062
            reward_cycle,
1,344✔
1063
            &self.burn_election_block.sortition_id,
1,344✔
1064
            &self.burnchain,
1,344✔
1065
            &mut chain_state,
1,344✔
1066
            &self.parent_tenure_id,
1,344✔
1067
            &sort_db,
1,344✔
1068
            &OnChainRewardSetProvider::new(),
1,344✔
1069
        ) {
1070
            Ok(Some((reward_info, _))) => reward_info,
1,344✔
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,344✔
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,344✔
1090
        Ok(reward_set)
1,344✔
1091
    }
10,436✔
1092

1093
    /// Fault injection -- possibly fail to broadcast
1094
    /// Return true to drop the block
1095
    fn fault_injection_broadcast_fail(&self) -> bool {
2,176✔
1096
        let drop_prob = self
2,176✔
1097
            .config
2,176✔
1098
            .node
2,176✔
1099
            .fault_injection_block_push_fail_probability
2,176✔
1100
            .unwrap_or(0)
2,176✔
1101
            .min(100);
2,176✔
1102
        if drop_prob > 0 {
2,176✔
1103
            let throw: u8 = thread_rng().gen_range(0..100);
×
1104
            throw < drop_prob
×
1105
        } else {
1106
            false
2,176✔
1107
        }
1108
    }
2,176✔
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,201✔
1113
        &mut self,
2,201✔
1114
        sort_db: &SortitionDB,
2,201✔
1115
        chain_state: &mut StacksChainState,
2,201✔
1116
        block: &NakamotoBlock,
2,201✔
1117
        reward_set: &RewardSet,
2,201✔
1118
    ) -> Result<(), ChainstateError> {
2,201✔
1119
        if Self::fault_injection_skip_block_broadcast() {
2,201✔
1120
            warn!(
5✔
1121
                "Fault injection: Skipping block broadcast for {}",
1122
                block.block_id()
5✔
1123
            );
1124
            return Ok(());
5✔
1125
        }
2,196✔
1126
        #[cfg(test)]
1127
        TEST_MINER_BROADCASTING_BLOCK.set(block.clone());
2,196✔
1128

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

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

1145
        if !accepted {
2,196✔
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());
20✔
1151

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

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

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

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

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

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

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

1206
        let Some(ref miner_privkey) = self.config.miner.mining_key else {
2,201✔
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,201✔
1216
            NakamotoNodeError::MinerConfigurationFailed("Failed to get RPC loopback socket")
×
1217
        })?;
×
1218
        let miners_contract_id = boot_code_id(MINERS_NAME, chain_state.mainnet);
2,201✔
1219
        let mut miners_session = StackerDBSession::new(
2,201✔
1220
            &rpc_socket.to_string(),
2,201✔
1221
            miners_contract_id,
2,201✔
1222
            self.config.miner.stackerdb_timeout,
2,201✔
1223
        );
1224

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

1233
        SignerCoordinator::send_miners_message(
2,201✔
1234
            miner_privkey,
2,201✔
1235
            &sort_db,
2,201✔
1236
            &self.burn_block,
2,201✔
1237
            stackerdbs,
2,201✔
1238
            SignerMessage::BlockPushed(block),
2,201✔
1239
            MinerSlotID::BlockPushed,
2,201✔
1240
            chain_state.mainnet,
2,201✔
1241
            &mut miners_session,
2,201✔
1242
            &self.burn_election_block.consensus_hash,
2,201✔
1243
            &self.miner_db,
2,201✔
1244
        )
1245
    }
2,244✔
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,270✔
1249
        if epoch_id < StacksEpochId::Epoch21 && self.config.miner.block_reward_recipient.is_some() {
1,270✔
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,270✔
1254
        }
1255
    }
1,270✔
1256

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

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

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

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

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

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

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

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

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

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

1319
        tx_signer.get_tx().unwrap()
1,270✔
1320
    }
1,270✔
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,698✔
1333
        &self,
7,698✔
1334
        burn_db: &mut SortitionDB,
7,698✔
1335
        chain_state: &mut StacksChainState,
7,698✔
1336
    ) -> Result<StacksHeaderInfo, NakamotoNodeError> {
7,698✔
1337
        let my_tenure_tip = self
7,698✔
1338
            .find_highest_known_block_in_my_tenure(&burn_db, &chain_state)
7,698✔
1339
            .map_err(|e| {
7,698✔
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,698✔
1347
            debug!(
6,394✔
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,394✔
1352
        }
1,304✔
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,304✔
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,288✔
1363
            NakamotoChainState::get_block_header(chain_state.db(), &self.parent_tenure_id)
1,304✔
1364
                .map_err(|e| {
1,304✔
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,304✔
1372
                    error!("No header for parent tenure ID {}", &self.parent_tenure_id);
16✔
1373
                    NakamotoNodeError::ParentNotFound
16✔
1374
                })?;
16✔
1375

1376
        let header_opt = NakamotoChainState::find_highest_known_block_header_in_tenure(
1,288✔
1377
            &chain_state,
1,288✔
1378
            burn_db,
1,288✔
1379
            &parent_tenure_header.consensus_hash,
1,288✔
1380
        )
1381
        .map_err(|e| {
1,288✔
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,288✔
1387
            return Ok(parent_tenure_tip);
1,288✔
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,698✔
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,698✔
1404
        &self,
7,698✔
1405
        burn_db: &mut SortitionDB,
7,698✔
1406
        chain_state: &mut StacksChainState,
7,698✔
1407
    ) -> Result<ParentStacksBlockInfo, NakamotoNodeError> {
7,698✔
1408
        let stacks_tip_header = self.load_block_parent_header(burn_db, chain_state)?;
7,698✔
1409

1410
        debug!(
7,682✔
1411
            "Miner: stacks tip parent header is {} {stacks_tip_header:?}",
1412
            &stacks_tip_header.index_block_hash()
×
1413
        );
1414
        let miner_address = self
7,682✔
1415
            .keychain
7,682✔
1416
            .origin_address(self.config.is_mainnet())
7,682✔
1417
            .unwrap();
7,682✔
1418
        ParentStacksBlockInfo::lookup(
7,682✔
1419
            chain_state,
7,682✔
1420
            burn_db,
7,682✔
1421
            &self.burn_block,
7,682✔
1422
            miner_address,
7,682✔
1423
            &self.parent_tenure_id,
7,682✔
1424
            stacks_tip_header,
7,682✔
1425
            &self.reason,
7,682✔
1426
        )
1427
        .inspect_err(|e| {
7,682✔
1428
            if matches!(e, NakamotoNodeError::BurnchainTipChanged) {
×
1429
                self.globals.counters.bump_missed_tenures();
×
1430
            }
×
1431
        })
×
1432
    }
7,698✔
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,478✔
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,478✔
1441
            self.keychain.generate_proof(
204✔
1442
                VRF_MOCK_MINER_KEY,
1443
                self.burn_election_block.sortition_hash.as_bytes(),
204✔
1444
            )
1445
        } else {
1446
            self.keychain.generate_proof(
7,274✔
1447
                self.registered_key.target_block_height,
7,274✔
1448
                self.burn_election_block.sortition_hash.as_bytes(),
7,274✔
1449
            )
1450
        };
1451

1452
        let Some(vrf_proof) = vrf_proof else {
7,478✔
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,478✔
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,478✔
1472
    }
7,478✔
1473

1474
    fn validate_timestamp_info(
9,856✔
1475
        &self,
9,856✔
1476
        current_timestamp_secs: u64,
9,856✔
1477
        stacks_parent_header: &StacksHeaderInfo,
9,856✔
1478
    ) -> bool {
9,856✔
1479
        let parent_timestamp = match stacks_parent_header.anchored_header.as_stacks_nakamoto() {
9,856✔
1480
            Some(naka_header) => naka_header.timestamp,
9,439✔
1481
            None => stacks_parent_header.burn_header_timestamp,
417✔
1482
        };
1483
        let time_since_parent_ms = current_timestamp_secs.saturating_sub(parent_timestamp) * 1000;
9,856✔
1484
        if time_since_parent_ms < self.config.miner.min_time_between_blocks_ms {
9,856✔
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;
205✔
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
205✔
1492
        } else {
1493
            true
9,651✔
1494
        }
1495
    }
9,856✔
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,390✔
1500
        let chain_state = neon_node::open_chainstate_with_faults(&self.config)
2,390✔
1501
            .expect("FATAL: could not open chainstate DB");
2,390✔
1502
        let stacks_parent_header =
2,390✔
1503
            NakamotoChainState::get_block_header(chain_state.db(), &x.header.parent_block_id)
2,390✔
1504
                .map_err(|e| {
2,390✔
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,390✔
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,390✔
1519
    }
2,390✔
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,092✔
1526
        &mut self,
9,092✔
1527
        coordinator: &mut SignerCoordinator,
9,092✔
1528
    ) -> Result<NakamotoBlock, NakamotoNodeError> {
9,092✔
1529
        debug!("block miner thread ID is {:?}", thread::current().id());
9,092✔
1530
        info!("Miner: Mining block");
9,092✔
1531

1532
        let burn_db_path = self.config.get_burn_db_file_path();
9,092✔
1533
        let reward_set = self.load_signer_set()?;
9,092✔
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,092✔
1538
            &burn_db_path,
9,092✔
1539
            true,
1540
            self.burnchain.pox_constants.clone(),
9,092✔
1541
            Some(self.config.node.get_marf_opts()),
9,092✔
1542
        )
1543
        .expect("FATAL: could not open sortition DB");
9,092✔
1544

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

1548
        self.check_burn_tip_changed(&burn_db)?;
9,092✔
1549
        if Self::fault_injection_block_mining_skip() {
8,981✔
1550
            return Err(ChainstateError::MinerAborted.into());
1,503✔
1551
        }
7,478✔
1552
        neon_node::fault_injection_long_tenure();
7,478✔
1553

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

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

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

1580
        if self.config.node.mock_mining {
7,478✔
1581
            if let Some((last_block_consensus_hash, _)) = &self.last_block_mined {
204✔
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
139✔
1585
                    == &parent_block_info.stacks_parent_header.consensus_hash
139✔
1586
                {
127✔
1587
                    // If the parent block is in the same tenure, then we should
127✔
1588
                    // pretend that we mined it.
127✔
1589
                    self.last_block_mined = Some((
127✔
1590
                        parent_block_info
127✔
1591
                            .stacks_parent_header
127✔
1592
                            .consensus_hash
127✔
1593
                            .clone(),
127✔
1594
                        parent_block_info
127✔
1595
                            .stacks_parent_header
127✔
1596
                            .anchored_header
127✔
1597
                            .block_hash(),
127✔
1598
                    ));
127✔
1599
                } else {
127✔
1600
                    // If the parent block is not in the same tenure, then we
12✔
1601
                    // should act as though we haven't mined anything yet.
12✔
1602
                    self.last_block_mined = None;
12✔
1603
                }
12✔
1604
            }
65✔
1605
        }
7,274✔
1606

1607
        if self.last_block_mined.is_none() && parent_block_info.parent_tenure.is_none() {
7,478✔
1608
            if self.config.node.mock_mining {
58✔
1609
                info!("Mock miner will follow canonical tip within an ongoing tenure; no parent tenure info loaded yet");
58✔
1610
            } else {
1611
                warn!(
×
1612
                    "Miner should be starting a new tenure, but failed to load parent tenure info"
1613
                );
1614
                return Err(NakamotoNodeError::ParentNotFound);
×
1615
            }
1616
        };
7,420✔
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,478✔
1620
            &chain_state,
7,478✔
1621
            &parent_block_info,
7,478✔
1622
            vrf_proof,
7,478✔
1623
            target_epoch_id,
7,478✔
1624
            coordinator,
7,478✔
1625
        )?;
×
1626

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

1629
        let signer_bitvec_len = reward_set.rewarded_addresses.len().try_into().ok();
7,478✔
1630

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

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

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

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

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

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

1730
        self.event_dispatcher.process_mined_nakamoto_block_event(
2,394✔
1731
            self.burn_block.block_height,
2,394✔
1732
            &block_metadata.block,
2,394✔
1733
            block_metadata.tenure_size,
2,394✔
1734
            &block_metadata.tenure_consumed,
2,394✔
1735
            block_metadata.tx_events,
2,394✔
1736
        );
1737

1738
        self.tenure_cost = block_metadata.tenure_consumed;
2,394✔
1739
        self.tenure_budget = block_metadata.tenure_budget;
2,394✔
1740

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

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

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

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

1789
        let tenure_extend_timestamp = coordinator.get_tenure_extend_timestamp();
375✔
1790
        if get_epoch_time_secs() <= tenure_extend_timestamp
375✔
1791
            && self.tenure_change_time.elapsed() <= self.config.miner.tenure_timeout
246✔
1792
        {
1793
            return Ok(false);
240✔
1794
        }
135✔
1795

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

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

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

1819
        if usage < self.config.miner.read_count_extend_cost_threshold {
5,844✔
1820
            info!(
5,753✔
1821
                "Miner: not read-count extending because threshold not reached";
1822
                "threshold" => self.config.miner.read_count_extend_cost_threshold,
5,753✔
1823
                "usage" => usage
5,753✔
1824
            );
1825
            return Ok(false);
5,753✔
1826
        }
91✔
1827

1828
        let tenure_extend_timestamp = coordinator.get_read_count_extend_timestamp();
91✔
1829
        if get_epoch_time_secs() <= tenure_extend_timestamp {
91✔
1830
            info!(
84✔
1831
                "Miner: not read-count extending because idle timestamp not reached";
1832
                "now" => get_epoch_time_secs(),
84✔
1833
                "extend_ts" => tenure_extend_timestamp,
84✔
1834
            );
1835
            return Ok(false);
84✔
1836
        }
7✔
1837

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

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

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

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

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

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

1958
        debug!(
1,572✔
1959
            "make_tenure_start_info: reason = {:?}, burn_view = {:?}, tenure_change_tx = {:?}",
1960
            &self.reason, &self.burn_block.consensus_hash, &tenure_change_tx
×
1961
        );
1962

1963
        Ok(NakamotoTenureInfo {
1,572✔
1964
            coinbase_tx,
1,572✔
1965
            tenure_change_tx,
1,572✔
1966
        })
1,572✔
1967
    }
7,466✔
1968

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

1976
        if cur_burn_chain_tip.consensus_hash != self.burn_tip_at_start {
44,474✔
1977
            info!("Miner: Cancel block assembly; burnchain tip has changed";
1,011✔
1978
                "new_tip" => %cur_burn_chain_tip.consensus_hash,
1979
                "local_tip" => %self.burn_tip_at_start);
1980
            self.globals.counters.bump_missed_tenures();
1,011✔
1981
            Err(NakamotoNodeError::BurnchainTipChanged)
1,011✔
1982
        } else {
1983
            Ok(())
43,463✔
1984
        }
1985
    }
44,474✔
1986

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

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

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

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

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

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

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

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

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

2139
        Ok(ParentStacksBlockInfo {
7,682✔
2140
            stacks_parent_header: stacks_tip_header,
7,682✔
2141
            coinbase_nonce,
7,682✔
2142
            parent_tenure: parent_tenure_info,
7,682✔
2143
        })
7,682✔
2144
    }
7,682✔
2145
}
2146

2147
#[cfg(test)]
2148
impl ReadCountCheck for () {
2149
    fn get_read_count_extend_timestamp(&self) -> u64 {
1✔
2150
        // always allow the read count extend
2151
        0
1✔
2152
    }
1✔
2153
}
2154

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

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

2211
    miner.tenure_cost = ExecutionCost {
1✔
2212
        write_length: 1000,
1✔
2213
        write_count: 1000,
1✔
2214
        read_length: 1000,
1✔
2215
        read_count: 199,
1✔
2216
        runtime: 1000,
1✔
2217
    };
1✔
2218

2219
    miner.tenure_budget = ExecutionCost {
1✔
2220
        write_length: 1000,
1✔
2221
        write_count: 1000,
1✔
2222
        read_length: 1000,
1✔
2223
        read_count: 1000,
1✔
2224
        runtime: 1000,
1✔
2225
    };
1✔
2226

2227
    assert_eq!(
1✔
2228
        miner.should_read_count_extend(&()).unwrap(),
1✔
2229
        false,
2230
        "When read_count is below the configured threshold, we shouldn't try to extend"
2231
    );
2232

2233
    miner.tenure_cost = ExecutionCost {
1✔
2234
        write_length: 1000,
1✔
2235
        write_count: 1000,
1✔
2236
        read_length: 1000,
1✔
2237
        read_count: 200,
1✔
2238
        runtime: 1000,
1✔
2239
    };
1✔
2240

2241
    miner.tenure_budget = ExecutionCost {
1✔
2242
        write_length: 1000,
1✔
2243
        write_count: 1000,
1✔
2244
        read_length: 1000,
1✔
2245
        read_count: 1000,
1✔
2246
        runtime: 1000,
1✔
2247
    };
1✔
2248

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