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

stacks-network / stacks-core / 25801484257-1

13 May 2026 01:15PM UTC coverage: 85.648% (-0.06%) from 85.712%
25801484257-1

Pull #7183

github

2d7e6d
web-flow
Merge 420cb597a into 31276d071
Pull Request #7183: Fix problematic transaction handling

110 of 130 new or added lines in 7 files covered. (84.62%)

5464 existing lines in 98 files now uncovered.

188263 of 219809 relevant lines covered (85.65%)

18940648.33 hits per line

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

86.1
/stacks-node/src/nakamoto_node/relayer.rs
1
// Copyright (C) 2013-2020 Blockstack PBC, a public benefit corporation
2
// Copyright (C) 2020-2023 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 core::fmt;
17
use std::io::Read;
18
use std::sync::atomic::{AtomicBool, Ordering};
19
use std::sync::mpsc::{Receiver, RecvTimeoutError};
20
use std::sync::Arc;
21
#[cfg(test)]
22
use std::sync::LazyLock;
23
use std::thread::JoinHandle;
24
use std::time::{Duration, Instant};
25
use std::{fs, thread};
26

27
use rand::{thread_rng, Rng};
28
use stacks::burnchains::{Burnchain, Txid};
29
use stacks::chainstate::burn::db::sortdb::{FindIter, SortitionDB};
30
use stacks::chainstate::burn::operations::leader_block_commit::{
31
    RewardSetInfo, BURN_BLOCK_MINED_AT_MODULUS,
32
};
33
use stacks::chainstate::burn::operations::{
34
    BlockstackOperationType, LeaderBlockCommitOp, LeaderKeyRegisterOp,
35
};
36
use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash};
37
use stacks::chainstate::nakamoto::coordinator::get_nakamoto_next_recipients;
38
use stacks::chainstate::nakamoto::{NakamotoBlockHeader, NakamotoChainState};
39
use stacks::chainstate::stacks::address::PoxAddress;
40
use stacks::chainstate::stacks::db::StacksChainState;
41
use stacks::chainstate::stacks::miner::{
42
    set_mining_spend_amount, signal_mining_blocked, signal_mining_ready,
43
};
44
use stacks::chainstate::stacks::Error as ChainstateError;
45
use stacks::config::BurnchainConfig;
46
use stacks::core::mempool::MemPoolDB;
47
use stacks::core::STACKS_EPOCH_LATEST_MARKER;
48
use stacks::monitoring::increment_stx_blocks_mined_counter;
49
use stacks::net::db::LocalPeer;
50
use stacks::net::p2p::NetworkHandle;
51
use stacks::net::relay::Relayer;
52
use stacks::net::NetworkResult;
53
use stacks::util_lib::db::Error as DbError;
54
use stacks_common::types::chainstate::{
55
    BlockHeaderHash, BurnchainHeaderHash, StacksBlockId, StacksPublicKey, VRFSeed,
56
};
57
use stacks_common::types::StacksEpochId;
58
use stacks_common::util::get_epoch_time_ms;
59
use stacks_common::util::hash::Hash160;
60
#[cfg(test)]
61
use stacks_common::util::tests::TestFlag;
62
use stacks_common::util::vrf::VRFPublicKey;
63

64
use super::miner::MinerReason;
65
use super::{
66
    Config, Error as NakamotoNodeError, EventDispatcher, Keychain, BLOCK_PROCESSOR_STACK_SIZE,
67
};
68
use crate::burnchains::BurnchainController;
69
use crate::nakamoto_node::miner::{BlockMinerThread, MinerDirective};
70
use crate::neon_node::{
71
    fault_injection_skip_mining, open_chainstate_with_faults, LeaderKeyRegistrationState,
72
};
73
use crate::run_loop::nakamoto::{Globals, RunLoop};
74
use crate::run_loop::RegisteredKey;
75
use crate::BitcoinRegtestController;
76

77
#[cfg(test)]
78
/// Mutex to stall the relayer thread right before it creates a miner thread.
79
pub static TEST_MINER_THREAD_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
80

81
#[cfg(test)]
82
/// Mutex to stall the miner thread right after it starts up (does not block the relayer thread)
83
pub static TEST_MINER_THREAD_START_STALL: LazyLock<TestFlag<bool>> =
84
    LazyLock::new(TestFlag::default);
85

86
#[cfg(test)]
87
/// Test flag to set the tip for the miner to commit to
88
pub static TEST_MINER_COMMIT_TIP: LazyLock<TestFlag<Option<(ConsensusHash, BlockHeaderHash)>>> =
89
    LazyLock::new(TestFlag::default);
90

91
/// Command types for the Nakamoto relayer thread, issued to it by other threads
92
#[allow(clippy::large_enum_variant)]
93
pub enum RelayerDirective {
94
    /// Handle some new data that arrived on the network (such as blocks, transactions, and
95
    HandleNetResult(NetworkResult),
96
    /// A new burn block has been processed by the SortitionDB, check if this miner won sortition,
97
    ///  and if so, start the miner thread
98
    ProcessedBurnBlock(ConsensusHash, BurnchainHeaderHash, BlockHeaderHash),
99
    /// Either a new burn block has been processed (without a miner active yet) or a
100
    ///  nakamoto tenure's first block has been processed, so the relayer should issue
101
    ///  a block commit
102
    IssueBlockCommit(ConsensusHash, BlockHeaderHash),
103
    /// Try to register a VRF public key
104
    RegisterKey(BlockSnapshot),
105
    /// Stop the relayer thread
106
    Exit,
107
}
108

109
impl fmt::Display for RelayerDirective {
110
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
×
111
        match self {
×
112
            RelayerDirective::HandleNetResult(_) => write!(f, "HandleNetResult"),
×
113
            RelayerDirective::ProcessedBurnBlock(_, _, _) => write!(f, "ProcessedBurnBlock"),
×
114
            RelayerDirective::IssueBlockCommit(_, _) => write!(f, "IssueBlockCommit"),
×
115
            RelayerDirective::RegisterKey(_) => write!(f, "RegisterKey"),
×
116
            RelayerDirective::Exit => write!(f, "Exit"),
×
117
        }
118
    }
×
119
}
120

121
/// Last commitment data
122
/// This represents the tenure that the last-sent block-commit committed to.
123
pub struct LastCommit {
124
    /// block-commit sent
125
    block_commit: LeaderBlockCommitOp,
126
    /// the sortition tip at the time the block-commit was sent
127
    burn_tip: BlockSnapshot,
128
    /// the stacks tip at the time the block-commit was sent
129
    stacks_tip: StacksBlockId,
130
    /// the tenure consensus hash for the tip's tenure
131
    tenure_consensus_hash: ConsensusHash,
132
    /// the start-block hash of the tip's tenure
133
    #[allow(dead_code)]
134
    start_block_hash: BlockHeaderHash,
135
    /// What is the epoch in which this was sent?
136
    epoch_id: StacksEpochId,
137
    /// commit txid (to be filled in on submission)
138
    txid: Option<Txid>,
139
}
140

141
/// Timer used to check whether or not a burnchain view change has
142
///  waited long enough to issue a burn commit without a tenure change
143
enum BurnBlockCommitTimer {
144
    /// The timer hasn't been set: we aren't currently waiting to submit a commit
145
    NotSet,
146
    /// The timer is set, and has been set for a particular burn view
147
    Set {
148
        start_time: Instant,
149
        /// This is the canonical sortition at the time that the
150
        ///  timer began. This is used to make sure we aren't reusing
151
        ///  the timeout between sortitions
152
        burn_tip: ConsensusHash,
153
    },
154
}
155

156
impl BurnBlockCommitTimer {
157
    /// Check if the timer has expired (and was set).
158
    /// If the timer was not set, then set it.
159
    ///
160
    /// Returns true if the timer expired
161
    fn is_ready(&mut self, current_burn_tip: &ConsensusHash, timeout: &Duration) -> bool {
44,994✔
162
        let needs_reset = match self {
44,994✔
163
            BurnBlockCommitTimer::NotSet => true,
227✔
164
            BurnBlockCommitTimer::Set {
165
                start_time,
44,767✔
166
                burn_tip,
44,767✔
167
            } => {
168
                if burn_tip != current_burn_tip {
44,767✔
169
                    true
1,036✔
170
                } else {
171
                    if start_time.elapsed() > *timeout {
43,731✔
172
                        // timer expired and was pointed at the correct burn tip
173
                        // so we can just return is_ready here
174
                        return true;
8,286✔
175
                    }
35,445✔
176
                    // timer didn't expire, but the burn tip was correct, so
177
                    //  we don't need to reset the timer
178
                    false
35,445✔
179
                }
180
            }
181
        };
182
        if needs_reset {
36,708✔
183
            info!(
1,263✔
184
                "Starting new tenure timeout";
185
                "timeout_secs" => timeout.as_secs(),
1,263✔
186
                "burn_tip_ch" => %current_burn_tip
187
            );
188
            *self = Self::Set {
1,263✔
189
                burn_tip: current_burn_tip.clone(),
1,263✔
190
                start_time: Instant::now(),
1,263✔
191
            };
1,263✔
192
        }
35,445✔
193

194
        debug!(
36,708✔
195
            "Waiting for tenure timeout before issuing commit";
196
            "elapsed_secs" => self.elapsed_secs(),
×
197
            "burn_tip_ch" => %current_burn_tip
198
        );
199

200
        false
36,708✔
201
    }
44,994✔
202

203
    /// At what time, if set, would this timer be ready?
204
    fn deadline(&self, timeout: &Duration) -> Option<Instant> {
36,704✔
205
        match self {
36,704✔
206
            BurnBlockCommitTimer::NotSet => None,
×
207
            BurnBlockCommitTimer::Set { start_time, .. } => Some(*start_time + *timeout),
36,704✔
208
        }
209
    }
36,704✔
210

211
    /// How much time has elapsed on the current timer?
212
    fn elapsed_secs(&self) -> u64 {
1✔
213
        match self {
1✔
214
            BurnBlockCommitTimer::NotSet => 0,
1✔
215
            BurnBlockCommitTimer::Set { start_time, .. } => start_time.elapsed().as_secs(),
×
216
        }
217
    }
1✔
218
}
219

220
impl LastCommit {
221
    pub fn new(
2,954✔
222
        commit: LeaderBlockCommitOp,
2,954✔
223
        burn_tip: BlockSnapshot,
2,954✔
224
        stacks_tip: StacksBlockId,
2,954✔
225
        tenure_consensus_hash: ConsensusHash,
2,954✔
226
        start_block_hash: BlockHeaderHash,
2,954✔
227
        epoch_id: StacksEpochId,
2,954✔
228
    ) -> Self {
2,954✔
229
        Self {
2,954✔
230
            block_commit: commit,
2,954✔
231
            burn_tip,
2,954✔
232
            stacks_tip,
2,954✔
233
            tenure_consensus_hash,
2,954✔
234
            start_block_hash,
2,954✔
235
            epoch_id,
2,954✔
236
            txid: None,
2,954✔
237
        }
2,954✔
238
    }
2,954✔
239

240
    /// Get the commit
241
    pub fn get_block_commit(&self) -> &LeaderBlockCommitOp {
2,954✔
242
        &self.block_commit
2,954✔
243
    }
2,954✔
244

245
    /// What's the parent tenure's tenure-start block hash?
246
    pub fn parent_tenure_id(&self) -> StacksBlockId {
×
247
        StacksBlockId(self.block_commit.block_header_hash.0)
×
248
    }
×
249

250
    /// What's the stacks tip at the time of commit?
251
    pub fn get_stacks_tip(&self) -> &StacksBlockId {
×
252
        &self.stacks_tip
×
253
    }
×
254

255
    /// What's the burn tip at the time of commit?
256
    pub fn get_burn_tip(&self) -> &BlockSnapshot {
186,640✔
257
        &self.burn_tip
186,640✔
258
    }
186,640✔
259

260
    /// What's the epoch in which this was sent?
261
    pub fn get_epoch_id(&self) -> &StacksEpochId {
2,954✔
262
        &self.epoch_id
2,954✔
263
    }
2,954✔
264

265
    /// Get the tenure ID of the tenure this commit builds on
266
    pub fn get_tenure_id(&self) -> &ConsensusHash {
231,701✔
267
        &self.tenure_consensus_hash
231,701✔
268
    }
231,701✔
269

270
    /// Set our txid
271
    pub fn set_txid(&mut self, txid: &Txid) {
1,721✔
272
        self.txid = Some(txid.clone());
1,721✔
273
    }
1,721✔
274
}
275

276
pub type MinerThreadJoinHandle = JoinHandle<Result<(), NakamotoNodeError>>;
277

278
/// Miner thread join handle, as well as an "abort" flag to force the miner thread to exit when it
279
/// is blocked.
280
pub struct MinerStopHandle {
281
    /// The join handle itself
282
    join_handle: MinerThreadJoinHandle,
283
    /// The relayer-set abort flag
284
    abort_flag: Arc<AtomicBool>,
285
}
286

287
impl MinerStopHandle {
288
    pub fn new(join_handle: MinerThreadJoinHandle, abort_flag: Arc<AtomicBool>) -> Self {
1,580✔
289
        Self {
1,580✔
290
            join_handle,
1,580✔
291
            abort_flag,
1,580✔
292
        }
1,580✔
293
    }
1,580✔
294

295
    /// Get a ref to the inner thread object
296
    pub fn inner_thread(&self) -> &std::thread::Thread {
1,561✔
297
        self.join_handle.thread()
1,561✔
298
    }
1,561✔
299

300
    /// Destroy this stop handle to get the thread join handle
301
    pub fn into_inner(self) -> MinerThreadJoinHandle {
1,347✔
302
        self.join_handle
1,347✔
303
    }
1,347✔
304

305
    /// Stop the inner miner thread.
306
    /// Blocks the miner, and sets the abort flag so that a blocked miner will error out.
307
    pub fn stop(self, globals: &Globals) -> Result<(), NakamotoNodeError> {
1,347✔
308
        let my_id = thread::current().id();
1,347✔
309
        let prior_thread_id = self.inner_thread().id();
1,347✔
310
        debug!(
1,347✔
311
            "[Thread {:?}]: Stopping prior miner thread ID {:?}",
312
            &my_id, &prior_thread_id
×
313
        );
314

315
        self.abort_flag.store(true, Ordering::SeqCst);
1,347✔
316
        globals.block_miner();
1,347✔
317

318
        let prior_miner = self.into_inner();
1,347✔
319
        let prior_miner_result = prior_miner.join().map_err(|_| {
1,347✔
320
            error!("Miner: failed to join prior miner");
×
321
            ChainstateError::MinerAborted
×
322
        })?;
×
323
        debug!("Stopped prior miner thread ID {:?}", &prior_thread_id);
1,347✔
324
        if let Err(e) = prior_miner_result {
1,347✔
325
            // it's okay if the prior miner thread exited with an error.
326
            // in many cases this is expected (i.e., a burnchain block occurred)
327
            // if some error condition should be handled though, this is the place
328
            //  to do that handling.
329
            debug!("Prior mining thread exited with: {e:?}");
1,156✔
330
        }
191✔
331

332
        globals.unblock_miner();
1,347✔
333
        Ok(())
1,347✔
334
    }
1,347✔
335
}
336

337
/// The reason for issuing a tenure extend
338
#[derive(PartialEq, Eq, Debug, Clone)]
339
pub enum TenureExtendReason {
340
    /// There was an empty sortition
341
    EmptySortition,
342
    /// There was a bad sortition winner
343
    BadSortitionWinner,
344
    /// We are waiting for the current winner to produce a block.
345
    UnresponsiveWinner,
346
}
347

348
/// Information necessary to determine when to extend a tenure
349
#[derive(Clone)]
350
pub struct TenureExtendTime {
351
    /// The time at which we determined that we should tenure-extend
352
    time: Instant,
353
    /// The amount of time we should wait before tenure-extending
354
    timeout: Duration,
355
    /// The reason for tenure-extending
356
    reason: TenureExtendReason,
357
}
358

359
impl TenureExtendTime {
360
    /// Create a new `TenureExtendTime` for an UnresponsiveWinner with the specified `timeout`
361
    pub fn unresponsive_winner(timeout: Duration) -> Self {
54✔
362
        Self {
54✔
363
            time: Instant::now(),
54✔
364
            timeout,
54✔
365
            reason: TenureExtendReason::UnresponsiveWinner,
54✔
366
        }
54✔
367
    }
54✔
368

369
    /// Create a new `TenureExtendTime` with the provided `reason` and no `timeout`
370
    pub fn immediate(reason: TenureExtendReason) -> Self {
55✔
371
        Self {
55✔
372
            time: Instant::now(),
55✔
373
            timeout: Duration::from_millis(0),
55✔
374
            reason,
55✔
375
        }
55✔
376
    }
55✔
377

378
    /// Should we attempt to tenure-extend?
379
    pub fn should_extend(&self) -> bool {
20,251✔
380
        // We set the time, but have we waited long enough?
381
        self.time.elapsed() > self.timeout
20,251✔
382
    }
20,251✔
383

384
    // Amount of time elapsed since we decided to tenure-extend
385
    pub fn elapsed(&self) -> Duration {
×
386
        self.time.elapsed()
×
387
    }
×
388

389
    // The timeout specified when we decided to tenure-extend
390
    pub fn timeout(&self) -> Duration {
×
391
        self.timeout
×
392
    }
×
393

394
    /// The reason for tenure-extending
395
    pub fn reason(&self) -> &TenureExtendReason {
352✔
396
        &self.reason
352✔
397
    }
352✔
398

399
    /// Update the timeout for this `TenureExtendTime` and reset the time
400
    pub fn refresh(&mut self, timeout: Duration) {
352✔
401
        self.timeout = timeout;
352✔
402
        self.time = Instant::now();
352✔
403
    }
352✔
404
}
405

406
/// Relayer thread
407
/// * accepts network results and stores blocks and microblocks
408
/// * forwards new blocks, microblocks, and transactions to the p2p thread
409
/// * issues (and re-issues) block commits to participate as a miner
410
/// * processes burnchain state to determine if selected as a miner
411
/// * if mining, runs the miner and broadcasts blocks (via a subordinate MinerThread)
412
pub struct RelayerThread {
413
    /// Node config
414
    pub(crate) config: Config,
415
    /// Handle to the sortition DB
416
    sortdb: SortitionDB,
417
    /// Handle to the chainstate DB
418
    chainstate: StacksChainState,
419
    /// Handle to the mempool DB
420
    mempool: MemPoolDB,
421
    /// Handle to global state and inter-thread communication channels
422
    pub(crate) globals: Globals,
423
    /// Authoritative copy of the keychain state
424
    pub(crate) keychain: Keychain,
425
    /// Burnchian configuration
426
    pub(crate) burnchain: Burnchain,
427
    /// height of last VRF key registration request
428
    last_vrf_key_burn_height: Option<u64>,
429
    /// client to the burnchain (used only for sending block-commits)
430
    pub(crate) bitcoin_controller: BitcoinRegtestController,
431
    /// client to the event dispatcher
432
    pub(crate) event_dispatcher: EventDispatcher,
433
    /// copy of the local peer state
434
    local_peer: LocalPeer,
435
    /// last observed burnchain block height from the p2p thread (obtained from network results)
436
    last_network_block_height: u64,
437
    /// time at which we observed a change in the network block height (epoch time in millis)
438
    last_network_block_height_ts: u128,
439
    /// last observed number of downloader state-machine passes from the p2p thread (obtained from
440
    /// network results)
441
    last_network_download_passes: u64,
442
    /// last observed number of inventory state-machine passes from the p2p thread (obtained from
443
    /// network results)
444
    last_network_inv_passes: u64,
445
    /// minimum number of downloader state-machine passes that must take place before mining (this
446
    /// is used to ensure that the p2p thread attempts to download new Stacks block data before
447
    /// this thread tries to mine a block)
448
    min_network_download_passes: u64,
449
    /// minimum number of inventory state-machine passes that must take place before mining (this
450
    /// is used to ensure that the p2p thread attempts to download new Stacks block data before
451
    /// this thread tries to mine a block)
452
    min_network_inv_passes: u64,
453

454
    /// Inner relayer instance for forwarding broadcasted data back to the p2p thread for dispatch
455
    /// to neighbors
456
    relayer: Relayer,
457

458
    /// handle to the subordinate miner thread
459
    miner_thread: Option<MinerStopHandle>,
460
    /// miner thread's burn view
461
    miner_thread_burn_view: Option<BlockSnapshot>,
462

463
    /// The relayer thread reads directives from the relay_rcv, but it also periodically wakes up
464
    ///  to check if it should issue a block commit or try to register a VRF key
465
    next_initiative: Instant,
466
    is_miner: bool,
467
    /// Information about the last-sent block commit, and the relayer's view of the chain at the
468
    /// time it was sent.
469
    last_committed: Option<LastCommit>,
470
    /// Timeout for waiting for the first block in a tenure before submitting a block commit
471
    new_tenure_timeout: BurnBlockCommitTimer,
472
    /// Time to wait before attempting a tenure extend
473
    tenure_extend_time: Option<TenureExtendTime>,
474
}
475

476
impl RelayerThread {
477
    /// Instantiate relayer thread.
478
    /// Uses `runloop` to obtain globals, config, and `is_miner`` status
479
    pub fn new(
245✔
480
        runloop: &RunLoop,
245✔
481
        local_peer: LocalPeer,
245✔
482
        relayer: Relayer,
245✔
483
        keychain: Keychain,
245✔
484
    ) -> RelayerThread {
245✔
485
        let config = runloop.config().clone();
245✔
486
        let globals = runloop.get_globals();
245✔
487
        let burn_db_path = config.get_burn_db_file_path();
245✔
488
        let is_miner = runloop.is_miner();
245✔
489

490
        let sortdb = SortitionDB::open(
245✔
491
            &burn_db_path,
245✔
492
            true,
493
            runloop.get_burnchain().pox_constants,
245✔
494
            Some(config.node.get_marf_opts()),
245✔
495
        )
496
        .expect("FATAL: failed to open burnchain DB");
245✔
497

498
        let chainstate =
245✔
499
            open_chainstate_with_faults(&config).expect("FATAL: failed to open chainstate DB");
245✔
500

501
        let mempool = config
245✔
502
            .connect_mempool_db()
245✔
503
            .expect("Database failure opening mempool");
245✔
504

505
        let bitcoin_controller = BitcoinRegtestController::new_dummy(config.clone());
245✔
506

507
        let next_initiative_delay = config.node.next_initiative_delay;
245✔
508

509
        RelayerThread {
245✔
510
            config,
245✔
511
            sortdb,
245✔
512
            chainstate,
245✔
513
            mempool,
245✔
514
            globals,
245✔
515
            keychain,
245✔
516
            burnchain: runloop.get_burnchain(),
245✔
517
            last_vrf_key_burn_height: None,
245✔
518
            bitcoin_controller,
245✔
519
            event_dispatcher: runloop.get_event_dispatcher(),
245✔
520
            local_peer,
245✔
521

245✔
522
            last_network_block_height: 0,
245✔
523
            last_network_block_height_ts: 0,
245✔
524
            last_network_download_passes: 0,
245✔
525
            min_network_download_passes: 0,
245✔
526
            last_network_inv_passes: 0,
245✔
527
            min_network_inv_passes: 0,
245✔
528

245✔
529
            relayer,
245✔
530

245✔
531
            miner_thread: None,
245✔
532
            miner_thread_burn_view: None,
245✔
533
            is_miner,
245✔
534
            next_initiative: Instant::now() + Duration::from_millis(next_initiative_delay),
245✔
535
            last_committed: None,
245✔
536
            new_tenure_timeout: BurnBlockCommitTimer::NotSet,
245✔
537
            tenure_extend_time: None,
245✔
538
        }
245✔
539
    }
245✔
540

541
    /// Get a handle to the p2p thread
542
    pub fn get_p2p_handle(&self) -> NetworkHandle {
1,366✔
543
        self.relayer.get_p2p_handle()
1,366✔
544
    }
1,366✔
545

546
    /// have we waited for the right conditions under which to start mining a block off of our
547
    /// chain tip?
548
    fn has_waited_for_latest_blocks(&self) -> bool {
221,816✔
549
        // a network download pass took place
550
        self.min_network_download_passes <= self.last_network_download_passes
221,816✔
551
        // we waited long enough for a download pass, but timed out waiting
552
        || self.last_network_block_height_ts + (self.config.node.wait_time_for_blocks as u128) < get_epoch_time_ms()
204,937✔
553
        // we're not supposed to wait at all
554
        || !self.config.miner.wait_for_block_download
9,937✔
555
    }
221,816✔
556

557
    /// Handle a NetworkResult from the p2p/http state machine.  Usually this is the act of
558
    /// * preprocessing and storing new blocks and microblocks
559
    /// * relaying blocks, microblocks, and transacctions
560
    /// * updating unconfirmed state views
561
    pub fn process_network_result(&mut self, mut net_result: NetworkResult) {
221,819✔
562
        debug!(
221,819✔
563
            "Relayer: Handle network result (from {})",
564
            net_result.burn_height
565
        );
566

567
        if self.last_network_block_height != net_result.burn_height {
221,819✔
568
            // burnchain advanced; disable mining until we also do a download pass.
1,796✔
569
            self.last_network_block_height = net_result.burn_height;
1,796✔
570
            self.min_network_download_passes = net_result.num_download_passes + 1;
1,796✔
571
            self.min_network_inv_passes = net_result.num_inv_sync_passes + 1;
1,796✔
572
            self.last_network_block_height_ts = get_epoch_time_ms();
1,796✔
573
        }
220,023✔
574

575
        let net_receipts = self
221,819✔
576
            .relayer
221,819✔
577
            .process_network_result(
221,819✔
578
                &self.local_peer,
221,819✔
579
                &mut net_result,
221,819✔
580
                &self.burnchain,
221,819✔
581
                &mut self.sortdb,
221,819✔
582
                &mut self.chainstate,
221,819✔
583
                &mut self.mempool,
221,819✔
584
                self.globals.sync_comms.get_ibd(),
221,819✔
585
                Some(&self.globals.coord_comms),
221,819✔
586
                Some(&self.event_dispatcher),
221,819✔
587
            )
588
            .expect("BUG: failure processing network results");
221,819✔
589

590
        if net_receipts.num_new_blocks > 0 {
221,819✔
591
            // if we received any new block data that could invalidate our view of the chain tip,
592
            // then stop mining until we process it
593
            debug!("Relayer: block mining to process newly-arrived blocks or microblocks");
15✔
594
            signal_mining_blocked(self.globals.get_miner_status());
15✔
595
        }
221,804✔
596

597
        let mempool_txs_added = net_receipts.mempool_txs_added.len();
221,819✔
598
        if mempool_txs_added > 0 {
221,819✔
599
            self.event_dispatcher
2,800✔
600
                .process_new_mempool_txs(net_receipts.mempool_txs_added);
2,800✔
601
        }
219,019✔
602

603
        // Dispatch retrieved attachments, if any.
604
        if net_result.has_attachments() {
221,819✔
605
            self.event_dispatcher
×
606
                .process_new_attachments(&net_result.attachments);
×
607
        }
221,819✔
608

609
        // resume mining if we blocked it, and if we've done the requisite download
610
        // passes
611
        self.last_network_download_passes = net_result.num_download_passes;
221,819✔
612
        self.last_network_inv_passes = net_result.num_inv_sync_passes;
221,819✔
613
        if self.has_waited_for_latest_blocks() {
221,819✔
614
            debug!("Relayer: did a download pass, so unblocking mining");
221,816✔
615
            signal_mining_ready(self.globals.get_miner_status());
221,816✔
616
        }
3✔
617
    }
221,819✔
618

619
    /// Choose a miner directive for a sortition with a winner.
620
    ///
621
    /// The decision process is a little tricky, because the right decision depends on:
622
    /// * whether or not we won the _given_ sortition (`sn`)
623
    /// * whether or not we won the sortition that started the ongoing Stacks tenure
624
    /// * whether or not the ongoing Stacks tenure is at or descended from the last-winning
625
    /// sortition
626
    ///
627
    /// Specifically:
628
    ///
629
    /// If we won the given sortition `sn`, then we can start mining immediately with a `BlockFound`
630
    /// tenure-change. The exception is if we won the sortition, but the sortition's winning commit
631
    /// does not commit to the ongoing tenure. In this case, we instead extend the current tenure.
632
    ///
633
    /// Otherwise, if we did not win `sn`, if we won the tenure which started the ongoing Stacks tenure
634
    /// (i.e. we're the active miner), then we _may_ start mining after a timeout _if_ the winning
635
    /// miner (not us) fails to submit a `BlockFound` tenure-change block for `sn`.
636
    fn choose_directive_sortition_with_winner(
1,395✔
637
        &mut self,
1,395✔
638
        sn: BlockSnapshot,
1,395✔
639
        mining_pkh: &Hash160,
1,395✔
640
        committed_index_hash: StacksBlockId,
1,395✔
641
    ) -> MinerDirective {
1,395✔
642
        let won_sortition = sn.miner_pk_hash.as_ref() == Some(mining_pkh);
1,395✔
643

644
        let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
1,395✔
645
            SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn())
1,395✔
646
                .expect("FATAL: failed to query sortition DB for stacks tip");
1,395✔
647
        let canonical_stacks_snapshot =
1,395✔
648
            SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &canonical_stacks_tip_ch)
1,395✔
649
                .expect("FATAL: failed to query sortiiton DB for epoch")
1,395✔
650
                .expect("FATAL: no sortition for canonical stacks tip");
1,395✔
651

652
        // If we won the sortition, ensure that the sortition's winning commit actually commits to
653
        // the ongoing tenure. If it does not (i.e. commit is "stale" and points to N-1 when we are
654
        // currently in N), and if we are also the ongoing tenure's miner, then we must not attempt
655
        // a tenure change (which would reorg our own signed blocks). Instead, we should immediately
656
        // extend the tenure.
657
        if won_sortition && !self.config.get_node_config(false).mock_mining {
1,395✔
658
            let canonical_stacks_tip =
1,188✔
659
                StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh);
1,188✔
660

661
            let commits_to_tip_tenure = Self::sortition_commits_to_stacks_tip_tenure(
1,188✔
662
                &mut self.chainstate,
1,188✔
663
                &canonical_stacks_tip,
1,188✔
664
                &canonical_stacks_snapshot,
1,188✔
665
                &sn,
1,188✔
666
            ).unwrap_or_else(|e| {
1,188✔
667
                warn!(
×
668
                    "Relayer: Failed to determine if winning sortition commits to current tenure: {e:?}";
669
                    "sortition_ch" => %sn.consensus_hash,
670
                    "stacks_tip_ch" => %canonical_stacks_tip_ch
671
                );
672
                false
×
673
            });
×
674

675
            if !commits_to_tip_tenure {
1,188✔
676
                let won_ongoing_tenure_sortition =
14✔
677
                    canonical_stacks_snapshot.miner_pk_hash.as_ref() == Some(mining_pkh);
14✔
678

679
                if won_ongoing_tenure_sortition {
14✔
680
                    info!(
4✔
681
                        "Relayer: Won sortition, but commit does not target ongoing tenure. Will extend instead of starting a new tenure.";
682
                        "winning_sortition" => %sn.consensus_hash,
683
                        "ongoing_tenure" => %canonical_stacks_snapshot.consensus_hash,
684
                        "commits_to_tip_tenure?" => commits_to_tip_tenure
4✔
685
                    );
686
                    // Extend tenure to the new burn view instead of attempting BlockFound
687
                    return MinerDirective::ContinueTenure {
4✔
688
                        new_burn_view: sn.consensus_hash,
4✔
689
                    };
4✔
690
                }
10✔
691
            }
1,174✔
692
        }
207✔
693

694
        if won_sortition || self.config.get_node_config(false).mock_mining {
1,391✔
695
            // a sortition happenend, and we won
696
            info!("Won sortition; begin tenure.";
1,191✔
697
                    "winning_sortition" => %sn.consensus_hash);
698
            return MinerDirective::BeginTenure {
1,191✔
699
                parent_tenure_start: committed_index_hash,
1,191✔
700
                burnchain_tip: sn.clone(),
1,191✔
701
                election_block: sn,
1,191✔
702
                late: false,
1,191✔
703
            };
1,191✔
704
        }
200✔
705

706
        // a sortition happened, but we didn't win. Check if we won the ongoing tenure.
707
        debug!(
200✔
708
            "Relayer: did not win sortition {}, so stopping tenure",
709
            &sn.sortition
×
710
        );
711

712
        let won_ongoing_tenure_sortition =
200✔
713
            canonical_stacks_snapshot.miner_pk_hash.as_ref() == Some(mining_pkh);
200✔
714
        if won_ongoing_tenure_sortition {
200✔
715
            // we won the current ongoing tenure, but not the most recent sortition. Should we attempt to extend immediately or wait for the incoming miner?
716
            if let Ok(has_higher) = Self::has_higher_sortition_commits_to_stacks_tip_tenure(
62✔
717
                &self.sortdb,
62✔
718
                &mut self.chainstate,
62✔
719
                &sn,
62✔
720
                &canonical_stacks_snapshot,
62✔
721
            ) {
62✔
722
                if has_higher {
62✔
723
                    debug!("Relayer: Did not win current sortition but won the prior valid sortition. Will attempt to extend tenure after allowing the new miner some time to come online.";
51✔
724
                            "tenure_extend_wait_timeout_ms" => self.config.miner.tenure_extend_wait_timeout.as_millis(),
×
725
                    );
726
                    self.tenure_extend_time = Some(TenureExtendTime::unresponsive_winner(
51✔
727
                        self.config.miner.tenure_extend_wait_timeout,
51✔
728
                    ));
51✔
729
                } else {
730
                    info!("Relayer: no valid sortition since our last winning sortition. Will extend tenure.");
11✔
731
                    self.tenure_extend_time = Some(TenureExtendTime::immediate(
11✔
732
                        TenureExtendReason::BadSortitionWinner,
11✔
733
                    ));
11✔
734
                }
735
            }
×
736
        }
138✔
737
        MinerDirective::StopTenure
200✔
738
    }
1,395✔
739

740
    /// Choose a miner directive for a sortition with no winner.
741
    ///
742
    /// The decision process is a little tricky, because the right decision depends on:
743
    /// * whether or not we won the sortition that started the ongoing Stacks tenure
744
    /// * whether or not we won the last sortition with a winner
745
    /// * whether or not the last sortition winner has produced a Stacks block
746
    /// * whether or not the ongoing Stacks tenure is at or descended from the last-winning
747
    /// sortition
748
    ///
749
    /// Find out who won the last sortition with a winner.  If it was us, and if we haven't yet
750
    /// submitted a `BlockFound` tenure-change for it (which can happen if this given sortition is
751
    /// from a flash block), then start mining immediately with a "late" `BlockFound` tenure, _and_
752
    /// prepare to start mining right afterwards with an `Extended` tenure-change so as to represent
753
    /// the given sortition `sn`'s burn view in the Stacks chain.
754
    ///
755
    /// Otherwise, if did not win the last-winning sortition, then check to see if we're the ongoing
756
    /// Stack's tenure's miner. If so, then we _may_ start mining after a timeout _if_ the winner of
757
    /// the last-good sortition (not us) fails to submit a `BlockFound` tenure-change block.
758
    /// This can happen if `sn` was a flash block, and the remote miner has yet to process it.
759
    ///
760
    /// We won't always be able to mine -- for example, this could be an empty sortition, but the
761
    /// parent block could be an epoch 2 block.  In this case, the right thing to do is to wait for
762
    /// the next block-commit.
763
    fn choose_directive_sortition_without_winner(
143✔
764
        &mut self,
143✔
765
        sn: BlockSnapshot,
143✔
766
        mining_pk: &Hash160,
143✔
767
    ) -> Option<MinerDirective> {
143✔
768
        let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
143✔
769
            SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn())
143✔
770
                .expect("FATAL: failed to query sortition DB for stacks tip");
143✔
771
        let canonical_stacks_snapshot =
143✔
772
            SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &canonical_stacks_tip_ch)
143✔
773
                .expect("FATAL: failed to query sortiiton DB for epoch")
143✔
774
                .expect("FATAL: no sortition for canonical stacks tip");
143✔
775

776
        // find out what epoch the Stacks tip is in.
777
        // If it's in epoch 2.x, then we must always begin a new tenure, but we can't do so
778
        // right now since this sortition has no winner.
779
        let cur_epoch = SortitionDB::get_stacks_epoch(
143✔
780
            self.sortdb.conn(),
143✔
781
            canonical_stacks_snapshot.block_height,
143✔
782
        )
783
        .expect("FATAL: failed to query sortition DB for epoch")
143✔
784
        .expect("FATAL: no epoch defined for existing sortition");
143✔
785

786
        if cur_epoch.epoch_id < StacksEpochId::Epoch30 {
143✔
787
            debug!(
×
788
                "As of sortition {}, there has not yet been a Nakamoto tip. Cannot mine.",
789
                &canonical_stacks_snapshot.consensus_hash
×
790
            );
791
            return None;
×
792
        }
143✔
793

794
        // find out who won the last non-empty sortition. It may have been us.
795
        let Ok(last_winning_snapshot) = Self::get_last_winning_snapshot(&self.sortdb, &sn)
143✔
796
            .inspect_err(|e| {
143✔
797
                warn!("Relayer: Failed to load last winning snapshot: {e:?}");
×
798
            })
×
799
        else {
800
            // this should be unreachable, but don't tempt fate.
801
            info!("Relayer: No prior snapshots have a winning sortition. Will not try to mine.");
×
802
            return None;
×
803
        };
804

805
        // Check if we won the last winning snapshot AND it commits to the ongoing tenure.
806
        let won_last_winning_snapshot =
143✔
807
            last_winning_snapshot.miner_pk_hash.as_ref() == Some(mining_pk);
143✔
808
        let canonical_stacks_tip =
143✔
809
            StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh);
143✔
810
        let commits_to_tip_tenure = Self::sortition_commits_to_stacks_tip_tenure(
143✔
811
            &mut self.chainstate,
143✔
812
            &canonical_stacks_tip,
143✔
813
            &canonical_stacks_snapshot,
143✔
814
            &last_winning_snapshot,
143✔
815
        ).unwrap_or_else(|e| {
143✔
816
            warn!(
×
817
                "Relayer: Failed to determine if last winning sortition commits to current tenure: {e:?}";
818
                "sortition_ch" => %sn.consensus_hash,
819
                "stacks_tip_ch" => %canonical_stacks_tip_ch
820
            );
821
            false
×
822
        });
×
823

824
        if (won_last_winning_snapshot && commits_to_tip_tenure)
143✔
825
            || self.config.get_node_config(false).mock_mining
99✔
826
        {
827
            debug!(
44✔
828
                "Relayer: we won the last winning sortition {}",
829
                &last_winning_snapshot.consensus_hash
×
830
            );
831

832
            if Self::need_block_found(&canonical_stacks_snapshot, &last_winning_snapshot) {
44✔
833
                info!(
44✔
834
                    "Relayer: will submit late BlockFound for {}",
835
                    &last_winning_snapshot.consensus_hash
44✔
836
                );
837
                // prepare to immediately extend after our BlockFound gets mined.
838
                self.tenure_extend_time = Some(TenureExtendTime::immediate(
44✔
839
                    TenureExtendReason::EmptySortition,
44✔
840
                ));
44✔
841
                return Some(MinerDirective::BeginTenure {
44✔
842
                    parent_tenure_start: StacksBlockId(
44✔
843
                        last_winning_snapshot.winning_stacks_block_hash.clone().0,
44✔
844
                    ),
44✔
845
                    burnchain_tip: sn,
44✔
846
                    election_block: last_winning_snapshot,
44✔
847
                    late: true,
44✔
848
                });
44✔
UNCOV
849
            }
×
UNCOV
850
            let tip_is_last_winning_snapshot = canonical_stacks_snapshot.block_height
×
UNCOV
851
                == last_winning_snapshot.block_height
×
UNCOV
852
                && canonical_stacks_snapshot.consensus_hash == last_winning_snapshot.consensus_hash;
×
853

UNCOV
854
            if tip_is_last_winning_snapshot {
×
855
                // this is the ongoing tenure snapshot. A BlockFound has already been issued. We
856
                // can instead opt to Extend immediately
UNCOV
857
                info!("Relayer: BlockFound already issued for the last winning sortition. Will extend tenure.");
×
UNCOV
858
                return Some(MinerDirective::ContinueTenure {
×
UNCOV
859
                    new_burn_view: sn.consensus_hash,
×
UNCOV
860
                });
×
861
            }
×
862
        }
99✔
863

864
        let won_ongoing_tenure_sortition =
99✔
865
            canonical_stacks_snapshot.miner_pk_hash.as_ref() == Some(mining_pk);
99✔
866
        if won_ongoing_tenure_sortition {
99✔
867
            info!("Relayer: No sortition, but we produced the canonical Stacks tip. Will extend tenure.");
94✔
868
            if !won_last_winning_snapshot {
94✔
869
                // delay trying to continue since the last snasphot with a sortition was won
870
                // by someone else -- there's a chance that this other miner will produce a
871
                // BlockFound in the interim.
872
                debug!("Relayer: Did not win last winning snapshot despite mining the ongoing tenure. Will attempt to extend tenure after allowing the new miner some time to produce a block.");
3✔
873
                self.tenure_extend_time = Some(TenureExtendTime::unresponsive_winner(
3✔
874
                    self.config.miner.tenure_extend_wait_timeout,
3✔
875
                ));
3✔
876
                return None;
3✔
877
            }
91✔
878
            return Some(MinerDirective::ContinueTenure {
91✔
879
                new_burn_view: sn.consensus_hash,
91✔
880
            });
91✔
881
        }
5✔
882

883
        info!("Relayer: No sortition, and we did not produce the last Stacks tip. Will not mine.");
5✔
884
        return None;
5✔
885
    }
143✔
886

887
    /// Determine if we the current tenure winner needs to issue a BlockFound.
888
    /// Assumes the caller has already checked that the last-winning snapshot was won by us.
889
    ///
890
    /// Returns true if the stacks tip's snapshot is an ancestor of the last-won sortition
891
    /// Returns false otherwise.
892
    fn need_block_found(
381✔
893
        canonical_stacks_snapshot: &BlockSnapshot,
381✔
894
        last_winning_snapshot: &BlockSnapshot,
381✔
895
    ) -> bool {
381✔
896
        // we won the last non-empty sortition. Has there been a BlockFound issued for it?
897
        // This would be true if the stacks tip's tenure is at or descends from this snapshot.
898
        // If there has _not_ been a BlockFound, then we should issue one.
899
        if canonical_stacks_snapshot.block_height > last_winning_snapshot.block_height {
381✔
900
            // stacks tip is ahead of this snapshot, so no BlockFound can be issued.
901
            test_debug!(
1✔
902
                "Stacks_tip_sn.block_height ({}) > last_winning_snapshot.block_height ({})",
903
                canonical_stacks_snapshot.block_height,
904
                last_winning_snapshot.block_height
905
            );
906
            false
1✔
907
        } else if canonical_stacks_snapshot.block_height == last_winning_snapshot.block_height
380✔
908
            && canonical_stacks_snapshot.consensus_hash == last_winning_snapshot.consensus_hash
20✔
909
        {
910
            // this is the ongoing tenure snapshot. A BlockFound has already been issued.
911
            test_debug!(
18✔
912
                "Ongoing tenure {} already represents last-winning snapshot",
913
                &canonical_stacks_snapshot.consensus_hash
×
914
            );
915
            false
18✔
916
        } else {
917
            // The stacks tip is behind the last-won sortition, so a BlockFound is still needed.
918
            true
362✔
919
        }
920
    }
381✔
921

922
    /// Given the pointer to a recently processed sortition, see if we won the sortition, and
923
    /// determine what miner action (if any) to take.
924
    ///
925
    /// Returns a directive to the relayer thread to either start, stop, or continue a tenure, if
926
    /// this sortition matches the sortition tip and we have a parent to build atop.
927
    ///
928
    /// Otherwise, returns None, meaning no action will be taken.
929
    // This method is covered by the e2e bitcoind tests, which do not show up
930
    //  in mutant coverage.
931
    #[cfg_attr(test, mutants::skip)]
932
    fn process_sortition(
4,423✔
933
        &mut self,
4,423✔
934
        consensus_hash: ConsensusHash,
4,423✔
935
        burn_hash: BurnchainHeaderHash,
4,423✔
936
        committed_index_hash: StacksBlockId,
4,423✔
937
    ) -> Option<MinerDirective> {
4,423✔
938
        let sn = SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &consensus_hash)
4,423✔
939
            .expect("FATAL: failed to query sortition DB")
4,423✔
940
            .expect("FATAL: unknown consensus hash");
4,423✔
941

942
        let was_winning_pkh = if let (Some(winning_pkh), Some(my_pkh)) = (
4,423✔
943
            sn.miner_pk_hash.as_ref(),
4,423✔
944
            self.get_mining_key_pkh().as_ref(),
4,423✔
945
        ) {
946
            winning_pkh == my_pkh
4,272✔
947
        } else {
948
            false
151✔
949
        };
950

951
        let won_sortition = sn.sortition && was_winning_pkh;
4,423✔
952
        if won_sortition {
4,423✔
953
            increment_stx_blocks_mined_counter();
3,370✔
954
        }
3,382✔
955
        self.globals.set_last_sortition(sn.clone());
4,423✔
956
        self.globals.counters.bump_blocks_processed();
4,423✔
957
        self.globals.counters.bump_sortitions_processed();
4,423✔
958

959
        // there may be a bufferred stacks block to process, so wake up the coordinator to check
960
        self.globals.coord_comms.announce_new_stacks_block();
4,423✔
961

962
        info!(
4,423✔
963
            "Relayer: Process sortition";
964
            "sortition_ch" => %consensus_hash,
965
            "burn_hash" => %burn_hash,
966
            "burn_height" => sn.block_height,
4,423✔
967
            "winning_txid" => %sn.winning_block_txid,
968
            "committed_parent" => %committed_index_hash,
969
            "won_sortition?" => won_sortition,
4,423✔
970
        );
971

972
        let cur_sn = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())
4,423✔
973
            .expect("FATAL: failed to query sortition DB");
4,423✔
974

975
        if cur_sn.consensus_hash != consensus_hash {
4,423✔
976
            info!("Relayer: Current sortition {} is ahead of processed sortition {consensus_hash}; taking no action", &cur_sn.consensus_hash);
2,651✔
977
            self.globals
2,651✔
978
                .raise_initiative("process_sortition".to_string());
2,651✔
979
            return None;
2,651✔
980
        }
1,772✔
981

982
        // Reset the tenure extend time
983
        self.tenure_extend_time = None;
1,772✔
984
        let Some(mining_pk) = self.get_mining_key_pkh() else {
1,772✔
985
            debug!("No mining key, will not mine");
×
986
            return None;
×
987
        };
988

989
        let epoch = SortitionDB::get_stacks_epoch(self.sortdb.conn(), sn.block_height)
1,772✔
990
            .expect("FATAL: epoch not found for current snapshot")
1,772✔
991
            .expect("FATAL: epoch not found for current snapshot");
1,772✔
992
        if !epoch.epoch_id.uses_nakamoto_blocks() {
1,772✔
993
            return None;
234✔
994
        }
1,538✔
995

996
        let directive_opt = if sn.sortition {
1,538✔
997
            Some(self.choose_directive_sortition_with_winner(sn, &mining_pk, committed_index_hash))
1,395✔
998
        } else {
999
            self.choose_directive_sortition_without_winner(sn, &mining_pk)
143✔
1000
        };
1001
        debug!(
1,538✔
1002
            "Relayer: Processed sortition {consensus_hash}: Miner directive is {directive_opt:?}"
1003
        );
1004
        directive_opt
1,538✔
1005
    }
4,423✔
1006

1007
    /// Constructs and returns a LeaderKeyRegisterOp out of the provided params
1008
    fn make_key_register_op(
38✔
1009
        vrf_public_key: VRFPublicKey,
38✔
1010
        consensus_hash: &ConsensusHash,
38✔
1011
        miner_pkh: &Hash160,
38✔
1012
    ) -> BlockstackOperationType {
38✔
1013
        BlockstackOperationType::LeaderKeyRegister(LeaderKeyRegisterOp {
38✔
1014
            public_key: vrf_public_key,
38✔
1015
            memo: miner_pkh.as_bytes().to_vec(),
38✔
1016
            consensus_hash: consensus_hash.clone(),
38✔
1017
            vtxindex: 0,
38✔
1018
            txid: Txid([0u8; 32]),
38✔
1019
            block_height: 0,
38✔
1020
            burn_header_hash: BurnchainHeaderHash::zero(),
38✔
1021
        })
38✔
1022
    }
38✔
1023

1024
    /// Create and broadcast a VRF public key registration transaction.
1025
    /// Returns true if we succeed in doing so; false if not.
1026
    pub fn rotate_vrf_and_register(&mut self, burn_block: &BlockSnapshot) {
38✔
1027
        if self.last_vrf_key_burn_height.is_some() {
38✔
1028
            // already in-flight
1029
            return;
×
1030
        }
38✔
1031
        let cur_epoch = SortitionDB::get_stacks_epoch(self.sortdb.conn(), burn_block.block_height)
38✔
1032
            .expect("FATAL: failed to query sortition DB")
38✔
1033
            .expect("FATAL: no epoch defined")
38✔
1034
            .epoch_id;
38✔
1035
        let (vrf_pk, _) = self.keychain.make_vrf_keypair(burn_block.block_height);
38✔
1036
        let burnchain_tip_consensus_hash = &burn_block.consensus_hash;
38✔
1037
        let miner_pkh = self.keychain.get_nakamoto_pkh();
38✔
1038

1039
        debug!(
38✔
1040
            "Submitting LeaderKeyRegister";
1041
            "vrf_pk" => vrf_pk.to_hex(),
×
1042
            "burn_block_height" => burn_block.block_height,
×
1043
            "miner_pkh" => miner_pkh.to_hex(),
×
1044
        );
1045

1046
        let op = Self::make_key_register_op(vrf_pk, burnchain_tip_consensus_hash, &miner_pkh);
38✔
1047

1048
        let mut op_signer = self.keychain.generate_op_signer();
38✔
1049
        if let Ok(txid) = self
38✔
1050
            .bitcoin_controller
38✔
1051
            .submit_operation(cur_epoch, op, &mut op_signer)
38✔
1052
        {
38✔
1053
            // advance key registration state
38✔
1054
            self.last_vrf_key_burn_height = Some(burn_block.block_height);
38✔
1055
            self.globals
38✔
1056
                .set_pending_leader_key_registration(burn_block.block_height, txid);
38✔
1057
            self.globals.counters.bump_naka_submitted_vrfs();
38✔
1058
        }
38✔
1059
    }
38✔
1060

1061
    /// Produce the block-commit for this upcoming tenure, if we can.
1062
    ///
1063
    /// Takes the Nakamoto chain tip (consensus hash, block header hash).
1064
    ///
1065
    /// Returns the (the most recent burn snapshot, the most recent stakcs tip, the commit-op) on success
1066
    /// Returns None if we fail somehow.
1067
    ///
1068
    /// TODO: unit test
1069
    pub(crate) fn make_block_commit(
2,955✔
1070
        &mut self,
2,955✔
1071
        tip_block_ch: &ConsensusHash,
2,955✔
1072
        tip_block_bh: &BlockHeaderHash,
2,955✔
1073
    ) -> Result<LastCommit, NakamotoNodeError> {
2,955✔
1074
        let tip_block_id = StacksBlockId::new(tip_block_ch, tip_block_bh);
2,955✔
1075
        let sort_tip = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())
2,955✔
1076
            .map_err(|_| NakamotoNodeError::SnapshotNotFoundForChainTip)?;
2,955✔
1077

1078
        let stacks_tip = StacksBlockId::new(tip_block_ch, tip_block_bh);
2,955✔
1079

1080
        // sanity check -- this block must exist and have been processed locally
1081
        let highest_tenure_start_block_header = NakamotoChainState::get_tenure_start_block_header(
2,955✔
1082
            &mut self.chainstate.index_conn(),
2,955✔
1083
            &stacks_tip,
2,955✔
1084
            tip_block_ch,
2,955✔
1085
        )
1086
        .map_err(|e| {
2,955✔
1087
            error!(
×
1088
                "Relayer: Failed to get tenure-start block header for stacks tip {stacks_tip}: {e:?}"
1089
            );
1090
            NakamotoNodeError::ParentNotFound
×
1091
        })?
×
1092
        .ok_or_else(|| {
2,955✔
1093
            error!(
×
1094
                "Relayer: Failed to find tenure-start block header for stacks tip {stacks_tip}"
1095
            );
1096
            NakamotoNodeError::ParentNotFound
×
1097
        })?;
×
1098

1099
        // load the VRF proof generated in this tenure, so we can use it to seed the VRF in the
1100
        // upcoming tenure.  This may be an epoch2x VRF proof.
1101
        let tip_vrf_proof = NakamotoChainState::get_block_vrf_proof(
2,955✔
1102
            &mut self.chainstate.index_conn(),
2,955✔
1103
            &stacks_tip,
2,955✔
1104
            tip_block_ch,
2,955✔
1105
        )
1106
        .map_err(|e| {
2,955✔
1107
            error!("Failed to load VRF proof for {tip_block_ch} off of {stacks_tip}: {e:?}");
×
1108
            NakamotoNodeError::ParentNotFound
×
1109
        })?
×
1110
        .ok_or_else(|| {
2,955✔
1111
            error!("No block VRF proof for {tip_block_ch} off of {stacks_tip}");
×
1112
            NakamotoNodeError::ParentNotFound
×
1113
        })?;
×
1114

1115
        // let's figure out the recipient set!
1116
        let recipients = get_nakamoto_next_recipients(
2,955✔
1117
            &sort_tip,
2,955✔
1118
            &mut self.sortdb,
2,955✔
1119
            &mut self.chainstate,
2,955✔
1120
            &stacks_tip,
2,955✔
1121
            &self.burnchain,
2,955✔
1122
        )
1123
        .map_err(|e| {
2,955✔
1124
            error!("Relayer: Failure fetching recipient set: {e:?}");
×
1125
            NakamotoNodeError::SnapshotNotFoundForChainTip
×
1126
        })?;
×
1127

1128
        let commit_outs = if self
2,955✔
1129
            .burnchain
2,955✔
1130
            .is_in_prepare_phase(sort_tip.block_height + 1)
2,955✔
1131
        {
1132
            vec![PoxAddress::standard_burn_address(self.config.is_mainnet())]
457✔
1133
        } else {
1134
            RewardSetInfo::into_commit_outs(recipients, self.config.is_mainnet())
2,498✔
1135
        };
1136

1137
        // find the sortition that kicked off this tenure (it may be different from the sortition
1138
        // tip, such as when there is no sortition or when the miner of the current sortition never
1139
        // produces a block).  This is used to find the parent block-commit of the block-commit
1140
        // we'll submit.
1141
        let Ok(Some(tip_tenure_sortition)) =
2,955✔
1142
            SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), tip_block_ch)
2,955✔
1143
        else {
1144
            error!("Relayer: Failed to lookup the block snapshot of highest tenure ID"; "tenure_consensus_hash" => %tip_block_ch);
×
1145
            return Err(NakamotoNodeError::ParentNotFound);
×
1146
        };
1147

1148
        // find the parent block-commit of this commit, so we can find the parent vtxindex
1149
        // if the parent is a shadow block, then the vtxindex would be 0.
1150
        let commit_parent_block_burn_height = tip_tenure_sortition.block_height;
2,955✔
1151
        let commit_parent_winning_vtxindex = if let Ok(Some(parent_winning_tx)) =
2,955✔
1152
            SortitionDB::get_block_commit(
2,955✔
1153
                self.sortdb.conn(),
2,955✔
1154
                &tip_tenure_sortition.winning_block_txid,
2,955✔
1155
                &tip_tenure_sortition.sortition_id,
2,955✔
1156
            ) {
1157
            parent_winning_tx.vtxindex
2,952✔
1158
        } else {
1159
            debug!(
3✔
1160
                "{}/{} ({}) must be a shadow block, since it has no block-commit",
1161
                &tip_block_bh, &tip_block_ch, &tip_block_id
×
1162
            );
1163
            let Ok(Some(parent_version)) =
3✔
1164
                NakamotoChainState::get_nakamoto_block_version(self.chainstate.db(), &tip_block_id)
3✔
1165
            else {
1166
                error!(
×
1167
                    "Relayer: Failed to lookup block version of {}",
1168
                    &tip_block_id
×
1169
                );
1170
                return Err(NakamotoNodeError::ParentNotFound);
×
1171
            };
1172

1173
            if !NakamotoBlockHeader::is_shadow_block_version(parent_version) {
3✔
1174
                error!(
×
1175
                    "Relayer: parent block-commit of {} not found, and it is not a shadow block",
1176
                    &tip_block_id
×
1177
                );
1178
                return Err(NakamotoNodeError::ParentNotFound);
×
1179
            }
3✔
1180

1181
            0
3✔
1182
        };
1183

1184
        // epoch in which this commit will be sent (affects how the burnchain client processes it)
1185
        let Ok(Some(target_epoch)) =
2,955✔
1186
            SortitionDB::get_stacks_epoch(self.sortdb.conn(), sort_tip.block_height + 1)
2,955✔
1187
        else {
1188
            error!("Relayer: Failed to lookup its epoch"; "target_height" => sort_tip.block_height + 1);
×
1189
            return Err(NakamotoNodeError::SnapshotNotFoundForChainTip);
×
1190
        };
1191

1192
        let (_, burnchain_config) = self.check_burnchain_config_changed();
2,955✔
1193

1194
        // let's commit, but target the current burnchain tip with our modulus so the commit is
1195
        // only valid if it lands in the targeted burnchain block height
1196
        let burn_parent_modulus = u8::try_from(sort_tip.block_height % BURN_BLOCK_MINED_AT_MODULUS)
2,955✔
1197
            .map_err(|_| {
2,955✔
1198
                error!("Relayer: Block mining modulus is not u8");
×
1199
                NakamotoNodeError::UnexpectedChainState
×
1200
            })?;
×
1201

1202
        // burnchain signer for this commit
1203
        let sender = self.keychain.get_burnchain_signer();
2,955✔
1204

1205
        // VRF key this commit uses (i.e. the one we registered)
1206
        let key = self
2,955✔
1207
            .globals
2,955✔
1208
            .get_leader_key_registration_state()
2,955✔
1209
            .get_active()
2,955✔
1210
            .ok_or_else(|| NakamotoNodeError::NoVRFKeyActive)?;
2,955✔
1211

1212
        let mut commit = LeaderBlockCommitOp {
2,955✔
1213
            // NOTE: to be filled in
2,955✔
1214
            treatment: vec![],
2,955✔
1215
            // NOTE: PoX sunset has been disabled prior to taking effect
2,955✔
1216
            sunset_burn: 0,
2,955✔
1217
            // block-commits in Nakamoto commit to the ongoing tenure's tenure-start block (which,
2,955✔
1218
            // when processed, become the start-block of the tenure atop which this miner will
2,955✔
1219
            // produce blocks)
2,955✔
1220
            block_header_hash: BlockHeaderHash(
2,955✔
1221
                highest_tenure_start_block_header.index_block_hash().0,
2,955✔
1222
            ),
2,955✔
1223
            // the rest of this is the same as epoch2x commits, modulo the new epoch marker
2,955✔
1224
            burn_fee: burnchain_config.burn_fee_cap,
2,955✔
1225
            apparent_sender: sender,
2,955✔
1226
            key_block_ptr: u32::try_from(key.block_height)
2,955✔
1227
                .expect("FATAL: burn block height exceeded u32"),
2,955✔
1228
            key_vtxindex: u16::try_from(key.op_vtxindex).expect("FATAL: vtxindex exceeded u16"),
2,955✔
1229
            memo: vec![STACKS_EPOCH_LATEST_MARKER],
2,955✔
1230
            new_seed: VRFSeed::from_proof(&tip_vrf_proof),
2,955✔
1231
            parent_block_ptr: u32::try_from(commit_parent_block_burn_height)
2,955✔
1232
                .expect("FATAL: burn block height exceeded u32"),
2,955✔
1233
            parent_vtxindex: u16::try_from(commit_parent_winning_vtxindex)
2,955✔
1234
                .expect("FATAL: vtxindex exceeded u16"),
2,955✔
1235
            burn_parent_modulus,
2,955✔
1236
            commit_outs,
2,955✔
1237

2,955✔
1238
            // NOTE: to be filled in
2,955✔
1239
            input: (Txid([0; 32]), 0),
2,955✔
1240
            vtxindex: 0,
2,955✔
1241
            txid: Txid([0u8; 32]),
2,955✔
1242
            block_height: 0,
2,955✔
1243
            burn_header_hash: BurnchainHeaderHash::zero(),
2,955✔
1244
        };
2,955✔
1245

1246
        if std::env::var("FAULT_INJECTION_BLOCK_COMMIT_VTXINDEX_SENTINEL") == Ok("1".to_string()) {
2,955✔
1247
            info!("Zeroing parent_vtxindex");
2✔
1248
            commit.parent_vtxindex = 0;
2✔
1249
        }
2,953✔
1250

1251
        if std::env::var("FAULT_INJECTION_BLOCK_COMMIT_PARENT_SENTINEL") == Ok("1".to_string()) {
2,955✔
1252
            info!("Altering parent_block_ptr");
2✔
1253
            commit.parent_block_ptr = commit.parent_block_ptr.saturating_sub(1);
2✔
1254

1255
            let parent_tenure_tip_id = highest_tenure_start_block_header
2✔
1256
                .anchored_header
2✔
1257
                .as_stacks_nakamoto()
2✔
1258
                .unwrap()
2✔
1259
                .parent_block_id
2✔
1260
                .clone();
2✔
1261

1262
            let parent_tenure_tip =
2✔
1263
                NakamotoChainState::get_block_header(&self.chainstate.db(), &parent_tenure_tip_id)
2✔
1264
                    .unwrap()
2✔
1265
                    .unwrap();
2✔
1266

1267
            let parent_tip_vrf_proof = NakamotoChainState::get_block_vrf_proof(
2✔
1268
                &mut self.chainstate.index_conn(),
2✔
1269
                &stacks_tip,
2✔
1270
                &parent_tenure_tip.consensus_hash,
2✔
1271
            )
1272
            .unwrap()
2✔
1273
            .unwrap();
2✔
1274

1275
            info!(
2✔
1276
                "Altering new_seed from {} to {}",
1277
                &commit.new_seed,
2✔
1278
                &VRFSeed::from_proof(&parent_tip_vrf_proof)
2✔
1279
            );
1280
            commit.new_seed = VRFSeed::from_proof(&parent_tip_vrf_proof);
2✔
1281
        }
2,953✔
1282

1283
        Ok(LastCommit::new(
2,955✔
1284
            commit,
2,955✔
1285
            sort_tip,
2,955✔
1286
            stacks_tip,
2,955✔
1287
            highest_tenure_start_block_header.consensus_hash,
2,955✔
1288
            highest_tenure_start_block_header
2,955✔
1289
                .anchored_header
2,955✔
1290
                .block_hash(),
2,955✔
1291
            target_epoch.epoch_id,
2,955✔
1292
        ))
2,955✔
1293
    }
2,955✔
1294

1295
    #[cfg(test)]
1296
    fn fault_injection_stall_miner_startup() {
1,366✔
1297
        if TEST_MINER_THREAD_STALL.get() {
1,366✔
1298
            // Do an extra check just so we don't log EVERY time.
UNCOV
1299
            warn!("Relayer miner thread startup is stalled due to testing directive to stall the miner");
×
UNCOV
1300
            while TEST_MINER_THREAD_STALL.get() {
×
UNCOV
1301
                std::thread::sleep(std::time::Duration::from_millis(10));
×
UNCOV
1302
            }
×
UNCOV
1303
            warn!(
×
1304
                "Relayer miner thread startup is no longer stalled due to testing directive. Continuing..."
1305
            );
1306
        }
1,366✔
1307
    }
1,366✔
1308

1309
    #[cfg(not(test))]
1310
    fn fault_injection_stall_miner_startup() {}
1311

1312
    #[cfg(test)]
1313
    fn fault_injection_stall_miner_thread_startup() {
1,366✔
1314
        if TEST_MINER_THREAD_START_STALL.get() {
1,366✔
1315
            // Do an extra check just so we don't log EVERY time.
UNCOV
1316
            warn!("Miner thread startup is stalled due to testing directive");
×
UNCOV
1317
            while TEST_MINER_THREAD_START_STALL.get() {
×
UNCOV
1318
                std::thread::sleep(std::time::Duration::from_millis(10));
×
UNCOV
1319
            }
×
UNCOV
1320
            warn!(
×
1321
                "Miner thread startup is no longer stalled due to testing directive. Continuing..."
1322
            );
1323
        }
1,366✔
1324
    }
1,366✔
1325

1326
    #[cfg(not(test))]
1327
    fn fault_injection_stall_miner_thread_startup() {}
1328

1329
    /// Create the block miner thread state.
1330
    /// Only proceeds if all of the following are true:
1331
    /// * the miner is not blocked
1332
    /// * last_burn_block corresponds to the canonical sortition DB's chain tip
1333
    /// * the time of issuance is sufficiently recent
1334
    /// * there are no unprocessed stacks blocks in the staging DB
1335
    /// * the relayer has already tried a download scan that included this sortition (which, if a block was found, would have placed it into the staging DB and marked it as unprocessed)
1336
    /// * a miner thread is not running already
1337
    fn create_block_miner(
1,366✔
1338
        &mut self,
1,366✔
1339
        registered_key: RegisteredKey,
1,366✔
1340
        burn_election_block: BlockSnapshot,
1,366✔
1341
        burn_tip: BlockSnapshot,
1,366✔
1342
        parent_tenure_id: StacksBlockId,
1,366✔
1343
        reason: MinerReason,
1,366✔
1344
        burn_tip_at_start: &ConsensusHash,
1,366✔
1345
    ) -> Result<BlockMinerThread, NakamotoNodeError> {
1,366✔
1346
        if fault_injection_skip_mining(&self.config.node.rpc_bind, burn_tip.block_height) {
1,366✔
UNCOV
1347
            debug!(
×
1348
                "Relayer: fault injection skip mining at block height {}",
1349
                burn_tip.block_height
1350
            );
UNCOV
1351
            return Err(NakamotoNodeError::FaultInjection);
×
1352
        }
1,366✔
1353
        Self::fault_injection_stall_miner_startup();
1,366✔
1354

1355
        let burn_header_hash = burn_tip.burn_header_hash.clone();
1,366✔
1356
        let burn_chain_sn = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())
1,366✔
1357
            .expect("FATAL: failed to query sortition DB for canonical burn chain tip");
1,366✔
1358

1359
        let burn_chain_tip = burn_chain_sn.burn_header_hash.clone();
1,366✔
1360

1361
        if &burn_chain_sn.consensus_hash != burn_tip_at_start {
1,366✔
UNCOV
1362
            info!(
×
1363
                "Relayer: Drop stale RunTenure for {burn_header_hash}: current sortition is for {burn_chain_tip}"
1364
            );
UNCOV
1365
            self.globals.counters.bump_missed_tenures();
×
UNCOV
1366
            return Err(NakamotoNodeError::MissedMiningOpportunity);
×
1367
        }
1,366✔
1368

1369
        debug!(
1,366✔
1370
            "Relayer: Spawn tenure thread";
UNCOV
1371
            "height" => burn_tip.block_height,
×
1372
            "burn_header_hash" => %burn_header_hash,
1373
            "parent_tenure_id" => %parent_tenure_id,
1374
            "reason" => %reason,
1375
            "burn_election_block.consensus_hash" => %burn_election_block.consensus_hash,
1376
            "burn_tip.consensus_hash" => %burn_tip.consensus_hash,
1377
        );
1378

1379
        let miner_thread_state = BlockMinerThread::new(
1,366✔
1380
            self,
1,366✔
1381
            registered_key,
1,366✔
1382
            burn_election_block,
1,366✔
1383
            burn_tip.clone(),
1,366✔
1384
            parent_tenure_id,
1,366✔
1385
            burn_tip_at_start,
1,366✔
1386
            reason,
1,366✔
UNCOV
1387
        )?;
×
1388
        Ok(miner_thread_state)
1,366✔
1389
    }
1,366✔
1390

1391
    fn start_new_tenure(
1,366✔
1392
        &mut self,
1,366✔
1393
        parent_tenure_start: StacksBlockId,
1,366✔
1394
        block_election_snapshot: BlockSnapshot,
1,366✔
1395
        burn_tip: BlockSnapshot,
1,366✔
1396
        reason: MinerReason,
1,366✔
1397
        burn_tip_at_start: &ConsensusHash,
1,366✔
1398
    ) -> Result<(), NakamotoNodeError> {
1,366✔
1399
        // when starting a new tenure, block the mining thread if its currently running.
1400
        // the new mining thread will join it (so that the new mining thread stalls, not the relayer)
1401
        let prior_tenure_thread = self.miner_thread.take();
1,366✔
1402
        self.miner_thread_burn_view = None;
1,366✔
1403

1404
        let vrf_key = self
1,366✔
1405
            .globals
1,366✔
1406
            .get_leader_key_registration_state()
1,366✔
1407
            .get_active()
1,366✔
1408
            .ok_or_else(|| {
1,366✔
UNCOV
1409
                warn!("Trying to start new tenure, but no VRF key active");
×
UNCOV
1410
                NakamotoNodeError::NoVRFKeyActive
×
1411
            })?;
×
1412
        let new_miner_state = self.create_block_miner(
1,366✔
1413
            vrf_key,
1,366✔
1414
            block_election_snapshot,
1,366✔
1415
            burn_tip.clone(),
1,366✔
1416
            parent_tenure_start.clone(),
1,366✔
1417
            reason,
1,366✔
1418
            burn_tip_at_start,
1,366✔
UNCOV
1419
        )?;
×
1420
        let miner_abort_flag = new_miner_state.get_abort_flag();
1,366✔
1421

1422
        debug!("Relayer: starting new tenure thread");
1,366✔
1423

1424
        let rand_id = thread_rng().gen::<u32>();
1,366✔
1425
        let is_mock = if self.config.node.mock_mining {
1,366✔
1426
            "mock-"
7✔
1427
        } else {
1428
            ""
1,359✔
1429
        };
1430

1431
        let new_miner_handle = std::thread::Builder::new()
1,366✔
1432
            .name(format!("{is_mock}miner.{parent_tenure_start}.{rand_id}",))
1,366✔
1433
            .stack_size(BLOCK_PROCESSOR_STACK_SIZE)
1,366✔
1434
            .spawn(move || {
1,366✔
1435
                debug!(
1,366✔
1436
                    "New block miner thread ID is {:?}",
UNCOV
1437
                    std::thread::current().id()
×
1438
                );
1439
                Self::fault_injection_stall_miner_thread_startup();
1,366✔
1440
                if let Err(e) = new_miner_state.run_miner(prior_tenure_thread) {
1,366✔
1441
                    info!("Miner thread failed: {e:?}");
1,366✔
1442
                    Err(e)
1,366✔
1443
                } else {
1444
                    Ok(())
×
1445
                }
1446
            })
1,366✔
1447
            .map_err(|e| {
1,366✔
UNCOV
1448
                error!("Relayer: Failed to start tenure thread: {e:?}");
×
UNCOV
1449
                NakamotoNodeError::SpawnError(e)
×
UNCOV
1450
            })?;
×
1451
        debug!(
1,366✔
1452
            "Relayer: started tenure thread ID {:?}",
UNCOV
1453
            new_miner_handle.thread().id()
×
1454
        );
1455
        self.miner_thread
1,366✔
1456
            .replace(MinerStopHandle::new(new_miner_handle, miner_abort_flag));
1,366✔
1457
        self.miner_thread_burn_view.replace(burn_tip);
1,366✔
1458
        Ok(())
1,366✔
1459
    }
1,366✔
1460

1461
    fn stop_tenure(&mut self) -> Result<(), NakamotoNodeError> {
331✔
1462
        // when stopping a tenure, block the mining thread if its currently running, then join it.
1463
        // do this in a new thread will (so that the new thread stalls, not the relayer)
1464
        let Some(prior_tenure_thread) = self.miner_thread.take() else {
331✔
1465
            debug!("Relayer: no tenure thread to stop");
117✔
1466
            return Ok(());
117✔
1467
        };
1468
        self.miner_thread_burn_view = None;
214✔
1469

1470
        let id = prior_tenure_thread.inner_thread().id();
214✔
1471
        let abort_flag = prior_tenure_thread.abort_flag.clone();
214✔
1472
        let globals = self.globals.clone();
214✔
1473

1474
        let stop_handle = std::thread::Builder::new()
214✔
1475
            .name(format!(
214✔
1476
                "tenure-stop({:?})-{}",
1477
                id, self.local_peer.data_url
1478
            ))
1479
            .spawn(move || prior_tenure_thread.stop(&globals))
214✔
1480
            .map_err(|e| {
214✔
UNCOV
1481
                error!("Relayer: Failed to spawn a stop-tenure thread: {e:?}");
×
UNCOV
1482
                NakamotoNodeError::SpawnError(e)
×
1483
            })?;
×
1484

1485
        self.miner_thread
214✔
1486
            .replace(MinerStopHandle::new(stop_handle, abort_flag));
214✔
1487
        debug!("Relayer: stopped tenure thread ID {id:?}");
214✔
1488
        Ok(())
214✔
1489
    }
331✔
1490

1491
    /// Get the public key hash for the mining key.
1492
    fn get_mining_key_pkh(&self) -> Option<Hash160> {
6,547✔
1493
        let Some(ref mining_key) = self.config.miner.mining_key else {
6,547✔
UNCOV
1494
            return None;
×
1495
        };
1496
        Some(Hash160::from_node_public_key(
6,547✔
1497
            &StacksPublicKey::from_private(mining_key),
6,547✔
1498
        ))
6,547✔
1499
    }
6,547✔
1500

1501
    /// Helper method to get the last snapshot with a winner
1502
    fn get_last_winning_snapshot(
486✔
1503
        sortdb: &SortitionDB,
486✔
1504
        sort_tip: &BlockSnapshot,
486✔
1505
    ) -> Result<BlockSnapshot, NakamotoNodeError> {
486✔
1506
        let ih = sortdb.index_handle(&sort_tip.sortition_id);
486✔
1507
        Ok(ih.get_last_snapshot_with_sortition(sort_tip.block_height)?)
486✔
1508
    }
486✔
1509

1510
    /// Returns true if the sortition `sn` commits to the tenure start block of the ongoing Stacks tenure `stacks_tip_sn`.
1511
    /// Returns false otherwise.
1512
    fn sortition_commits_to_stacks_tip_tenure(
1,393✔
1513
        chain_state: &mut StacksChainState,
1,393✔
1514
        stacks_tip_id: &StacksBlockId,
1,393✔
1515
        stacks_tip_sn: &BlockSnapshot,
1,393✔
1516
        sn: &BlockSnapshot,
1,393✔
1517
    ) -> Result<bool, NakamotoNodeError> {
1,393✔
1518
        if !sn.sortition {
1,393✔
1519
            // definitely not a valid sortition
UNCOV
1520
            debug!("Relayer: Sortition {} is empty", &sn.consensus_hash);
×
UNCOV
1521
            return Ok(false);
×
1522
        }
1,393✔
1523
        // The sortition must commit to the tenure start block of the ongoing Stacks tenure.
1524
        let mut ic = chain_state.index_conn();
1,393✔
1525
        let parent_tenure_id = StacksBlockId(sn.winning_stacks_block_hash.clone().0);
1,393✔
1526
        let highest_tenure_start_block_header = NakamotoChainState::get_tenure_start_block_header(
1,393✔
1527
            &mut ic,
1,393✔
1528
            stacks_tip_id,
1,393✔
1529
            &stacks_tip_sn.consensus_hash,
1,393✔
UNCOV
1530
        )?
×
1531
        .ok_or_else(|| {
1,393✔
UNCOV
1532
            error!(
×
1533
                "Relayer: Failed to find tenure-start block header for stacks tip {stacks_tip_id}"
1534
            );
UNCOV
1535
            NakamotoNodeError::ParentNotFound
×
UNCOV
1536
        })?;
×
1537

1538
        let highest_tenure_start_block_id = highest_tenure_start_block_header.index_block_hash();
1,393✔
1539
        if highest_tenure_start_block_id != parent_tenure_id {
1,393✔
1540
            debug!("Relayer: Sortition {} is at the tip, but does not commit to {parent_tenure_id} so cannot be valid", &sn.consensus_hash;
121✔
1541
                "highest_tenure_start_block_header_block_id" => %highest_tenure_start_block_id);
1542
            return Ok(false);
121✔
1543
        }
1,272✔
1544

1545
        Ok(true)
1,272✔
1546
    }
1,393✔
1547

1548
    /// Determine the highest sortition higher than `elected_tenure_id`, but no higher than
1549
    /// `sort_tip` whose winning commit's parent tenure ID matches the `stacks_tip`,
1550
    /// and whose consensus hash matches the `stacks_tip`'s tenure ID.
1551
    ///
1552
    /// Returns Ok(true) if such a sortition is found, and is higher than that of
1553
    /// `elected_tenure_id`.
1554
    /// Returns Ok(false) if no such sortition is found.
1555
    /// Returns Err(..) on DB errors.
1556
    fn has_higher_sortition_commits_to_stacks_tip_tenure(
62✔
1557
        sortdb: &SortitionDB,
62✔
1558
        chain_state: &mut StacksChainState,
62✔
1559
        sortition_tip: &BlockSnapshot,
62✔
1560
        elected_tenure: &BlockSnapshot,
62✔
1561
    ) -> Result<bool, NakamotoNodeError> {
62✔
1562
        let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
62✔
1563
            SortitionDB::get_canonical_stacks_chain_tip_hash(sortdb.conn()).unwrap();
62✔
1564
        let canonical_stacks_tip =
62✔
1565
            StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh);
62✔
1566

1567
        let Ok(Some(canonical_stacks_tip_sn)) =
62✔
1568
            SortitionDB::get_block_snapshot_consensus(sortdb.conn(), &canonical_stacks_tip_ch)
62✔
1569
        else {
UNCOV
1570
            return Err(NakamotoNodeError::ParentNotFound);
×
1571
        };
1572

1573
        sortdb
62✔
1574
            .find_from(sortition_tip.clone(), |cursor| {
73✔
1575
                debug!(
73✔
1576
                    "Relayer: check sortition {} to see if it is valid",
UNCOV
1577
                    &cursor.consensus_hash
×
1578
                );
1579
                // have we reached the last tenure we're looking at?
1580
                if cursor.block_height <= elected_tenure.block_height {
73✔
1581
                    return Ok(FindIter::Halt);
11✔
1582
                }
62✔
1583

1584
                if Self::sortition_commits_to_stacks_tip_tenure(
62✔
1585
                    chain_state,
62✔
1586
                    &canonical_stacks_tip,
62✔
1587
                    &canonical_stacks_tip_sn,
62✔
1588
                    &cursor,
62✔
UNCOV
1589
                )? {
×
1590
                    return Ok(FindIter::Found(()));
51✔
1591
                }
11✔
1592

1593
                // nope. continue the search
1594
                return Ok(FindIter::Continue);
11✔
1595
            })
73✔
1596
            .map(|found| found.is_some())
62✔
1597
    }
62✔
1598

1599
    /// Attempt to continue a miner's tenure into the next burn block.
1600
    /// This is allowed if the miner won the last good sortition -- that is, the sortition which
1601
    /// elected the local view of the canonical Stacks fork's ongoing tenure.
1602
    /// Or if the miner won the last valid sortition prior to the current and the current miner
1603
    /// has failed to produce a block before the required timeout.
1604
    ///
1605
    /// This function assumes that the caller has checked that the sortition referred to by
1606
    /// `new_burn_view` does not have a sortition winner or that the winner has not produced a
1607
    /// valid block yet.
1608
    fn continue_tenure(&mut self, new_burn_view: ConsensusHash) -> Result<(), NakamotoNodeError> {
95✔
1609
        if let Err(e) = self.stop_tenure() {
95✔
UNCOV
1610
            error!("Relayer: Failed to stop tenure: {e:?}");
×
1611
            return Ok(());
×
1612
        }
95✔
1613
        debug!("Relayer: successfully stopped tenure; will try to continue.");
95✔
1614

1615
        // try to extend, but only if we aren't already running a thread for the current or newer
1616
        // burnchain view
1617
        let Ok(sn) =
95✔
1618
            SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn()).inspect_err(|e| {
95✔
UNCOV
1619
                error!("Relayer: failed to read canonical burnchain sortition: {e:?}");
×
UNCOV
1620
            })
×
1621
        else {
UNCOV
1622
            return Ok(());
×
1623
        };
1624

1625
        if let Some(miner_thread_burn_view) = self.miner_thread_burn_view.as_ref() {
95✔
1626
            // a miner thread is already running.  If its burn view is the same as the canonical
1627
            // tip, then do nothing
UNCOV
1628
            if sn.consensus_hash == miner_thread_burn_view.consensus_hash {
×
UNCOV
1629
                info!("Relayer: will not tenure extend -- the current miner thread's burn view matches the sortition tip"; "sortition tip" => %sn.consensus_hash);
×
UNCOV
1630
                return Ok(());
×
UNCOV
1631
            }
×
1632
        }
95✔
1633

1634
        // Get the necessary snapshots and state
1635
        let burn_tip =
95✔
1636
            SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &new_burn_view)?
95✔
1637
                .ok_or_else(|| {
95✔
UNCOV
1638
                    error!("Relayer: failed to get block snapshot for new burn view");
×
UNCOV
1639
                    NakamotoNodeError::SnapshotNotFoundForChainTip
×
UNCOV
1640
                })?;
×
1641
        let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
95✔
1642
            SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn()).unwrap();
95✔
1643
        let canonical_stacks_tip =
95✔
1644
            StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh);
95✔
1645
        let canonical_stacks_snapshot = SortitionDB::get_block_snapshot_consensus(
95✔
1646
            self.sortdb.conn(),
95✔
1647
            &canonical_stacks_tip_ch,
95✔
UNCOV
1648
        )?
×
1649
        .ok_or_else(|| {
95✔
UNCOV
1650
            error!("Relayer: failed to get block snapshot for canonical tip");
×
UNCOV
1651
            NakamotoNodeError::SnapshotNotFoundForChainTip
×
UNCOV
1652
        })?;
×
1653
        let reason = MinerReason::Extended {
95✔
1654
            burn_view_consensus_hash: new_burn_view.clone(),
95✔
1655
        };
95✔
1656

1657
        if let Err(e) = self.start_new_tenure(
95✔
1658
            canonical_stacks_tip.clone(),
95✔
1659
            canonical_stacks_snapshot.clone(),
95✔
1660
            burn_tip.clone(),
95✔
1661
            reason.clone(),
95✔
1662
            &new_burn_view,
95✔
1663
        ) {
95✔
UNCOV
1664
            error!("Relayer: Failed to start new tenure: {e:?}");
×
1665
        } else {
1666
            debug!("Relayer: successfully started new tenure.";
95✔
1667
                   "parent_tenure_start" => %canonical_stacks_tip,
1668
                   "burn_tip" => %burn_tip.consensus_hash,
1669
                   "burn_view_snapshot" => %burn_tip.consensus_hash,
1670
                   "block_election_snapshot" => %canonical_stacks_snapshot.consensus_hash,
1671
                   "reason" => %reason);
1672
        }
1673
        Ok(())
95✔
1674
    }
95✔
1675

1676
    fn handle_sortition(
4,423✔
1677
        &mut self,
4,423✔
1678
        consensus_hash: ConsensusHash,
4,423✔
1679
        burn_hash: BurnchainHeaderHash,
4,423✔
1680
        committed_index_hash: StacksBlockId,
4,423✔
1681
    ) -> bool {
4,423✔
1682
        let miner_instruction =
1,530✔
1683
            match self.process_sortition(consensus_hash, burn_hash, committed_index_hash) {
4,423✔
1684
                Some(miner_instruction) => miner_instruction,
1,530✔
1685
                None => {
1686
                    return true;
2,893✔
1687
                }
1688
            };
1689

1690
        match miner_instruction {
1,530✔
1691
            MinerDirective::BeginTenure {
1692
                parent_tenure_start,
1,235✔
1693
                burnchain_tip,
1,235✔
1694
                election_block,
1,235✔
1695
                late,
1,235✔
1696
            } => match self.start_new_tenure(
1,235✔
1697
                parent_tenure_start.clone(),
1,235✔
1698
                election_block.clone(),
1,235✔
1699
                election_block.clone(),
1,235✔
1700
                MinerReason::BlockFound { late },
1,235✔
1701
                &burnchain_tip.consensus_hash,
1,235✔
1702
            ) {
1,235✔
1703
                Ok(()) => {
1704
                    debug!("Relayer: successfully started new tenure.";
1,235✔
1705
                           "parent_tenure_start" => %parent_tenure_start,
1706
                           "burn_tip" => %burnchain_tip.consensus_hash,
1707
                           "burn_view_snapshot" => %burnchain_tip.consensus_hash,
1708
                           "block_election_snapshot" => %burnchain_tip.consensus_hash,
UNCOV
1709
                           "reason" => %MinerReason::BlockFound { late });
×
1710
                }
UNCOV
1711
                Err(e) => {
×
UNCOV
1712
                    error!("Relayer: Failed to start new tenure: {e:?}");
×
1713
                }
1714
            },
1715
            MinerDirective::ContinueTenure { new_burn_view } => {
95✔
1716
                match self.continue_tenure(new_burn_view) {
95✔
1717
                    Ok(()) => {
1718
                        debug!("Relayer: successfully handled continue tenure.");
95✔
1719
                    }
UNCOV
1720
                    Err(e) => {
×
UNCOV
1721
                        error!("Relayer: Failed to continue tenure: {e:?}");
×
UNCOV
1722
                        return false;
×
1723
                    }
1724
                }
1725
            }
1726
            MinerDirective::StopTenure => match self.stop_tenure() {
200✔
1727
                Ok(()) => {
1728
                    debug!("Relayer: successfully stopped tenure.");
200✔
1729
                }
UNCOV
1730
                Err(e) => {
×
UNCOV
1731
                    error!("Relayer: Failed to stop tenure: {e:?}");
×
1732
                }
1733
            },
1734
        }
1735

1736
        self.globals.counters.bump_naka_miner_directives();
1,530✔
1737
        true
1,530✔
1738
    }
4,423✔
1739

1740
    #[cfg(test)]
1741
    fn fault_injection_skip_block_commit(&self) -> bool {
49,782✔
1742
        self.globals.counters.skip_commit_op.get()
49,782✔
1743
    }
49,782✔
1744

1745
    #[cfg(not(test))]
1746
    fn fault_injection_skip_block_commit(&self) -> bool {
1747
        false
1748
    }
1749

1750
    /// Get the canonical tip for the miner to commit to.
1751
    /// This is provided as a separate function so that it can be overridden for testing.
1752
    #[cfg(not(test))]
1753
    fn fault_injection_get_tip_for_commit(&self) -> Option<(ConsensusHash, BlockHeaderHash)> {
1754
        None
1755
    }
1756

1757
    #[cfg(test)]
1758
    fn fault_injection_get_tip_for_commit(&self) -> Option<(ConsensusHash, BlockHeaderHash)> {
2,955✔
1759
        TEST_MINER_COMMIT_TIP.get()
2,955✔
1760
    }
2,955✔
1761

1762
    fn get_commit_for_tip(&mut self) -> Result<(ConsensusHash, BlockHeaderHash), DbError> {
2,955✔
1763
        if let Some((consensus_hash, block_header_hash)) = self.fault_injection_get_tip_for_commit()
2,955✔
1764
        {
1765
            info!("Relayer: using test tip for commit";
98✔
1766
                "consensus_hash" => %consensus_hash,
1767
                "block_header_hash" => %block_header_hash,
1768
            );
1769
            Ok((consensus_hash, block_header_hash))
98✔
1770
        } else {
1771
            SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn())
2,857✔
1772
        }
1773
    }
2,955✔
1774

1775
    /// Generate and submit the next block-commit, and record it locally
1776
    fn issue_block_commit(&mut self) -> Result<(), NakamotoNodeError> {
49,782✔
1777
        if self.fault_injection_skip_block_commit() {
49,782✔
1778
            debug!(
46,827✔
1779
                "Relayer: not submitting block-commit to bitcoin network due to test directive."
1780
            );
1781
            return Ok(());
46,827✔
1782
        }
2,955✔
1783
        let (tip_block_ch, tip_block_bh) = self.get_commit_for_tip().unwrap_or_else(|e| {
2,955✔
UNCOV
1784
            panic!("Failed to load canonical stacks tip: {e:?}");
×
1785
        });
1786
        let mut last_committed = self.make_block_commit(&tip_block_ch, &tip_block_bh)?;
2,955✔
1787

1788
        let Some(tip_height) = NakamotoChainState::get_block_header(
2,955✔
1789
            self.chainstate.db(),
2,955✔
1790
            &StacksBlockId::new(&tip_block_ch, &tip_block_bh),
2,955✔
1791
        )
1792
        .map_err(|e| {
2,955✔
UNCOV
1793
            warn!("Relayer: failed to load tip {tip_block_ch}/{tip_block_bh}: {e:?}");
×
UNCOV
1794
            NakamotoNodeError::ParentNotFound
×
UNCOV
1795
        })?
×
1796
        .map(|header| header.stacks_block_height) else {
2,955✔
UNCOV
1797
            warn!(
×
1798
                "Relayer: failed to load height for tip {tip_block_ch}/{tip_block_bh} (got None)"
1799
            );
UNCOV
1800
            return Err(NakamotoNodeError::ParentNotFound);
×
1801
        };
1802

1803
        // sign and broadcast
1804
        let mut op_signer = self.keychain.generate_op_signer();
2,955✔
1805
        let res = self.bitcoin_controller.submit_operation(
2,955✔
1806
            *last_committed.get_epoch_id(),
2,955✔
1807
            BlockstackOperationType::LeaderBlockCommit(last_committed.get_block_commit().clone()),
2,955✔
1808
            &mut op_signer,
2,955✔
1809
        );
1810
        let txid = match res {
2,955✔
1811
            Ok(txid) => txid,
1,723✔
1812
            Err(e) => {
1,232✔
1813
                if self.config.node.mock_mining {
1,232✔
1814
                    debug!("Relayer: Mock-mining enabled; not sending Bitcoin transaction");
1,138✔
1815
                    return Ok(());
1,138✔
1816
                }
94✔
1817
                warn!("Failed to submit block-commit bitcoin transaction: {e}");
94✔
1818
                return Err(NakamotoNodeError::BurnchainSubmissionFailed(e));
94✔
1819
            }
1820
        };
1821

1822
        info!(
1,723✔
1823
            "Relayer: Submitted block-commit";
1824
            "tip_consensus_hash" => %tip_block_ch,
1825
            "tip_block_hash" => %tip_block_bh,
1826
            "tip_height" => %tip_height,
1827
            "tip_block_id" => %StacksBlockId::new(&tip_block_ch, &tip_block_bh),
1,721✔
1828
            "txid" => %txid,
1829
        );
1830

1831
        // update local state
1832
        last_committed.set_txid(&txid);
1,723✔
1833
        self.globals.counters.bump_naka_submitted_commits(
1,723✔
1834
            last_committed.burn_tip.block_height,
1,723✔
1835
            tip_height,
1,723✔
1836
            last_committed.block_commit.burn_fee,
1,723✔
1837
            &last_committed.tenure_consensus_hash,
1,723✔
1838
        );
1839
        self.last_committed = Some(last_committed);
1,723✔
1840

1841
        Ok(())
1,723✔
1842
    }
49,782✔
1843

1844
    /// Determine what the relayer should do to advance the chain.
1845
    /// * If this isn't a miner, then it's always nothing.
1846
    /// * Otherwise, if we haven't done so already, go register a VRF public key
1847
    /// * If the stacks chain tip or burnchain tip has changed, then issue a block-commit
1848
    /// * If the last burn view we started a miner for is not the canonical burn view, then
1849
    /// try and start a new tenure (or continue an existing one).
1850
    fn initiative(&mut self) -> Result<Option<RelayerDirective>, NakamotoNodeError> {
216,832✔
1851
        if !self.is_miner {
216,832✔
1852
            return Ok(None);
9,323✔
1853
        }
207,509✔
1854

1855
        match self.globals.get_leader_key_registration_state() {
207,509✔
1856
            // do we need a VRF key registration?
1857
            LeaderKeyRegistrationState::Inactive => {
1858
                let sort_tip = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())?;
39✔
1859
                return Ok(Some(RelayerDirective::RegisterKey(sort_tip)));
39✔
1860
            }
1861
            // are we still waiting on a pending registration?
1862
            LeaderKeyRegistrationState::Pending(..) => {
1863
                return Ok(None);
4,416✔
1864
            }
1865
            LeaderKeyRegistrationState::Active(_) => {}
203,054✔
1866
        };
1867

1868
        // load up canonical sortition and stacks tips
1869
        let sort_tip = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())?;
203,054✔
1870

1871
        // NOTE: this may be an epoch2x tip
1872
        let (stacks_tip_ch, stacks_tip_bh) =
203,054✔
1873
            SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn())?;
203,054✔
1874
        let stacks_tip = StacksBlockId::new(&stacks_tip_ch, &stacks_tip_bh);
203,054✔
1875

1876
        // check stacks and sortition tips to see if any chainstate change has happened.
1877
        // did our view of the sortition history change?
1878
        // if so, then let's try and confirm the highest tenure so far.
1879
        let burnchain_changed = self
203,054✔
1880
            .last_committed
203,054✔
1881
            .as_ref()
203,054✔
1882
            .map(|cmt| cmt.get_burn_tip().consensus_hash != sort_tip.consensus_hash)
203,054✔
1883
            .unwrap_or(true);
203,054✔
1884

1885
        let highest_tenure_changed = self
203,054✔
1886
            .last_committed
203,054✔
1887
            .as_ref()
203,054✔
1888
            .map(|cmt| cmt.get_tenure_id() != &stacks_tip_ch)
203,054✔
1889
            .unwrap_or(true);
203,054✔
1890

1891
        debug!("Relayer: initiative to commit";
203,054✔
1892
               "sortititon tip" => %sort_tip.consensus_hash,
1893
               "stacks tip" => %stacks_tip,
1894
               "stacks_tip_ch" => %stacks_tip_ch,
1895
               "stacks_tip_bh" => %stacks_tip_bh,
UNCOV
1896
               "last-commit burn view" => %self.last_committed.as_ref().map(|cmt| cmt.get_burn_tip().consensus_hash.to_string()).unwrap_or("(not set)".to_string()),
×
UNCOV
1897
               "last-commit ongoing tenure" => %self.last_committed.as_ref().map(|cmt| cmt.get_tenure_id().to_string()).unwrap_or("(not set)".to_string()),
×
1898
               "burnchain view changed?" => %burnchain_changed,
1899
               "highest tenure changed?" => %highest_tenure_changed);
1900

1901
        // If the miner spend or config has changed, we want to RBF with new config values.
1902
        let (burnchain_config_changed, _) = self.check_burnchain_config_changed();
203,054✔
1903
        let miner_config_changed = self.check_miner_config_changed();
203,054✔
1904

1905
        if burnchain_config_changed || miner_config_changed {
203,054✔
1906
            info!("Miner spend or config changed; issuing block commit with new values";
2✔
1907
                "miner_spend_changed" => %burnchain_config_changed,
1908
                "miner_config_changed" => %miner_config_changed,
1909
            );
1910
            return Ok(Some(RelayerDirective::IssueBlockCommit(
2✔
1911
                stacks_tip_ch,
2✔
1912
                stacks_tip_bh,
2✔
1913
            )));
2✔
1914
        }
203,052✔
1915

1916
        if !burnchain_changed && !highest_tenure_changed {
203,052✔
1917
            // nothing to do
1918
            return Ok(None);
116,568✔
1919
        }
86,484✔
1920

1921
        if highest_tenure_changed {
86,484✔
1922
            // highest-tenure view changed, so we need to send (or RBF) a commit
1923
            return Ok(Some(RelayerDirective::IssueBlockCommit(
41,423✔
1924
                stacks_tip_ch,
41,423✔
1925
                stacks_tip_bh,
41,423✔
1926
            )));
41,423✔
1927
        }
45,061✔
1928

1929
        debug!("Relayer: burnchain view changed, but highest tenure did not");
45,061✔
1930
        // First, check if the changed burnchain view includes any
1931
        // sortitions. If it doesn't submit a block commit immediately.
1932
        //
1933
        // If it does, then wait a bit for the first block in the new
1934
        // tenure to arrive. This is to avoid submitting a block
1935
        // commit that will be immediately RBFed when the first
1936
        // block arrives.
1937
        if let Some(last_committed) = self.last_committed.as_ref() {
45,061✔
1938
            // check if all the sortitions after `last_tenure` are empty sortitions. if they are,
1939
            //  we don't need to wait at all to submit a commit
1940
            let last_tenure_tip_height = SortitionDB::get_consensus_hash_height(
45,061✔
1941
                &self.sortdb,
45,061✔
1942
                last_committed.get_tenure_id(),
45,061✔
UNCOV
1943
            )?
×
1944
            .ok_or_else(|| NakamotoNodeError::ParentNotFound)?;
45,061✔
1945
            let no_sortitions_after_last_tenure = self
45,061✔
1946
                .sortdb
45,061✔
1947
                .find_in_canonical::<_, _, NakamotoNodeError>(|cursor| {
47,602✔
1948
                    if cursor.block_height <= last_tenure_tip_height {
47,602✔
1949
                        return Ok(FindIter::Halt);
72✔
1950
                    }
47,530✔
1951
                    if cursor.sortition {
47,530✔
1952
                        return Ok(FindIter::Found(()));
44,989✔
1953
                    }
2,541✔
1954
                    Ok(FindIter::Continue)
2,541✔
1955
                })?
47,602✔
1956
                .is_none();
45,061✔
1957
            if no_sortitions_after_last_tenure {
45,061✔
1958
                return Ok(Some(RelayerDirective::IssueBlockCommit(
72✔
1959
                    stacks_tip_ch,
72✔
1960
                    stacks_tip_bh,
72✔
1961
                )));
72✔
1962
            }
44,989✔
UNCOV
1963
        }
×
1964

1965
        if self.new_tenure_timeout.is_ready(
44,989✔
1966
            &sort_tip.consensus_hash,
44,989✔
1967
            &self.config.miner.block_commit_delay,
44,989✔
1968
        ) {
1969
            return Ok(Some(RelayerDirective::IssueBlockCommit(
8,285✔
1970
                stacks_tip_ch,
8,285✔
1971
                stacks_tip_bh,
8,285✔
1972
            )));
8,285✔
1973
        } else {
1974
            if let Some(deadline) = self
36,704✔
1975
                .new_tenure_timeout
36,704✔
1976
                .deadline(&self.config.miner.block_commit_delay)
36,704✔
1977
            {
36,704✔
1978
                self.next_initiative = std::cmp::min(self.next_initiative, deadline);
36,704✔
1979
            }
36,704✔
1980

1981
            return Ok(None);
36,704✔
1982
        }
1983
    }
216,832✔
1984

1985
    /// Try to start up a tenure-extend if the tenure_extend_time has expired.
1986
    ///
1987
    /// Will check if the tenure-extend time was set and has expired. If so, will
1988
    /// check if the current miner thread needs to issue a BlockFound or if it can
1989
    /// immediately tenure-extend.
1990
    ///
1991
    /// Note: tenure_extend_time is only set to Some(_) if during sortition processing, the sortition
1992
    /// winner commit is corrupted or the winning miner has yet to produce a block.
1993
    fn check_tenure_timers(&mut self) {
283,301✔
1994
        // Should begin a tenure-extend?
1995
        let Some(tenure_extend_time) = self.tenure_extend_time.clone() else {
283,301✔
1996
            // No tenure extend time set, so nothing to do.
1997
            return;
263,050✔
1998
        };
1999
        if !tenure_extend_time.should_extend() {
20,251✔
2000
            test_debug!(
19,899✔
2001
                "Relayer: will not try to tenure-extend yet ({} <= {})",
UNCOV
2002
                tenure_extend_time.elapsed().as_secs(),
×
UNCOV
2003
                tenure_extend_time.timeout().as_secs()
×
2004
            );
2005
            return;
19,899✔
2006
        }
352✔
2007

2008
        let Some(mining_pkh) = self.get_mining_key_pkh() else {
352✔
2009
            // This shouldn't really ever hit, but just in case.
UNCOV
2010
            warn!("Will not tenure extend -- no mining key");
×
2011
            // If we don't have a mining key set, don't bother checking again.
UNCOV
2012
            self.tenure_extend_time = None;
×
UNCOV
2013
            return;
×
2014
        };
2015
        // reset timer so we can try again if for some reason a miner was already running (e.g. a
2016
        // blockfound from earlier).
2017
        self.tenure_extend_time
352✔
2018
            .as_mut()
352✔
2019
            .map(|t| t.refresh(self.config.miner.tenure_extend_poll_timeout));
352✔
2020
        // try to extend, but only if we aren't already running a thread for the current or newer
2021
        // burnchain view
2022
        let Ok(burn_tip) = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())
352✔
2023
            .inspect_err(|e| {
352✔
2024
                error!("Failed to read canonical burnchain sortition: {e:?}");
×
UNCOV
2025
            })
×
2026
        else {
UNCOV
2027
            return;
×
2028
        };
2029

2030
        if let Some(miner_thread_burn_view) = self.miner_thread_burn_view.as_ref() {
352✔
2031
            // a miner thread is already running.  If its burn view is the same as the canonical
2032
            // tip, then do nothing for now
2033
            if burn_tip.consensus_hash == miner_thread_burn_view.consensus_hash {
332✔
UNCOV
2034
                info!("Will not try to start a tenure extend -- the current miner thread's burn view matches the sortition tip"; "sortition tip" => %burn_tip.consensus_hash);
×
2035
                // Do not reset the timer, as we may be able to extend later.
UNCOV
2036
                return;
×
2037
            }
332✔
2038
        }
20✔
2039

2040
        let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
352✔
2041
            SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn())
352✔
2042
                .expect("FATAL: failed to query sortition DB for stacks tip");
352✔
2043
        let canonical_stacks_tip =
352✔
2044
            StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh);
352✔
2045
        let canonical_stacks_snapshot =
352✔
2046
            SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &canonical_stacks_tip_ch)
352✔
2047
                .expect("FATAL: failed to query sortiiton DB for epoch")
352✔
2048
                .expect("FATAL: no sortition for canonical stacks tip");
352✔
2049

2050
        match tenure_extend_time.reason() {
352✔
2051
            TenureExtendReason::BadSortitionWinner | TenureExtendReason::EmptySortition => {
2052
                // Before we try to extend, check if we need to issue a BlockFound
2053
                let Ok(last_winning_snapshot) =
343✔
2054
                    Self::get_last_winning_snapshot(&self.sortdb, &burn_tip).inspect_err(|e| {
343✔
2055
                        warn!("Failed to load last winning snapshot: {e:?}");
×
UNCOV
2056
                    })
×
2057
                else {
2058
                    // this should be unreachable, but don't tempt fate.
UNCOV
2059
                    info!("No prior snapshots have a winning sortition. Will not try to mine.");
×
UNCOV
2060
                    self.tenure_extend_time = None;
×
UNCOV
2061
                    return;
×
2062
                };
2063
                let won_last_winning_snapshot =
343✔
2064
                    last_winning_snapshot.miner_pk_hash.as_ref() == Some(&mining_pkh);
343✔
2065
                if won_last_winning_snapshot
343✔
2066
                    && Self::need_block_found(&canonical_stacks_snapshot, &last_winning_snapshot)
332✔
2067
                {
2068
                    info!("Will not tenure extend yet -- need to issue a BlockFound first");
315✔
2069
                    // We may manage to extend later, so don't set the timer to None.
2070
                    return;
315✔
2071
                }
28✔
2072
            }
2073
            TenureExtendReason::UnresponsiveWinner => {}
9✔
2074
        }
2075

2076
        let won_ongoing_tenure_sortition =
37✔
2077
            canonical_stacks_snapshot.miner_pk_hash.as_ref() == Some(&mining_pkh);
37✔
2078
        if !won_ongoing_tenure_sortition {
37✔
2079
            debug!("Will not tenure extend. Did not win ongoing tenure sortition";
1✔
2080
                "burn_chain_sortition_tip_ch" => %burn_tip.consensus_hash,
2081
                "canonical_stacks_tip_ch" => %canonical_stacks_tip_ch,
2082
                "burn_chain_sortition_tip_mining_pk" => ?burn_tip.miner_pk_hash,
2083
                "mining_pk" => %mining_pkh
2084
            );
2085
            self.tenure_extend_time = None;
1✔
2086
            return;
1✔
2087
        }
36✔
2088
        // If we reach this code, we have either won the last winning snapshot and have already issued a block found for it and should extend.
2089
        // OR we did not win the last snapshot, but the person who did has failed to produce a block and we should extend our old tenure.
2090
        if let Err(e) = self.stop_tenure() {
36✔
UNCOV
2091
            error!("Relayer: Failed to stop tenure: {e:?}");
×
UNCOV
2092
            return;
×
2093
        }
36✔
2094
        let reason = MinerReason::Extended {
36✔
2095
            burn_view_consensus_hash: burn_tip.consensus_hash.clone(),
36✔
2096
        };
36✔
2097
        debug!("Relayer: successfully stopped tenure; will try to continue.");
36✔
2098
        if let Err(e) = self.start_new_tenure(
36✔
2099
            canonical_stacks_tip.clone(),
36✔
2100
            canonical_stacks_snapshot.clone(),
36✔
2101
            burn_tip.clone(),
36✔
2102
            reason.clone(),
36✔
2103
            &burn_tip.consensus_hash,
36✔
2104
        ) {
36✔
UNCOV
2105
            error!("Relayer: Failed to start new tenure: {e:?}");
×
2106
        } else {
2107
            debug!("Relayer: successfully started new tenure.";
36✔
2108
                   "parent_tenure_start" => %canonical_stacks_tip,
2109
                   "burn_tip" => %burn_tip.consensus_hash,
2110
                   "burn_view_snapshot" => %burn_tip.consensus_hash,
2111
                   "block_election_snapshot" => %canonical_stacks_snapshot.consensus_hash,
2112
                   "reason" => %reason);
2113
            self.tenure_extend_time = None;
36✔
2114
        }
2115
    }
283,301✔
2116

2117
    /// Main loop of the relayer.
2118
    /// Runs in a separate thread.
2119
    /// Continuously receives from `relay_rcv`.
2120
    /// Wakes up once per second to see if we need to continue mining an ongoing tenure.
2121
    pub fn main(mut self, relay_rcv: Receiver<RelayerDirective>) {
245✔
2122
        debug!("relayer thread ID is {:?}", std::thread::current().id());
245✔
2123

2124
        self.next_initiative =
245✔
2125
            Instant::now() + Duration::from_millis(self.config.node.next_initiative_delay);
245✔
2126

2127
        // how often we perform a loop pass below
2128
        let poll_frequency_ms = 1_000;
245✔
2129

2130
        while self.globals.keep_running() {
283,526✔
2131
            self.check_tenure_timers();
283,301✔
2132
            let raised_initiative = self.globals.take_initiative();
283,301✔
2133
            let timed_out = Instant::now() >= self.next_initiative;
283,301✔
2134
            let initiative_directive = if raised_initiative.is_some() || timed_out {
283,301✔
2135
                self.next_initiative =
216,832✔
2136
                    Instant::now() + Duration::from_millis(self.config.node.next_initiative_delay);
216,832✔
2137
                self.initiative()
216,832✔
2138
                    .inspect_err(|e| {
216,832✔
UNCOV
2139
                        error!("Error while getting directive from initiative()"; "err" => ?e);
×
UNCOV
2140
                    })
×
2141
                    .ok()
216,832✔
2142
                    .flatten()
216,832✔
2143
            } else {
2144
                None
66,469✔
2145
            };
2146

2147
            let directive_opt = initiative_directive.or_else(|| {
283,301✔
2148
                // do a time-bound recv on the relayer channel so that we can hit the `initiative()` invocation
2149
                //  and keep_running() checks on each loop iteration
2150
                match relay_rcv.recv_timeout(Duration::from_millis(poll_frequency_ms)) {
233,480✔
2151
                    Ok(directive) => {
226,257✔
2152
                        // only do this once, so we can call .initiative() again
2153
                        Some(directive)
226,257✔
2154
                    }
2155
                    Err(RecvTimeoutError::Timeout) => None,
7,223✔
2156
                    Err(RecvTimeoutError::Disconnected) => {
UNCOV
2157
                        warn!("Relayer receive channel disconnected. Exiting relayer thread");
×
2158
                        Some(RelayerDirective::Exit)
×
2159
                    }
2160
                }
2161
            });
233,480✔
2162

2163
            if let Some(directive) = directive_opt {
283,301✔
2164
                debug!("Relayer: main loop directive";
276,078✔
2165
                       "directive" => %directive,
2166
                       "raised_initiative" => ?raised_initiative,
2167
                       "timed_out" => %timed_out);
2168

2169
                if !self.handle_directive(directive) {
276,078✔
2170
                    break;
20✔
2171
                }
276,058✔
2172
            }
7,223✔
2173
        }
2174

2175
        // kill miner if it's running
2176
        signal_mining_blocked(self.globals.get_miner_status());
245✔
2177

2178
        // set termination flag so other threads die
2179
        self.globals.signal_stop();
245✔
2180

2181
        debug!("Relayer exit!");
245✔
2182
    }
245✔
2183

2184
    /// Try loading up a saved VRF key
2185
    pub(crate) fn load_saved_vrf_key(path: &str, pubkey_hash: &Hash160) -> Option<RegisteredKey> {
44✔
2186
        let mut f = match fs::File::open(path) {
44✔
2187
            Ok(f) => f,
43✔
2188
            Err(e) => {
1✔
2189
                warn!("Could not open {path}: {e:?}");
1✔
2190
                return None;
1✔
2191
            }
2192
        };
2193
        let mut registered_key_bytes = vec![];
43✔
2194
        if let Err(e) = f.read_to_end(&mut registered_key_bytes) {
43✔
UNCOV
2195
            warn!("Failed to read registered key bytes from {path}: {e:?}");
×
UNCOV
2196
            return None;
×
2197
        }
43✔
2198

2199
        let Ok(registered_key) = serde_json::from_slice::<RegisteredKey>(&registered_key_bytes)
43✔
2200
        else {
2201
            warn!("Did not load registered key from {path}: could not decode JSON");
2✔
2202
            return None;
2✔
2203
        };
2204

2205
        // Check that the loaded key's memo matches the current miner's key
2206
        if registered_key.memo != pubkey_hash.as_ref() {
41✔
2207
            warn!("Loaded VRF key does not match mining key");
39✔
2208
            return None;
39✔
2209
        }
2✔
2210

2211
        info!("Loaded registered key from {path}");
2✔
2212
        Some(registered_key)
2✔
2213
    }
44✔
2214

2215
    /// Top-level dispatcher
2216
    pub fn handle_directive(&mut self, directive: RelayerDirective) -> bool {
276,063✔
2217
        debug!("Relayer: handling directive"; "directive" => %directive);
276,063✔
2218
        let continue_running = match directive {
276,063✔
2219
            RelayerDirective::HandleNetResult(net_result) => {
221,819✔
2220
                self.process_network_result(net_result);
221,819✔
2221
                true
221,819✔
2222
            }
2223
            // RegisterKey directives mean that the relayer should try to register a new VRF key.
2224
            // These are triggered by the relayer waking up without an active VRF key.
2225
            RelayerDirective::RegisterKey(last_burn_block) => {
39✔
2226
                if !self.is_miner {
39✔
UNCOV
2227
                    return true;
×
2228
                }
39✔
2229
                if self.globals.in_initial_block_download() {
39✔
UNCOV
2230
                    info!("In initial block download, will not submit VRF registration");
×
2231
                    return true;
×
2232
                }
39✔
2233
                let mut saved_key_opt = None;
39✔
2234
                if let Some(path) = self.config.miner.activated_vrf_key_path.as_ref() {
39✔
2235
                    saved_key_opt =
39✔
2236
                        Self::load_saved_vrf_key(path, &self.keychain.get_nakamoto_pkh());
39✔
2237
                }
39✔
2238
                if let Some(saved_key) = saved_key_opt {
39✔
2239
                    debug!("Relayer: resuming VRF key");
1✔
2240
                    self.globals.resume_leader_key(saved_key);
1✔
2241
                } else {
2242
                    self.rotate_vrf_and_register(&last_burn_block);
38✔
2243
                    debug!("Relayer: directive Registered VRF key");
38✔
2244
                }
2245
                self.globals.counters.bump_blocks_processed();
39✔
2246
                true
39✔
2247
            }
2248
            // ProcessedBurnBlock directives correspond to a new sortition perhaps occurring.
2249
            //  relayer should invoke `handle_sortition` to determine if they won the sortition,
2250
            //  and to start their miner, or stop their miner if an active tenure is now ending
2251
            RelayerDirective::ProcessedBurnBlock(consensus_hash, burn_hash, block_header_hash) => {
4,423✔
2252
                if !self.is_miner {
4,423✔
UNCOV
2253
                    return true;
×
2254
                }
4,423✔
2255
                if self.globals.in_initial_block_download() {
4,423✔
UNCOV
2256
                    debug!("In initial block download, will not check sortition for miner");
×
UNCOV
2257
                    return true;
×
2258
                }
4,423✔
2259
                self.handle_sortition(
4,423✔
2260
                    consensus_hash,
4,423✔
2261
                    burn_hash,
4,423✔
2262
                    StacksBlockId(block_header_hash.0),
4,423✔
2263
                )
2264
            }
2265
            // These are triggered by the relayer waking up, seeing a new consensus hash *or* a new first tenure block
2266
            RelayerDirective::IssueBlockCommit(..) => {
2267
                if !self.is_miner {
49,782✔
UNCOV
2268
                    return true;
×
2269
                }
49,782✔
2270
                if self.globals.in_initial_block_download() {
49,782✔
UNCOV
2271
                    debug!("In initial block download, will not issue block commit");
×
UNCOV
2272
                    return true;
×
2273
                }
49,782✔
2274
                if let Err(e) = self.issue_block_commit() {
49,782✔
2275
                    warn!("Relayer failed to issue block commit"; "err" => ?e);
96✔
2276
                }
49,686✔
2277
                true
49,782✔
2278
            }
UNCOV
2279
            RelayerDirective::Exit => false,
×
2280
        };
2281
        debug!("Relayer: handled directive"; "continue_running" => continue_running);
276,063✔
2282
        continue_running
276,063✔
2283
    }
276,063✔
2284

2285
    /// Reload config.burnchain to see if burn_fee_cap has changed.
2286
    /// If it has, update the miner spend amount and return true.
2287
    pub fn check_burnchain_config_changed(&self) -> (bool, BurnchainConfig) {
206,008✔
2288
        let burnchain_config = self.config.get_burnchain_config();
206,008✔
2289
        let last_burnchain_config_opt = self.globals.get_last_burnchain_config();
206,008✔
2290
        let burnchain_config_changed =
206,008✔
2291
            if let Some(last_burnchain_config) = last_burnchain_config_opt {
206,008✔
2292
                last_burnchain_config != burnchain_config
205,767✔
2293
            } else {
2294
                false
241✔
2295
            };
2296

2297
        self.globals
206,008✔
2298
            .set_last_miner_spend_amount(burnchain_config.burn_fee_cap);
206,008✔
2299
        self.globals
206,008✔
2300
            .set_last_burnchain_config(burnchain_config.clone());
206,008✔
2301

2302
        set_mining_spend_amount(
206,008✔
2303
            self.globals.get_miner_status(),
206,008✔
2304
            burnchain_config.burn_fee_cap,
206,008✔
2305
        );
2306

2307
        (burnchain_config_changed, burnchain_config)
206,008✔
2308
    }
206,008✔
2309

2310
    pub fn check_miner_config_changed(&self) -> bool {
203,054✔
2311
        let miner_config = self.config.get_miner_config();
203,054✔
2312
        let last_miner_config_opt = self.globals.get_last_miner_config();
203,054✔
2313
        let miner_config_changed = if let Some(last_miner_config) = last_miner_config_opt {
203,054✔
2314
            last_miner_config != miner_config
202,813✔
2315
        } else {
2316
            false
241✔
2317
        };
2318

2319
        self.globals.set_last_miner_config(miner_config);
203,054✔
2320

2321
        miner_config_changed
203,054✔
2322
    }
203,054✔
2323
}
2324

2325
#[cfg(test)]
2326
pub mod test {
2327
    use std::fs::File;
2328
    use std::io::Write;
2329
    use std::path::Path;
2330
    use std::time::Duration;
2331
    use std::u64;
2332

2333
    use rand::{thread_rng, Rng};
2334
    use stacks::burnchains::Txid;
2335
    use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash, OpsHash, SortitionHash};
2336
    use stacks::types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, SortitionId, TrieHash};
2337
    use stacks::util::hash::Hash160;
2338
    use stacks::util::secp256k1::Secp256k1PublicKey;
2339
    use stacks::util::vrf::VRFPublicKey;
2340

2341
    use super::{BurnBlockCommitTimer, RelayerThread};
2342
    use crate::nakamoto_node::save_activated_vrf_key;
2343
    use crate::run_loop::RegisteredKey;
2344
    use crate::Keychain;
2345

2346
    #[test]
2347
    fn load_nonexistent_vrf_key() {
1✔
2348
        let keychain = Keychain::default(vec![0u8; 32]);
1✔
2349
        let pk = Secp256k1PublicKey::from_private(keychain.get_nakamoto_sk());
1✔
2350
        let pubkey_hash = Hash160::from_node_public_key(&pk);
1✔
2351

2352
        let path = "/tmp/does_not_exist.json";
1✔
2353
        _ = std::fs::remove_file(path);
1✔
2354

2355
        let res = RelayerThread::load_saved_vrf_key(path, &pubkey_hash);
1✔
2356
        assert!(res.is_none());
1✔
2357
    }
1✔
2358

2359
    #[test]
2360
    fn load_empty_vrf_key() {
1✔
2361
        let keychain = Keychain::default(vec![0u8; 32]);
1✔
2362
        let pk = Secp256k1PublicKey::from_private(keychain.get_nakamoto_sk());
1✔
2363
        let pubkey_hash = Hash160::from_node_public_key(&pk);
1✔
2364

2365
        let path = "/tmp/empty.json";
1✔
2366
        File::create(path).expect("Failed to create test file");
1✔
2367
        assert!(Path::new(path).exists());
1✔
2368

2369
        let res = RelayerThread::load_saved_vrf_key(path, &pubkey_hash);
1✔
2370
        assert!(res.is_none());
1✔
2371

2372
        std::fs::remove_file(path).expect("Failed to delete test file");
1✔
2373
    }
1✔
2374

2375
    #[test]
2376
    fn load_bad_vrf_key() {
1✔
2377
        let keychain = Keychain::default(vec![0u8; 32]);
1✔
2378
        let pk = Secp256k1PublicKey::from_private(keychain.get_nakamoto_sk());
1✔
2379
        let pubkey_hash = Hash160::from_node_public_key(&pk);
1✔
2380

2381
        let path = "/tmp/invalid_saved_key.json";
1✔
2382
        let json_content = r#"{ "hello": "world" }"#;
1✔
2383

2384
        // Write the JSON content to the file
2385
        let mut file = File::create(path).expect("Failed to create test file");
1✔
2386
        file.write_all(json_content.as_bytes())
1✔
2387
            .expect("Failed to write to test file");
1✔
2388
        assert!(Path::new(path).exists());
1✔
2389

2390
        let res = RelayerThread::load_saved_vrf_key(path, &pubkey_hash);
1✔
2391
        assert!(res.is_none());
1✔
2392

2393
        std::fs::remove_file(path).expect("Failed to delete test file");
1✔
2394
    }
1✔
2395

2396
    #[test]
2397
    fn save_load_vrf_key() {
1✔
2398
        let keychain = Keychain::default(vec![0u8; 32]);
1✔
2399
        let pk = Secp256k1PublicKey::from_private(keychain.get_nakamoto_sk());
1✔
2400
        let pubkey_hash = Hash160::from_node_public_key(&pk);
1✔
2401
        let key = RegisteredKey {
1✔
2402
            target_block_height: 101,
1✔
2403
            block_height: 102,
1✔
2404
            op_vtxindex: 1,
1✔
2405
            vrf_public_key: VRFPublicKey::from_hex(
1✔
2406
                "1da75863a7e1ef86f0f550d92b1f77dc60af23694b884b2816b703137ff94e71",
1✔
2407
            )
1✔
2408
            .unwrap(),
1✔
2409
            memo: pubkey_hash.as_ref().to_vec(),
1✔
2410
        };
1✔
2411
        let path = "/tmp/vrf_key.json";
1✔
2412
        save_activated_vrf_key(path, &key);
1✔
2413

2414
        let res = RelayerThread::load_saved_vrf_key(path, &pubkey_hash);
1✔
2415
        assert!(res.is_some());
1✔
2416

2417
        std::fs::remove_file(path).expect("Failed to delete test file");
1✔
2418
    }
1✔
2419

2420
    #[test]
2421
    fn invalid_saved_memo() {
1✔
2422
        let keychain = Keychain::default(vec![0u8; 32]);
1✔
2423
        let pk = Secp256k1PublicKey::from_private(keychain.get_nakamoto_sk());
1✔
2424
        let pubkey_hash = Hash160::from_node_public_key(&pk);
1✔
2425
        let key = RegisteredKey {
1✔
2426
            target_block_height: 101,
1✔
2427
            block_height: 102,
1✔
2428
            op_vtxindex: 1,
1✔
2429
            vrf_public_key: VRFPublicKey::from_hex(
1✔
2430
                "1da75863a7e1ef86f0f550d92b1f77dc60af23694b884b2816b703137ff94e71",
1✔
2431
            )
1✔
2432
            .unwrap(),
1✔
2433
            memo: pubkey_hash.as_ref().to_vec(),
1✔
2434
        };
1✔
2435
        let path = "/tmp/vrf_key.json";
1✔
2436
        save_activated_vrf_key(path, &key);
1✔
2437

2438
        let keychain = Keychain::default(vec![1u8; 32]);
1✔
2439
        let pk = Secp256k1PublicKey::from_private(keychain.get_nakamoto_sk());
1✔
2440
        let pubkey_hash = Hash160::from_node_public_key(&pk);
1✔
2441

2442
        let res = RelayerThread::load_saved_vrf_key(path, &pubkey_hash);
1✔
2443
        assert!(res.is_none());
1✔
2444

2445
        std::fs::remove_file(path).expect("Failed to delete test file");
1✔
2446
    }
1✔
2447

2448
    #[test]
2449
    fn check_need_block_found() {
1✔
2450
        let consensus_hash_byte = thread_rng().gen();
1✔
2451
        let canonical_stacks_snapshot = BlockSnapshot {
1✔
2452
            block_height: thread_rng().gen::<u64>().wrapping_add(1), // Add one to ensure we can always decrease by 1 without underflowing.
1✔
2453
            burn_header_timestamp: thread_rng().gen(),
1✔
2454
            burn_header_hash: BurnchainHeaderHash([thread_rng().gen(); 32]),
1✔
2455
            consensus_hash: ConsensusHash([consensus_hash_byte; 20]),
1✔
2456
            parent_burn_header_hash: BurnchainHeaderHash([thread_rng().gen(); 32]),
1✔
2457
            ops_hash: OpsHash([thread_rng().gen(); 32]),
1✔
2458
            total_burn: thread_rng().gen(),
1✔
2459
            sortition: true,
1✔
2460
            sortition_hash: SortitionHash([thread_rng().gen(); 32]),
1✔
2461
            winning_block_txid: Txid([thread_rng().gen(); 32]),
1✔
2462
            winning_stacks_block_hash: BlockHeaderHash([thread_rng().gen(); 32]),
1✔
2463
            index_root: TrieHash([thread_rng().gen(); 32]),
1✔
2464
            num_sortitions: thread_rng().gen(),
1✔
2465
            stacks_block_accepted: true,
1✔
2466
            stacks_block_height: thread_rng().gen(),
1✔
2467
            arrival_index: thread_rng().gen(),
1✔
2468
            canonical_stacks_tip_consensus_hash: ConsensusHash([thread_rng().gen(); 20]),
1✔
2469
            canonical_stacks_tip_hash: BlockHeaderHash([thread_rng().gen(); 32]),
1✔
2470
            canonical_stacks_tip_height: thread_rng().gen(),
1✔
2471
            sortition_id: SortitionId([thread_rng().gen(); 32]),
1✔
2472
            parent_sortition_id: SortitionId([thread_rng().gen(); 32]),
1✔
2473
            pox_valid: true,
1✔
2474
            accumulated_coinbase_ustx: thread_rng().gen::<u64>() as u128,
1✔
2475
            miner_pk_hash: Some(Hash160([thread_rng().gen(); 20])),
1✔
2476
        };
1✔
2477

2478
        // The consensus_hashes are the same, and the block heights are the same. Therefore, don't need a block found.
2479
        let last_winning_block_snapshot = canonical_stacks_snapshot.clone();
1✔
2480
        assert!(!RelayerThread::need_block_found(
1✔
2481
            &canonical_stacks_snapshot,
1✔
2482
            &last_winning_block_snapshot
1✔
2483
        ));
1✔
2484

2485
        // The block height of the canonical tip is higher than the last winning snapshot. We already issued a block found.
2486
        let mut canonical_stacks_snapshot_is_higher_than_last_winning_snapshot =
1✔
2487
            last_winning_block_snapshot.clone();
1✔
2488
        canonical_stacks_snapshot_is_higher_than_last_winning_snapshot.block_height =
1✔
2489
            canonical_stacks_snapshot.block_height.saturating_sub(1);
1✔
2490
        assert!(!RelayerThread::need_block_found(
1✔
2491
            &canonical_stacks_snapshot,
1✔
2492
            &canonical_stacks_snapshot_is_higher_than_last_winning_snapshot
1✔
2493
        ));
1✔
2494

2495
        // The block height is the same, but we have different consensus hashes. We need to issue a block found.
2496
        let mut tip_consensus_hash_mismatch = last_winning_block_snapshot.clone();
1✔
2497
        tip_consensus_hash_mismatch.consensus_hash =
1✔
2498
            ConsensusHash([consensus_hash_byte.wrapping_add(1); 20]);
1✔
2499
        assert!(RelayerThread::need_block_found(
1✔
2500
            &canonical_stacks_snapshot,
1✔
2501
            &tip_consensus_hash_mismatch
1✔
2502
        ));
2503

2504
        // The block height is the same, but we have different consensus hashes. We need to issue a block found.
2505
        let mut tip_consensus_hash_mismatch = last_winning_block_snapshot.clone();
1✔
2506
        tip_consensus_hash_mismatch.consensus_hash =
1✔
2507
            ConsensusHash([consensus_hash_byte.wrapping_add(1); 20]);
1✔
2508
        assert!(RelayerThread::need_block_found(
1✔
2509
            &canonical_stacks_snapshot,
1✔
2510
            &tip_consensus_hash_mismatch
1✔
2511
        ));
2512

2513
        // The block height of the canonical tip is lower than the last winning snapshot blockheight. We need to issue a block found.
2514
        let mut canonical_stacks_snapshot_is_lower_than_last_winning_snapshot =
1✔
2515
            last_winning_block_snapshot.clone();
1✔
2516
        canonical_stacks_snapshot_is_lower_than_last_winning_snapshot.block_height =
1✔
2517
            canonical_stacks_snapshot.block_height.saturating_add(1);
1✔
2518
        assert!(RelayerThread::need_block_found(
1✔
2519
            &canonical_stacks_snapshot,
1✔
2520
            &canonical_stacks_snapshot_is_lower_than_last_winning_snapshot
1✔
2521
        ));
2522
    }
1✔
2523

2524
    #[test]
2525
    fn burn_block_commit_timer_units() {
1✔
2526
        let mut burn_block_timer = BurnBlockCommitTimer::NotSet;
1✔
2527
        assert_eq!(burn_block_timer.elapsed_secs(), 0);
1✔
2528

2529
        let ch_0 = ConsensusHash([0; 20]);
1✔
2530
        let ch_1 = ConsensusHash([1; 20]);
1✔
2531
        let ch_2 = ConsensusHash([2; 20]);
1✔
2532

2533
        assert!(!burn_block_timer.is_ready(&ch_0, &Duration::from_secs(1)));
1✔
2534
        let BurnBlockCommitTimer::Set { burn_tip, .. } = &burn_block_timer else {
1✔
UNCOV
2535
            panic!("The burn block timer should be set");
×
2536
        };
2537
        assert_eq!(burn_tip, &ch_0);
1✔
2538

2539
        std::thread::sleep(Duration::from_secs(1));
1✔
2540

2541
        assert!(burn_block_timer.is_ready(&ch_0, &Duration::from_secs(0)));
1✔
2542
        let BurnBlockCommitTimer::Set { burn_tip, .. } = &burn_block_timer else {
1✔
UNCOV
2543
            panic!("The burn block timer should be set");
×
2544
        };
2545
        assert_eq!(burn_tip, &ch_0);
1✔
2546

2547
        assert!(!burn_block_timer.is_ready(&ch_1, &Duration::from_secs(0)));
1✔
2548
        let BurnBlockCommitTimer::Set { burn_tip, .. } = &burn_block_timer else {
1✔
UNCOV
2549
            panic!("The burn block timer should be set");
×
2550
        };
2551
        assert_eq!(burn_tip, &ch_1);
1✔
2552

2553
        assert!(!burn_block_timer.is_ready(&ch_1, &Duration::from_secs(u64::MAX)));
1✔
2554
        let BurnBlockCommitTimer::Set { burn_tip, .. } = &burn_block_timer else {
1✔
UNCOV
2555
            panic!("The burn block timer should be set");
×
2556
        };
2557
        assert_eq!(burn_tip, &ch_1);
1✔
2558

2559
        std::thread::sleep(Duration::from_secs(1));
1✔
2560
        assert!(!burn_block_timer.is_ready(&ch_2, &Duration::from_secs(0)));
1✔
2561
        let BurnBlockCommitTimer::Set { burn_tip, .. } = &burn_block_timer else {
1✔
UNCOV
2562
            panic!("The burn block timer should be set");
×
2563
        };
2564
        assert_eq!(burn_tip, &ch_2);
1✔
2565
    }
1✔
2566
}
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