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

stacks-network / stacks-core / 26829844658-1

02 Jun 2026 03:24PM UTC coverage: 86.038% (+0.08%) from 85.959%
26829844658-1

Pull #7249

github

06b7ec
web-flow
Merge e017b11f4 into b945664c8
Pull Request #7249: feat: seamless rollovers between bond and stx-only staking positions

382 of 407 new or added lines in 3 files covered. (93.86%)

6474 existing lines in 95 files now uncovered.

194813 of 226426 relevant lines covered (86.04%)

18725269.97 hits per line

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

86.74
/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,747✔
118
        std::thread::sleep(std::time::Duration::from_millis(10));
14,666✔
119
    }
14,666✔
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 {
15,921✔
214
        match self {
15,921✔
215
            Self::BlockFound { ref late } => *late,
13,661✔
216
            Self::Extended { .. } => false,
2,152✔
217
            Self::ReadCountExtend { .. } => false,
108✔
218
        }
219
    }
15,921✔
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,822✔
246
        match self {
2,822✔
247
            Self::BlockFound { .. } => MiningReason::BlockFound,
2,520✔
248
            Self::Extended { .. } => MiningReason::Extended,
299✔
249
            Self::ReadCountExtend { .. } => MiningReason::ReadCountExtend,
3✔
250
        }
251
    }
2,822✔
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 {
95✔
322
        SignerCoordinator::get_read_count_extend_timestamp(self)
95✔
323
    }
95✔
324
}
325

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

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

368
    #[cfg(test)]
369
    fn fault_injection_block_proposal_stall(new_block: &NakamotoBlock) {
2,871✔
370
        if TEST_BROADCAST_PROPOSAL_STALL.get().iter().any(|key| {
2,871✔
371
            key.verify(
34✔
372
                new_block.header.miner_signature_hash().as_bytes(),
34✔
373
                &new_block.header.miner_signature,
34✔
374
            )
375
            .unwrap_or_default()
34✔
376
        }) {
34✔
377
            warn!("Fault injection: Block proposal broadcast is stalled due to testing directive.";
24✔
378
                        "stacks_block_id" => %new_block.block_id(),
24✔
379
                        "stacks_block_hash" => %new_block.header.block_hash(),
24✔
380
                        "height" => new_block.header.chain_length,
24✔
381
                        "consensus_hash" => %new_block.header.consensus_hash
382
            );
383
            while TEST_BROADCAST_PROPOSAL_STALL.get().iter().any(|key| {
37,654✔
384
                key.verify(
37,638✔
385
                    new_block.header.miner_signature_hash().as_bytes(),
37,638✔
386
                    &new_block.header.miner_signature,
37,638✔
387
                )
388
                .unwrap_or_default()
37,638✔
389
            }) {
37,638✔
390
                std::thread::sleep(std::time::Duration::from_millis(10));
35,566✔
391
            }
35,566✔
392
            info!("Fault injection: Block proposal broadcast is no longer stalled due to testing directive.";
24✔
393
                    "block_id" => %new_block.block_id(),
22✔
394
                    "height" => new_block.header.chain_length,
22✔
395
                    "consensus_hash" => %new_block.header.consensus_hash
396
            );
397
        }
2,847✔
398
    }
2,871✔
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 {
10,405✔
405
        if TEST_MINE_SKIP.get() {
10,405✔
406
            warn!("Fault injection: Block mining is skipped due to testing directive.");
1,786✔
407
            true
1,786✔
408
        } else {
409
            false
8,619✔
410
        }
411
    }
10,405✔
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,680✔
420
        if TEST_BLOCK_ANNOUNCE_STALL.get() {
2,680✔
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,889✔
429
                std::thread::sleep(std::time::Duration::from_millis(10));
2,885✔
430
            }
2,885✔
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,676✔
437
    }
2,680✔
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,636✔
444
        TEST_P2P_BROADCAST_SKIP.get()
2,636✔
445
    }
2,636✔
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,631✔
454
        if TEST_P2P_BROADCAST_STALL.get() {
2,631✔
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() {
837✔
463
                std::thread::sleep(std::time::Duration::from_millis(10));
832✔
464
            }
832✔
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,626✔
471
    }
2,631✔
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,636✔
478
        TEST_BLOCK_PUSH_SKIP.get()
2,636✔
479
    }
2,636✔
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() {
10,564✔
490
        if TEST_MINE_STALL.get() != TestMineStall::NotStalled {
10,564✔
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 {
108,030✔
495
                std::thread::sleep(std::time::Duration::from_millis(10));
107,951✔
496
            }
107,951✔
497
            warn!("Mining is no longer stalled due to testing directive. Continuing...");
79✔
498
        }
10,485✔
499
    }
10,564✔
500

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

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

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

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

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

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

655
            if self.reset_mempool_caches || self.config.node.mock_mining {
10,439✔
656
                mem_pool.reset_mempool_caches()?;
6,013✔
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()?;
4,426✔
662
            }
663
        }
94✔
664

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

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

685
        Ok(())
2,588✔
686
    }
10,564✔
687

688
    /// Check if the parent block has been processed
689
    fn is_parent_processed(
237✔
690
        &mut self,
237✔
691
        chain_state: &mut StacksChainState,
237✔
692
    ) -> Result<bool, NakamotoNodeError> {
237✔
693
        let burn_db_path = self.config.get_burn_db_file_path();
237✔
694
        let mut burn_db = SortitionDB::open(
237✔
695
            &burn_db_path,
237✔
696
            true,
697
            self.burnchain.pox_constants.clone(),
237✔
698
            Some(self.config.node.get_marf_opts()),
237✔
699
        )
700
        .expect("FATAL: could not open sortition DB");
237✔
701
        self.check_burn_tip_changed(&burn_db)?;
237✔
702
        match self.load_block_parent_info(&mut burn_db, chain_state) {
235✔
703
            Ok(..) => Ok(true),
223✔
704
            Err(NakamotoNodeError::ParentNotFound) => Ok(false),
12✔
705
            Err(e) => {
×
706
                warn!("Failed to load parent info: {e:?}");
×
707
                Err(e)
×
708
            }
709
        }
710
    }
237✔
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(
10,529✔
718
        &mut self,
10,529✔
719
        coordinator: &mut SignerCoordinator,
10,529✔
720
    ) -> Result<Option<NakamotoBlock>, NakamotoNodeError> {
10,529✔
721
        match self.mine_block(coordinator) {
10,529✔
722
            Ok(x) => {
2,884✔
723
                if !self.validate_timestamp(&x)? {
2,884✔
724
                    info!("Block mined too quickly. Will try again.";
13✔
725
                        "block_timestamp" => x.header.timestamp,
×
726
                    );
727
                    return Ok(None);
13✔
728
                }
2,871✔
729
                Ok(Some(x))
2,871✔
730
            }
731
            Err(NakamotoNodeError::MiningFailure(ChainstateError::MinerAborted)) => {
732
                if self.abort_flag.load(Ordering::SeqCst) {
4,446✔
733
                    info!("Miner interrupted while mining in order to shut down");
×
734
                    self.globals
×
735
                        .raise_initiative(format!("MiningFailure: aborted by node"));
×
736
                    return Err(ChainstateError::MinerAborted.into());
×
737
                }
4,446✔
738

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

741
                // sleep, and try again. if the miner was interrupted because the burnchain
742
                // view changed, the next `mine_block()` invocation will error
743
                thread::sleep(Duration::from_millis(ABORT_TRY_AGAIN_MS));
4,446✔
744
                Ok(None)
4,446✔
745
            }
746
            Err(NakamotoNodeError::MiningFailure(ChainstateError::NoTransactionsToMine)) => {
747
                debug!(
3,075✔
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;
3,075✔
752

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

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

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

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

815
        let signer_signature = match self.propose_block(
2,871✔
816
            coordinator,
2,871✔
817
            &mut new_block,
2,871✔
818
            sortdb,
2,871✔
819
            stackerdbs,
2,871✔
820
        ) {
2,871✔
821
            Ok(x) => x,
2,704✔
822
            Err(e) => match e {
167✔
823
                NakamotoNodeError::StacksTipChanged => {
824
                    info!("Stacks tip changed while waiting for signatures";
4✔
825
                        "signer_signature_hash" => %new_block.header.signer_signature_hash(),
4✔
826
                        "block_height" => new_block.header.chain_length,
4✔
827
                        "consensus_hash" => %new_block.header.consensus_hash,
828
                    );
829
                    return Ok(false);
4✔
830
                }
831
                NakamotoNodeError::BurnchainTipChanged => {
832
                    info!("Burnchain tip changed while waiting for signatures";
20✔
833
                        "signer_signature_hash" => %new_block.header.signer_signature_hash(),
20✔
834
                        "block_height" => new_block.header.chain_length,
20✔
835
                        "consensus_hash" => %new_block.header.consensus_hash,
836
                    );
837
                    return Err(e);
20✔
838
                }
839
                NakamotoNodeError::StackerDBUploadError(ref ack) => {
×
840
                    if ack.code == Some(StackerDBErrorCodes::BadSigner.code()) {
×
841
                        error!("Error while gathering signatures: failed to upload miner StackerDB data: {ack:?}. Giving up.";
×
842
                            "signer_signature_hash" => %new_block.header.signer_signature_hash(),
×
843
                            "block_height" => new_block.header.chain_length,
×
844
                            "consensus_hash" => %new_block.header.consensus_hash,
845
                        );
846
                        return Err(e);
×
847
                    }
×
848
                    self.pause_and_retry(&new_block, last_block_rejected, &e);
×
849
                    return Ok(false);
×
850
                }
851
                NakamotoNodeError::SignersRejected {
852
                    ref temporarily_excluded_txids,
129✔
853
                    ref permanently_excluded_txids,
129✔
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();
129✔
859
                    // Permanently excluded txids will be blacklisted from the
860
                    // mempool when mine_block opens the mempool connection.
861
                    self.permanently_excluded_txids
129✔
862
                        .extend(permanently_excluded_txids.iter().cloned());
129✔
863
                    self.pause_and_retry(&new_block, last_block_rejected, &e);
129✔
864
                    return Ok(false);
129✔
865
                }
866
                _ => {
867
                    self.pause_and_retry(&new_block, last_block_rejected, &e);
14✔
868
                    return Ok(false);
14✔
869
                }
870
            },
871
        };
872
        *last_block_rejected = false;
2,704✔
873

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

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

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

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

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

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

955
            thread::sleep(Duration::from_millis(ABORT_TRY_AGAIN_MS));
4,188✔
956

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1319
        tx_signer.get_tx().unwrap()
1,576✔
1320
    }
1,576✔
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(
8,854✔
1333
        &self,
8,854✔
1334
        burn_db: &mut SortitionDB,
8,854✔
1335
        chain_state: &mut StacksChainState,
8,854✔
1336
    ) -> Result<StacksHeaderInfo, NakamotoNodeError> {
8,854✔
1337
        let my_tenure_tip = self
8,854✔
1338
            .find_highest_known_block_in_my_tenure(&burn_db, &chain_state)
8,854✔
1339
            .map_err(|e| {
8,854✔
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 {
8,854✔
1347
            debug!(
7,246✔
1348
                "Stacks block parent ID is last block in tenure ID {}",
1349
                &my_tenure_tip.consensus_hash
×
1350
            );
1351
            return Ok(my_tenure_tip);
7,246✔
1352
        }
1,608✔
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,608✔
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,596✔
1363
            NakamotoChainState::get_block_header(chain_state.db(), &self.parent_tenure_id)
1,608✔
1364
                .map_err(|e| {
1,608✔
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,608✔
1372
                    error!("No header for parent tenure ID {}", &self.parent_tenure_id);
12✔
1373
                    NakamotoNodeError::ParentNotFound
12✔
1374
                })?;
12✔
1375

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

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

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

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

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

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

1548
        self.check_burn_tip_changed(&burn_db)?;
10,529✔
1549
        if Self::fault_injection_block_mining_skip() {
10,405✔
1550
            return Err(ChainstateError::MinerAborted.into());
1,786✔
1551
        }
8,619✔
1552
        neon_node::fault_injection_long_tenure();
8,619✔
1553

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

1559
        // Blacklist permanently excluded transactions reported by signers
1560
        if !self.permanently_excluded_txids.is_empty() {
8,619✔
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✔
UNCOV
1566
                warn!("Miner: failed to blacklist permanently excluded transactions: {e:?}");
×
1567
            }
1✔
1568
        }
8,618✔
1569

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

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

1607
        if self.last_block_mined.is_none() && parent_block_info.parent_tenure.is_none() {
8,619✔
1608
            if self.config.node.mock_mining {
×
1609
                info!("Mock miner will follow canonical tip within an ongoing tenure; no parent tenure info loaded yet");
×
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
        };
8,619✔
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(
8,619✔
1620
            &chain_state,
8,619✔
1621
            &parent_block_info,
8,619✔
1622
            vrf_proof,
8,619✔
1623
            target_epoch_id,
8,619✔
1624
            coordinator,
8,619✔
1625
        )?;
×
1626

1627
        parent_block_info.stacks_parent_header.microblock_tail = None;
8,619✔
1628

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1966
        Ok(NakamotoTenureInfo {
1,963✔
1967
            coinbase_tx,
1,963✔
1968
            tenure_change_tx,
1,963✔
1969
        })
1,963✔
1970
    }
8,612✔
1971

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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