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

stacks-network / stacks-core / 23943270448

03 Apr 2026 10:32AM UTC coverage: 77.559% (-8.2%) from 85.712%
23943270448

Pull #7077

github

52f01d
web-flow
Merge fa3f939ed into c529ad924
Pull Request #7077: feat: add burnchain DB copy and validation

3654 of 4220 new or added lines in 18 files covered. (86.59%)

19324 existing lines in 182 files now uncovered.

171991 of 221755 relevant lines covered (77.56%)

7658447.9 hits per line

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

15.13
/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 {
5✔
162
        let needs_reset = match self {
5✔
163
            BurnBlockCommitTimer::NotSet => true,
1✔
164
            BurnBlockCommitTimer::Set {
165
                start_time,
4✔
166
                burn_tip,
4✔
167
            } => {
168
                if burn_tip != current_burn_tip {
4✔
169
                    true
2✔
170
                } else {
171
                    if start_time.elapsed() > *timeout {
2✔
172
                        // timer expired and was pointed at the correct burn tip
173
                        // so we can just return is_ready here
174
                        return true;
1✔
175
                    }
1✔
176
                    // timer didn't expire, but the burn tip was correct, so
177
                    //  we don't need to reset the timer
178
                    false
1✔
179
                }
180
            }
181
        };
182
        if needs_reset {
4✔
183
            info!(
3✔
184
                "Starting new tenure timeout";
185
                "timeout_secs" => timeout.as_secs(),
3✔
186
                "burn_tip_ch" => %current_burn_tip
187
            );
188
            *self = Self::Set {
3✔
189
                burn_tip: current_burn_tip.clone(),
3✔
190
                start_time: Instant::now(),
3✔
191
            };
3✔
192
        }
1✔
193

194
        debug!(
4✔
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
4✔
201
    }
5✔
202

203
    /// At what time, if set, would this timer be ready?
UNCOV
204
    fn deadline(&self, timeout: &Duration) -> Option<Instant> {
×
UNCOV
205
        match self {
×
206
            BurnBlockCommitTimer::NotSet => None,
×
UNCOV
207
            BurnBlockCommitTimer::Set { start_time, .. } => Some(*start_time + *timeout),
×
208
        }
UNCOV
209
    }
×
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 {
UNCOV
221
    pub fn new(
×
UNCOV
222
        commit: LeaderBlockCommitOp,
×
UNCOV
223
        burn_tip: BlockSnapshot,
×
UNCOV
224
        stacks_tip: StacksBlockId,
×
UNCOV
225
        tenure_consensus_hash: ConsensusHash,
×
UNCOV
226
        start_block_hash: BlockHeaderHash,
×
UNCOV
227
        epoch_id: StacksEpochId,
×
UNCOV
228
    ) -> Self {
×
UNCOV
229
        Self {
×
UNCOV
230
            block_commit: commit,
×
UNCOV
231
            burn_tip,
×
UNCOV
232
            stacks_tip,
×
UNCOV
233
            tenure_consensus_hash,
×
UNCOV
234
            start_block_hash,
×
UNCOV
235
            epoch_id,
×
UNCOV
236
            txid: None,
×
UNCOV
237
        }
×
UNCOV
238
    }
×
239

240
    /// Get the commit
UNCOV
241
    pub fn get_block_commit(&self) -> &LeaderBlockCommitOp {
×
UNCOV
242
        &self.block_commit
×
UNCOV
243
    }
×
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?
UNCOV
256
    pub fn get_burn_tip(&self) -> &BlockSnapshot {
×
UNCOV
257
        &self.burn_tip
×
UNCOV
258
    }
×
259

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

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

270
    /// Set our txid
UNCOV
271
    pub fn set_txid(&mut self, txid: &Txid) {
×
UNCOV
272
        self.txid = Some(txid.clone());
×
UNCOV
273
    }
×
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 {
UNCOV
288
    pub fn new(join_handle: MinerThreadJoinHandle, abort_flag: Arc<AtomicBool>) -> Self {
×
UNCOV
289
        Self {
×
UNCOV
290
            join_handle,
×
UNCOV
291
            abort_flag,
×
UNCOV
292
        }
×
UNCOV
293
    }
×
294

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

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

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

UNCOV
315
        self.abort_flag.store(true, Ordering::SeqCst);
×
UNCOV
316
        globals.block_miner();
×
317

UNCOV
318
        let prior_miner = self.into_inner();
×
UNCOV
319
        let prior_miner_result = prior_miner.join().map_err(|_| {
×
320
            error!("Miner: failed to join prior miner");
×
321
            ChainstateError::MinerAborted
×
322
        })?;
×
UNCOV
323
        debug!("Stopped prior miner thread ID {:?}", &prior_thread_id);
×
UNCOV
324
        if let Err(e) = prior_miner_result {
×
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.
UNCOV
329
            debug!("Prior mining thread exited with: {e:?}");
×
UNCOV
330
        }
×
331

UNCOV
332
        globals.unblock_miner();
×
UNCOV
333
        Ok(())
×
UNCOV
334
    }
×
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`
UNCOV
361
    pub fn unresponsive_winner(timeout: Duration) -> Self {
×
UNCOV
362
        Self {
×
UNCOV
363
            time: Instant::now(),
×
UNCOV
364
            timeout,
×
UNCOV
365
            reason: TenureExtendReason::UnresponsiveWinner,
×
UNCOV
366
        }
×
UNCOV
367
    }
×
368

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

378
    /// Should we attempt to tenure-extend?
UNCOV
379
    pub fn should_extend(&self) -> bool {
×
380
        // We set the time, but have we waited long enough?
UNCOV
381
        self.time.elapsed() > self.timeout
×
UNCOV
382
    }
×
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
UNCOV
395
    pub fn reason(&self) -> &TenureExtendReason {
×
UNCOV
396
        &self.reason
×
UNCOV
397
    }
×
398

399
    /// Update the timeout for this `TenureExtendTime` and reset the time
UNCOV
400
    pub fn refresh(&mut self, timeout: Duration) {
×
UNCOV
401
        self.timeout = timeout;
×
UNCOV
402
        self.time = Instant::now();
×
UNCOV
403
    }
×
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
UNCOV
479
    pub fn new(
×
UNCOV
480
        runloop: &RunLoop,
×
UNCOV
481
        local_peer: LocalPeer,
×
UNCOV
482
        relayer: Relayer,
×
UNCOV
483
        keychain: Keychain,
×
UNCOV
484
    ) -> RelayerThread {
×
UNCOV
485
        let config = runloop.config().clone();
×
UNCOV
486
        let globals = runloop.get_globals();
×
UNCOV
487
        let burn_db_path = config.get_burn_db_file_path();
×
UNCOV
488
        let is_miner = runloop.is_miner();
×
489

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

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

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

UNCOV
505
        let bitcoin_controller = BitcoinRegtestController::new_dummy(config.clone());
×
506

UNCOV
507
        let next_initiative_delay = config.node.next_initiative_delay;
×
508

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

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

×
UNCOV
529
            relayer,
×
UNCOV
530

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

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

546
    /// have we waited for the right conditions under which to start mining a block off of our
547
    /// chain tip?
UNCOV
548
    fn has_waited_for_latest_blocks(&self) -> bool {
×
549
        // a network download pass took place
UNCOV
550
        self.min_network_download_passes <= self.last_network_download_passes
×
551
        // we waited long enough for a download pass, but timed out waiting
UNCOV
552
        || self.last_network_block_height_ts + (self.config.node.wait_time_for_blocks as u128) < get_epoch_time_ms()
×
553
        // we're not supposed to wait at all
UNCOV
554
        || !self.config.miner.wait_for_block_download
×
UNCOV
555
    }
×
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
UNCOV
561
    pub fn process_network_result(&mut self, mut net_result: NetworkResult) {
×
UNCOV
562
        debug!(
×
563
            "Relayer: Handle network result (from {})",
564
            net_result.burn_height
565
        );
566

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

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

UNCOV
590
        if net_receipts.num_new_blocks > 0 {
×
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
UNCOV
593
            debug!("Relayer: block mining to process newly-arrived blocks or microblocks");
×
UNCOV
594
            signal_mining_blocked(self.globals.get_miner_status());
×
UNCOV
595
        }
×
596

UNCOV
597
        let mempool_txs_added = net_receipts.mempool_txs_added.len();
×
UNCOV
598
        if mempool_txs_added > 0 {
×
UNCOV
599
            self.event_dispatcher
×
UNCOV
600
                .process_new_mempool_txs(net_receipts.mempool_txs_added);
×
UNCOV
601
        }
×
602

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

609
        // resume mining if we blocked it, and if we've done the requisite download
610
        // passes
UNCOV
611
        self.last_network_download_passes = net_result.num_download_passes;
×
UNCOV
612
        self.last_network_inv_passes = net_result.num_inv_sync_passes;
×
UNCOV
613
        if self.has_waited_for_latest_blocks() {
×
UNCOV
614
            debug!("Relayer: did a download pass, so unblocking mining");
×
UNCOV
615
            signal_mining_ready(self.globals.get_miner_status());
×
UNCOV
616
        }
×
UNCOV
617
    }
×
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`.
UNCOV
636
    fn choose_directive_sortition_with_winner(
×
UNCOV
637
        &mut self,
×
UNCOV
638
        sn: BlockSnapshot,
×
UNCOV
639
        mining_pkh: &Hash160,
×
UNCOV
640
        committed_index_hash: StacksBlockId,
×
UNCOV
641
    ) -> MinerDirective {
×
UNCOV
642
        let won_sortition = sn.miner_pk_hash.as_ref() == Some(mining_pkh);
×
643

UNCOV
644
        let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
×
UNCOV
645
            SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn())
×
UNCOV
646
                .expect("FATAL: failed to query sortition DB for stacks tip");
×
UNCOV
647
        let canonical_stacks_snapshot =
×
UNCOV
648
            SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &canonical_stacks_tip_ch)
×
UNCOV
649
                .expect("FATAL: failed to query sortiiton DB for epoch")
×
UNCOV
650
                .expect("FATAL: no sortition for canonical stacks tip");
×
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.
UNCOV
657
        if won_sortition && !self.config.get_node_config(false).mock_mining {
×
UNCOV
658
            let canonical_stacks_tip =
×
UNCOV
659
                StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh);
×
660

UNCOV
661
            let commits_to_tip_tenure = Self::sortition_commits_to_stacks_tip_tenure(
×
UNCOV
662
                &mut self.chainstate,
×
UNCOV
663
                &canonical_stacks_tip,
×
UNCOV
664
                &canonical_stacks_snapshot,
×
UNCOV
665
                &sn,
×
UNCOV
666
            ).unwrap_or_else(|e| {
×
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

UNCOV
675
            if !commits_to_tip_tenure {
×
UNCOV
676
                let won_ongoing_tenure_sortition =
×
UNCOV
677
                    canonical_stacks_snapshot.miner_pk_hash.as_ref() == Some(mining_pkh);
×
678

UNCOV
679
                if won_ongoing_tenure_sortition {
×
UNCOV
680
                    info!(
×
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,
UNCOV
684
                        "commits_to_tip_tenure?" => commits_to_tip_tenure
×
685
                    );
686
                    // Extend tenure to the new burn view instead of attempting BlockFound
UNCOV
687
                    return MinerDirective::ContinueTenure {
×
UNCOV
688
                        new_burn_view: sn.consensus_hash,
×
UNCOV
689
                    };
×
UNCOV
690
                }
×
UNCOV
691
            }
×
UNCOV
692
        }
×
693

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

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

UNCOV
712
        let won_ongoing_tenure_sortition =
×
UNCOV
713
            canonical_stacks_snapshot.miner_pk_hash.as_ref() == Some(mining_pkh);
×
UNCOV
714
        if won_ongoing_tenure_sortition {
×
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?
UNCOV
716
            if let Ok(has_higher) = Self::has_higher_sortition_commits_to_stacks_tip_tenure(
×
UNCOV
717
                &self.sortdb,
×
UNCOV
718
                &mut self.chainstate,
×
UNCOV
719
                &sn,
×
UNCOV
720
                &canonical_stacks_snapshot,
×
UNCOV
721
            ) {
×
UNCOV
722
                if has_higher {
×
UNCOV
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.";
×
724
                            "tenure_extend_wait_timeout_ms" => self.config.miner.tenure_extend_wait_timeout.as_millis(),
×
725
                    );
UNCOV
726
                    self.tenure_extend_time = Some(TenureExtendTime::unresponsive_winner(
×
UNCOV
727
                        self.config.miner.tenure_extend_wait_timeout,
×
UNCOV
728
                    ));
×
729
                } else {
UNCOV
730
                    info!("Relayer: no valid sortition since our last winning sortition. Will extend tenure.");
×
UNCOV
731
                    self.tenure_extend_time = Some(TenureExtendTime::immediate(
×
UNCOV
732
                        TenureExtendReason::BadSortitionWinner,
×
UNCOV
733
                    ));
×
734
                }
735
            }
×
UNCOV
736
        }
×
UNCOV
737
        MinerDirective::StopTenure
×
UNCOV
738
    }
×
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.
UNCOV
763
    fn choose_directive_sortition_without_winner(
×
UNCOV
764
        &mut self,
×
UNCOV
765
        sn: BlockSnapshot,
×
UNCOV
766
        mining_pk: &Hash160,
×
UNCOV
767
    ) -> Option<MinerDirective> {
×
UNCOV
768
        let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
×
UNCOV
769
            SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn())
×
UNCOV
770
                .expect("FATAL: failed to query sortition DB for stacks tip");
×
UNCOV
771
        let canonical_stacks_snapshot =
×
UNCOV
772
            SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &canonical_stacks_tip_ch)
×
UNCOV
773
                .expect("FATAL: failed to query sortiiton DB for epoch")
×
UNCOV
774
                .expect("FATAL: no sortition for canonical stacks tip");
×
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.
UNCOV
779
        let cur_epoch = SortitionDB::get_stacks_epoch(
×
UNCOV
780
            self.sortdb.conn(),
×
UNCOV
781
            canonical_stacks_snapshot.block_height,
×
782
        )
UNCOV
783
        .expect("FATAL: failed to query sortition DB for epoch")
×
UNCOV
784
        .expect("FATAL: no epoch defined for existing sortition");
×
785

UNCOV
786
        if cur_epoch.epoch_id < StacksEpochId::Epoch30 {
×
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;
×
UNCOV
792
        }
×
793

794
        // find out who won the last non-empty sortition. It may have been us.
UNCOV
795
        let Ok(last_winning_snapshot) = Self::get_last_winning_snapshot(&self.sortdb, &sn)
×
UNCOV
796
            .inspect_err(|e| {
×
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.
UNCOV
806
        let won_last_winning_snapshot =
×
UNCOV
807
            last_winning_snapshot.miner_pk_hash.as_ref() == Some(mining_pk);
×
UNCOV
808
        let canonical_stacks_tip =
×
UNCOV
809
            StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh);
×
UNCOV
810
        let commits_to_tip_tenure = Self::sortition_commits_to_stacks_tip_tenure(
×
UNCOV
811
            &mut self.chainstate,
×
UNCOV
812
            &canonical_stacks_tip,
×
UNCOV
813
            &canonical_stacks_snapshot,
×
UNCOV
814
            &last_winning_snapshot,
×
UNCOV
815
        ).unwrap_or_else(|e| {
×
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

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

UNCOV
832
            if Self::need_block_found(&canonical_stacks_snapshot, &last_winning_snapshot) {
×
UNCOV
833
                info!(
×
834
                    "Relayer: will submit late BlockFound for {}",
UNCOV
835
                    &last_winning_snapshot.consensus_hash
×
836
                );
837
                // prepare to immediately extend after our BlockFound gets mined.
UNCOV
838
                self.tenure_extend_time = Some(TenureExtendTime::immediate(
×
UNCOV
839
                    TenureExtendReason::EmptySortition,
×
UNCOV
840
                ));
×
UNCOV
841
                return Some(MinerDirective::BeginTenure {
×
UNCOV
842
                    parent_tenure_start: StacksBlockId(
×
UNCOV
843
                        last_winning_snapshot.winning_stacks_block_hash.clone().0,
×
UNCOV
844
                    ),
×
UNCOV
845
                    burnchain_tip: sn,
×
UNCOV
846
                    election_block: last_winning_snapshot,
×
UNCOV
847
                    late: true,
×
UNCOV
848
                });
×
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
            }
×
UNCOV
862
        }
×
863

UNCOV
864
        let won_ongoing_tenure_sortition =
×
UNCOV
865
            canonical_stacks_snapshot.miner_pk_hash.as_ref() == Some(mining_pk);
×
UNCOV
866
        if won_ongoing_tenure_sortition {
×
UNCOV
867
            info!("Relayer: No sortition, but we produced the canonical Stacks tip. Will extend tenure.");
×
UNCOV
868
            if !won_last_winning_snapshot {
×
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.
UNCOV
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.");
×
UNCOV
873
                self.tenure_extend_time = Some(TenureExtendTime::unresponsive_winner(
×
UNCOV
874
                    self.config.miner.tenure_extend_wait_timeout,
×
UNCOV
875
                ));
×
UNCOV
876
                return None;
×
UNCOV
877
            }
×
UNCOV
878
            return Some(MinerDirective::ContinueTenure {
×
UNCOV
879
                new_burn_view: sn.consensus_hash,
×
UNCOV
880
            });
×
UNCOV
881
        }
×
882

UNCOV
883
        info!("Relayer: No sortition, and we did not produce the last Stacks tip. Will not mine.");
×
UNCOV
884
        return None;
×
UNCOV
885
    }
×
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(
5✔
893
        canonical_stacks_snapshot: &BlockSnapshot,
5✔
894
        last_winning_snapshot: &BlockSnapshot,
5✔
895
    ) -> bool {
5✔
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 {
5✔
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
4✔
908
            && canonical_stacks_snapshot.consensus_hash == last_winning_snapshot.consensus_hash
3✔
909
        {
910
            // this is the ongoing tenure snapshot. A BlockFound has already been issued.
911
            test_debug!(
1✔
912
                "Ongoing tenure {} already represents last-winning snapshot",
913
                &canonical_stacks_snapshot.consensus_hash
×
914
            );
915
            false
1✔
916
        } else {
917
            // The stacks tip is behind the last-won sortition, so a BlockFound is still needed.
918
            true
3✔
919
        }
920
    }
5✔
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)]
UNCOV
932
    fn process_sortition(
×
UNCOV
933
        &mut self,
×
UNCOV
934
        consensus_hash: ConsensusHash,
×
UNCOV
935
        burn_hash: BurnchainHeaderHash,
×
UNCOV
936
        committed_index_hash: StacksBlockId,
×
UNCOV
937
    ) -> Option<MinerDirective> {
×
UNCOV
938
        let sn = SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &consensus_hash)
×
UNCOV
939
            .expect("FATAL: failed to query sortition DB")
×
UNCOV
940
            .expect("FATAL: unknown consensus hash");
×
941

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

UNCOV
951
        let won_sortition = sn.sortition && was_winning_pkh;
×
UNCOV
952
        if won_sortition {
×
UNCOV
953
            increment_stx_blocks_mined_counter();
×
UNCOV
954
        }
×
UNCOV
955
        self.globals.set_last_sortition(sn.clone());
×
UNCOV
956
        self.globals.counters.bump_blocks_processed();
×
UNCOV
957
        self.globals.counters.bump_sortitions_processed();
×
958

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

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

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

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

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

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

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

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

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

UNCOV
1039
        debug!(
×
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

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

UNCOV
1048
        let mut op_signer = self.keychain.generate_op_signer();
×
UNCOV
1049
        if let Ok(txid) = self
×
UNCOV
1050
            .bitcoin_controller
×
UNCOV
1051
            .submit_operation(cur_epoch, op, &mut op_signer)
×
UNCOV
1052
        {
×
UNCOV
1053
            // advance key registration state
×
UNCOV
1054
            self.last_vrf_key_burn_height = Some(burn_block.block_height);
×
UNCOV
1055
            self.globals
×
UNCOV
1056
                .set_pending_leader_key_registration(burn_block.block_height, txid);
×
UNCOV
1057
            self.globals.counters.bump_naka_submitted_vrfs();
×
UNCOV
1058
        }
×
UNCOV
1059
    }
×
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
UNCOV
1069
    pub(crate) fn make_block_commit(
×
UNCOV
1070
        &mut self,
×
UNCOV
1071
        tip_block_ch: &ConsensusHash,
×
UNCOV
1072
        tip_block_bh: &BlockHeaderHash,
×
UNCOV
1073
    ) -> Result<LastCommit, NakamotoNodeError> {
×
UNCOV
1074
        let tip_block_id = StacksBlockId::new(tip_block_ch, tip_block_bh);
×
UNCOV
1075
        let sort_tip = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())
×
UNCOV
1076
            .map_err(|_| NakamotoNodeError::SnapshotNotFoundForChainTip)?;
×
1077

UNCOV
1078
        let stacks_tip = StacksBlockId::new(tip_block_ch, tip_block_bh);
×
1079

1080
        // sanity check -- this block must exist and have been processed locally
UNCOV
1081
        let highest_tenure_start_block_header = NakamotoChainState::get_tenure_start_block_header(
×
UNCOV
1082
            &mut self.chainstate.index_conn(),
×
UNCOV
1083
            &stacks_tip,
×
UNCOV
1084
            tip_block_ch,
×
1085
        )
UNCOV
1086
        .map_err(|e| {
×
1087
            error!(
×
1088
                "Relayer: Failed to get tenure-start block header for stacks tip {stacks_tip}: {e:?}"
1089
            );
1090
            NakamotoNodeError::ParentNotFound
×
1091
        })?
×
UNCOV
1092
        .ok_or_else(|| {
×
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.
UNCOV
1101
        let tip_vrf_proof = NakamotoChainState::get_block_vrf_proof(
×
UNCOV
1102
            &mut self.chainstate.index_conn(),
×
UNCOV
1103
            &stacks_tip,
×
UNCOV
1104
            tip_block_ch,
×
1105
        )
UNCOV
1106
        .map_err(|e| {
×
1107
            error!("Failed to load VRF proof for {tip_block_ch} off of {stacks_tip}: {e:?}");
×
1108
            NakamotoNodeError::ParentNotFound
×
1109
        })?
×
UNCOV
1110
        .ok_or_else(|| {
×
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!
UNCOV
1116
        let recipients = get_nakamoto_next_recipients(
×
UNCOV
1117
            &sort_tip,
×
UNCOV
1118
            &mut self.sortdb,
×
UNCOV
1119
            &mut self.chainstate,
×
UNCOV
1120
            &stacks_tip,
×
UNCOV
1121
            &self.burnchain,
×
1122
        )
UNCOV
1123
        .map_err(|e| {
×
1124
            error!("Relayer: Failure fetching recipient set: {e:?}");
×
1125
            NakamotoNodeError::SnapshotNotFoundForChainTip
×
1126
        })?;
×
1127

UNCOV
1128
        let commit_outs = if self
×
UNCOV
1129
            .burnchain
×
UNCOV
1130
            .is_in_prepare_phase(sort_tip.block_height + 1)
×
1131
        {
UNCOV
1132
            vec![PoxAddress::standard_burn_address(self.config.is_mainnet())]
×
1133
        } else {
UNCOV
1134
            RewardSetInfo::into_commit_outs(recipients, self.config.is_mainnet())
×
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.
UNCOV
1141
        let Ok(Some(tip_tenure_sortition)) =
×
UNCOV
1142
            SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), tip_block_ch)
×
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.
UNCOV
1150
        let commit_parent_block_burn_height = tip_tenure_sortition.block_height;
×
UNCOV
1151
        let commit_parent_winning_vtxindex = if let Ok(Some(parent_winning_tx)) =
×
UNCOV
1152
            SortitionDB::get_block_commit(
×
UNCOV
1153
                self.sortdb.conn(),
×
UNCOV
1154
                &tip_tenure_sortition.winning_block_txid,
×
UNCOV
1155
                &tip_tenure_sortition.sortition_id,
×
1156
            ) {
UNCOV
1157
            parent_winning_tx.vtxindex
×
1158
        } else {
UNCOV
1159
            debug!(
×
1160
                "{}/{} ({}) must be a shadow block, since it has no block-commit",
1161
                &tip_block_bh, &tip_block_ch, &tip_block_id
×
1162
            );
UNCOV
1163
            let Ok(Some(parent_version)) =
×
UNCOV
1164
                NakamotoChainState::get_nakamoto_block_version(self.chainstate.db(), &tip_block_id)
×
1165
            else {
1166
                error!(
×
1167
                    "Relayer: Failed to lookup block version of {}",
1168
                    &tip_block_id
×
1169
                );
1170
                return Err(NakamotoNodeError::ParentNotFound);
×
1171
            };
1172

UNCOV
1173
            if !NakamotoBlockHeader::is_shadow_block_version(parent_version) {
×
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);
×
UNCOV
1179
            }
×
1180

UNCOV
1181
            0
×
1182
        };
1183

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

UNCOV
1192
        let (_, burnchain_config) = self.check_burnchain_config_changed();
×
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
UNCOV
1196
        let burn_parent_modulus = u8::try_from(sort_tip.block_height % BURN_BLOCK_MINED_AT_MODULUS)
×
UNCOV
1197
            .map_err(|_| {
×
1198
                error!("Relayer: Block mining modulus is not u8");
×
1199
                NakamotoNodeError::UnexpectedChainState
×
1200
            })?;
×
1201

1202
        // burnchain signer for this commit
UNCOV
1203
        let sender = self.keychain.get_burnchain_signer();
×
1204

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

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

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

UNCOV
1246
        Ok(LastCommit::new(
×
UNCOV
1247
            commit,
×
UNCOV
1248
            sort_tip,
×
UNCOV
1249
            stacks_tip,
×
UNCOV
1250
            highest_tenure_start_block_header.consensus_hash,
×
UNCOV
1251
            highest_tenure_start_block_header
×
UNCOV
1252
                .anchored_header
×
UNCOV
1253
                .block_hash(),
×
UNCOV
1254
            target_epoch.epoch_id,
×
UNCOV
1255
        ))
×
UNCOV
1256
    }
×
1257

1258
    #[cfg(test)]
UNCOV
1259
    fn fault_injection_stall_miner_startup() {
×
UNCOV
1260
        if TEST_MINER_THREAD_STALL.get() {
×
1261
            // Do an extra check just so we don't log EVERY time.
1262
            warn!("Relayer miner thread startup is stalled due to testing directive to stall the miner");
×
1263
            while TEST_MINER_THREAD_STALL.get() {
×
1264
                std::thread::sleep(std::time::Duration::from_millis(10));
×
1265
            }
×
1266
            warn!(
×
1267
                "Relayer miner thread startup is no longer stalled due to testing directive. Continuing..."
1268
            );
UNCOV
1269
        }
×
UNCOV
1270
    }
×
1271

1272
    #[cfg(not(test))]
1273
    fn fault_injection_stall_miner_startup() {}
1274

1275
    #[cfg(test)]
UNCOV
1276
    fn fault_injection_stall_miner_thread_startup() {
×
UNCOV
1277
        if TEST_MINER_THREAD_START_STALL.get() {
×
1278
            // Do an extra check just so we don't log EVERY time.
1279
            warn!("Miner thread startup is stalled due to testing directive");
×
1280
            while TEST_MINER_THREAD_START_STALL.get() {
×
1281
                std::thread::sleep(std::time::Duration::from_millis(10));
×
1282
            }
×
1283
            warn!(
×
1284
                "Miner thread startup is no longer stalled due to testing directive. Continuing..."
1285
            );
UNCOV
1286
        }
×
UNCOV
1287
    }
×
1288

1289
    #[cfg(not(test))]
1290
    fn fault_injection_stall_miner_thread_startup() {}
1291

1292
    /// Create the block miner thread state.
1293
    /// Only proceeds if all of the following are true:
1294
    /// * the miner is not blocked
1295
    /// * last_burn_block corresponds to the canonical sortition DB's chain tip
1296
    /// * the time of issuance is sufficiently recent
1297
    /// * there are no unprocessed stacks blocks in the staging DB
1298
    /// * 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)
1299
    /// * a miner thread is not running already
UNCOV
1300
    fn create_block_miner(
×
UNCOV
1301
        &mut self,
×
UNCOV
1302
        registered_key: RegisteredKey,
×
UNCOV
1303
        burn_election_block: BlockSnapshot,
×
UNCOV
1304
        burn_tip: BlockSnapshot,
×
UNCOV
1305
        parent_tenure_id: StacksBlockId,
×
UNCOV
1306
        reason: MinerReason,
×
UNCOV
1307
        burn_tip_at_start: &ConsensusHash,
×
UNCOV
1308
    ) -> Result<BlockMinerThread, NakamotoNodeError> {
×
UNCOV
1309
        if fault_injection_skip_mining(&self.config.node.rpc_bind, burn_tip.block_height) {
×
1310
            debug!(
×
1311
                "Relayer: fault injection skip mining at block height {}",
1312
                burn_tip.block_height
1313
            );
1314
            return Err(NakamotoNodeError::FaultInjection);
×
UNCOV
1315
        }
×
UNCOV
1316
        Self::fault_injection_stall_miner_startup();
×
1317

UNCOV
1318
        let burn_header_hash = burn_tip.burn_header_hash.clone();
×
UNCOV
1319
        let burn_chain_sn = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())
×
UNCOV
1320
            .expect("FATAL: failed to query sortition DB for canonical burn chain tip");
×
1321

UNCOV
1322
        let burn_chain_tip = burn_chain_sn.burn_header_hash.clone();
×
1323

UNCOV
1324
        if &burn_chain_sn.consensus_hash != burn_tip_at_start {
×
1325
            info!(
×
1326
                "Relayer: Drop stale RunTenure for {burn_header_hash}: current sortition is for {burn_chain_tip}"
1327
            );
1328
            self.globals.counters.bump_missed_tenures();
×
1329
            return Err(NakamotoNodeError::MissedMiningOpportunity);
×
UNCOV
1330
        }
×
1331

UNCOV
1332
        debug!(
×
1333
            "Relayer: Spawn tenure thread";
1334
            "height" => burn_tip.block_height,
×
1335
            "burn_header_hash" => %burn_header_hash,
1336
            "parent_tenure_id" => %parent_tenure_id,
1337
            "reason" => %reason,
1338
            "burn_election_block.consensus_hash" => %burn_election_block.consensus_hash,
1339
            "burn_tip.consensus_hash" => %burn_tip.consensus_hash,
1340
        );
1341

UNCOV
1342
        let miner_thread_state = BlockMinerThread::new(
×
UNCOV
1343
            self,
×
UNCOV
1344
            registered_key,
×
UNCOV
1345
            burn_election_block,
×
UNCOV
1346
            burn_tip.clone(),
×
UNCOV
1347
            parent_tenure_id,
×
UNCOV
1348
            burn_tip_at_start,
×
UNCOV
1349
            reason,
×
1350
        )?;
×
UNCOV
1351
        Ok(miner_thread_state)
×
UNCOV
1352
    }
×
1353

UNCOV
1354
    fn start_new_tenure(
×
UNCOV
1355
        &mut self,
×
UNCOV
1356
        parent_tenure_start: StacksBlockId,
×
UNCOV
1357
        block_election_snapshot: BlockSnapshot,
×
UNCOV
1358
        burn_tip: BlockSnapshot,
×
UNCOV
1359
        reason: MinerReason,
×
UNCOV
1360
        burn_tip_at_start: &ConsensusHash,
×
UNCOV
1361
    ) -> Result<(), NakamotoNodeError> {
×
1362
        // when starting a new tenure, block the mining thread if its currently running.
1363
        // the new mining thread will join it (so that the new mining thread stalls, not the relayer)
UNCOV
1364
        let prior_tenure_thread = self.miner_thread.take();
×
UNCOV
1365
        self.miner_thread_burn_view = None;
×
1366

UNCOV
1367
        let vrf_key = self
×
UNCOV
1368
            .globals
×
UNCOV
1369
            .get_leader_key_registration_state()
×
UNCOV
1370
            .get_active()
×
UNCOV
1371
            .ok_or_else(|| {
×
1372
                warn!("Trying to start new tenure, but no VRF key active");
×
1373
                NakamotoNodeError::NoVRFKeyActive
×
1374
            })?;
×
UNCOV
1375
        let new_miner_state = self.create_block_miner(
×
UNCOV
1376
            vrf_key,
×
UNCOV
1377
            block_election_snapshot,
×
UNCOV
1378
            burn_tip.clone(),
×
UNCOV
1379
            parent_tenure_start.clone(),
×
UNCOV
1380
            reason,
×
UNCOV
1381
            burn_tip_at_start,
×
1382
        )?;
×
UNCOV
1383
        let miner_abort_flag = new_miner_state.get_abort_flag();
×
1384

UNCOV
1385
        debug!("Relayer: starting new tenure thread");
×
1386

UNCOV
1387
        let rand_id = thread_rng().gen::<u32>();
×
UNCOV
1388
        let is_mock = if self.config.node.mock_mining {
×
UNCOV
1389
            "mock-"
×
1390
        } else {
UNCOV
1391
            ""
×
1392
        };
1393

UNCOV
1394
        let new_miner_handle = std::thread::Builder::new()
×
UNCOV
1395
            .name(format!("{is_mock}miner.{parent_tenure_start}.{rand_id}",))
×
UNCOV
1396
            .stack_size(BLOCK_PROCESSOR_STACK_SIZE)
×
UNCOV
1397
            .spawn(move || {
×
UNCOV
1398
                debug!(
×
1399
                    "New block miner thread ID is {:?}",
1400
                    std::thread::current().id()
×
1401
                );
UNCOV
1402
                Self::fault_injection_stall_miner_thread_startup();
×
UNCOV
1403
                if let Err(e) = new_miner_state.run_miner(prior_tenure_thread) {
×
UNCOV
1404
                    info!("Miner thread failed: {e:?}");
×
UNCOV
1405
                    Err(e)
×
1406
                } else {
1407
                    Ok(())
×
1408
                }
UNCOV
1409
            })
×
UNCOV
1410
            .map_err(|e| {
×
1411
                error!("Relayer: Failed to start tenure thread: {e:?}");
×
1412
                NakamotoNodeError::SpawnError(e)
×
1413
            })?;
×
UNCOV
1414
        debug!(
×
1415
            "Relayer: started tenure thread ID {:?}",
1416
            new_miner_handle.thread().id()
×
1417
        );
UNCOV
1418
        self.miner_thread
×
UNCOV
1419
            .replace(MinerStopHandle::new(new_miner_handle, miner_abort_flag));
×
UNCOV
1420
        self.miner_thread_burn_view.replace(burn_tip);
×
UNCOV
1421
        Ok(())
×
UNCOV
1422
    }
×
1423

UNCOV
1424
    fn stop_tenure(&mut self) -> Result<(), NakamotoNodeError> {
×
1425
        // when stopping a tenure, block the mining thread if its currently running, then join it.
1426
        // do this in a new thread will (so that the new thread stalls, not the relayer)
UNCOV
1427
        let Some(prior_tenure_thread) = self.miner_thread.take() else {
×
UNCOV
1428
            debug!("Relayer: no tenure thread to stop");
×
UNCOV
1429
            return Ok(());
×
1430
        };
UNCOV
1431
        self.miner_thread_burn_view = None;
×
1432

UNCOV
1433
        let id = prior_tenure_thread.inner_thread().id();
×
UNCOV
1434
        let abort_flag = prior_tenure_thread.abort_flag.clone();
×
UNCOV
1435
        let globals = self.globals.clone();
×
1436

UNCOV
1437
        let stop_handle = std::thread::Builder::new()
×
UNCOV
1438
            .name(format!(
×
1439
                "tenure-stop({:?})-{}",
1440
                id, self.local_peer.data_url
1441
            ))
UNCOV
1442
            .spawn(move || prior_tenure_thread.stop(&globals))
×
UNCOV
1443
            .map_err(|e| {
×
1444
                error!("Relayer: Failed to spawn a stop-tenure thread: {e:?}");
×
1445
                NakamotoNodeError::SpawnError(e)
×
1446
            })?;
×
1447

UNCOV
1448
        self.miner_thread
×
UNCOV
1449
            .replace(MinerStopHandle::new(stop_handle, abort_flag));
×
UNCOV
1450
        debug!("Relayer: stopped tenure thread ID {id:?}");
×
UNCOV
1451
        Ok(())
×
UNCOV
1452
    }
×
1453

1454
    /// Get the public key hash for the mining key.
UNCOV
1455
    fn get_mining_key_pkh(&self) -> Option<Hash160> {
×
UNCOV
1456
        let Some(ref mining_key) = self.config.miner.mining_key else {
×
1457
            return None;
×
1458
        };
UNCOV
1459
        Some(Hash160::from_node_public_key(
×
UNCOV
1460
            &StacksPublicKey::from_private(mining_key),
×
UNCOV
1461
        ))
×
UNCOV
1462
    }
×
1463

1464
    /// Helper method to get the last snapshot with a winner
UNCOV
1465
    fn get_last_winning_snapshot(
×
UNCOV
1466
        sortdb: &SortitionDB,
×
UNCOV
1467
        sort_tip: &BlockSnapshot,
×
UNCOV
1468
    ) -> Result<BlockSnapshot, NakamotoNodeError> {
×
UNCOV
1469
        let ih = sortdb.index_handle(&sort_tip.sortition_id);
×
UNCOV
1470
        Ok(ih.get_last_snapshot_with_sortition(sort_tip.block_height)?)
×
UNCOV
1471
    }
×
1472

1473
    /// Returns true if the sortition `sn` commits to the tenure start block of the ongoing Stacks tenure `stacks_tip_sn`.
1474
    /// Returns false otherwise.
UNCOV
1475
    fn sortition_commits_to_stacks_tip_tenure(
×
UNCOV
1476
        chain_state: &mut StacksChainState,
×
UNCOV
1477
        stacks_tip_id: &StacksBlockId,
×
UNCOV
1478
        stacks_tip_sn: &BlockSnapshot,
×
UNCOV
1479
        sn: &BlockSnapshot,
×
UNCOV
1480
    ) -> Result<bool, NakamotoNodeError> {
×
UNCOV
1481
        if !sn.sortition {
×
1482
            // definitely not a valid sortition
1483
            debug!("Relayer: Sortition {} is empty", &sn.consensus_hash);
×
1484
            return Ok(false);
×
UNCOV
1485
        }
×
1486
        // The sortition must commit to the tenure start block of the ongoing Stacks tenure.
UNCOV
1487
        let mut ic = chain_state.index_conn();
×
UNCOV
1488
        let parent_tenure_id = StacksBlockId(sn.winning_stacks_block_hash.clone().0);
×
UNCOV
1489
        let highest_tenure_start_block_header = NakamotoChainState::get_tenure_start_block_header(
×
UNCOV
1490
            &mut ic,
×
UNCOV
1491
            stacks_tip_id,
×
UNCOV
1492
            &stacks_tip_sn.consensus_hash,
×
1493
        )?
×
UNCOV
1494
        .ok_or_else(|| {
×
1495
            error!(
×
1496
                "Relayer: Failed to find tenure-start block header for stacks tip {stacks_tip_id}"
1497
            );
1498
            NakamotoNodeError::ParentNotFound
×
1499
        })?;
×
1500

UNCOV
1501
        let highest_tenure_start_block_id = highest_tenure_start_block_header.index_block_hash();
×
UNCOV
1502
        if highest_tenure_start_block_id != parent_tenure_id {
×
UNCOV
1503
            debug!("Relayer: Sortition {} is at the tip, but does not commit to {parent_tenure_id} so cannot be valid", &sn.consensus_hash;
×
1504
                "highest_tenure_start_block_header_block_id" => %highest_tenure_start_block_id);
UNCOV
1505
            return Ok(false);
×
UNCOV
1506
        }
×
1507

UNCOV
1508
        Ok(true)
×
UNCOV
1509
    }
×
1510

1511
    /// Determine the highest sortition higher than `elected_tenure_id`, but no higher than
1512
    /// `sort_tip` whose winning commit's parent tenure ID matches the `stacks_tip`,
1513
    /// and whose consensus hash matches the `stacks_tip`'s tenure ID.
1514
    ///
1515
    /// Returns Ok(true) if such a sortition is found, and is higher than that of
1516
    /// `elected_tenure_id`.
1517
    /// Returns Ok(false) if no such sortition is found.
1518
    /// Returns Err(..) on DB errors.
UNCOV
1519
    fn has_higher_sortition_commits_to_stacks_tip_tenure(
×
UNCOV
1520
        sortdb: &SortitionDB,
×
UNCOV
1521
        chain_state: &mut StacksChainState,
×
UNCOV
1522
        sortition_tip: &BlockSnapshot,
×
UNCOV
1523
        elected_tenure: &BlockSnapshot,
×
UNCOV
1524
    ) -> Result<bool, NakamotoNodeError> {
×
UNCOV
1525
        let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
×
UNCOV
1526
            SortitionDB::get_canonical_stacks_chain_tip_hash(sortdb.conn()).unwrap();
×
UNCOV
1527
        let canonical_stacks_tip =
×
UNCOV
1528
            StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh);
×
1529

UNCOV
1530
        let Ok(Some(canonical_stacks_tip_sn)) =
×
UNCOV
1531
            SortitionDB::get_block_snapshot_consensus(sortdb.conn(), &canonical_stacks_tip_ch)
×
1532
        else {
1533
            return Err(NakamotoNodeError::ParentNotFound);
×
1534
        };
1535

UNCOV
1536
        sortdb
×
UNCOV
1537
            .find_from(sortition_tip.clone(), |cursor| {
×
UNCOV
1538
                debug!(
×
1539
                    "Relayer: check sortition {} to see if it is valid",
1540
                    &cursor.consensus_hash
×
1541
                );
1542
                // have we reached the last tenure we're looking at?
UNCOV
1543
                if cursor.block_height <= elected_tenure.block_height {
×
UNCOV
1544
                    return Ok(FindIter::Halt);
×
UNCOV
1545
                }
×
1546

UNCOV
1547
                if Self::sortition_commits_to_stacks_tip_tenure(
×
UNCOV
1548
                    chain_state,
×
UNCOV
1549
                    &canonical_stacks_tip,
×
UNCOV
1550
                    &canonical_stacks_tip_sn,
×
UNCOV
1551
                    &cursor,
×
1552
                )? {
×
UNCOV
1553
                    return Ok(FindIter::Found(()));
×
UNCOV
1554
                }
×
1555

1556
                // nope. continue the search
UNCOV
1557
                return Ok(FindIter::Continue);
×
UNCOV
1558
            })
×
UNCOV
1559
            .map(|found| found.is_some())
×
UNCOV
1560
    }
×
1561

1562
    /// Attempt to continue a miner's tenure into the next burn block.
1563
    /// This is allowed if the miner won the last good sortition -- that is, the sortition which
1564
    /// elected the local view of the canonical Stacks fork's ongoing tenure.
1565
    /// Or if the miner won the last valid sortition prior to the current and the current miner
1566
    /// has failed to produce a block before the required timeout.
1567
    ///
1568
    /// This function assumes that the caller has checked that the sortition referred to by
1569
    /// `new_burn_view` does not have a sortition winner or that the winner has not produced a
1570
    /// valid block yet.
UNCOV
1571
    fn continue_tenure(&mut self, new_burn_view: ConsensusHash) -> Result<(), NakamotoNodeError> {
×
UNCOV
1572
        if let Err(e) = self.stop_tenure() {
×
1573
            error!("Relayer: Failed to stop tenure: {e:?}");
×
1574
            return Ok(());
×
UNCOV
1575
        }
×
UNCOV
1576
        debug!("Relayer: successfully stopped tenure; will try to continue.");
×
1577

1578
        // try to extend, but only if we aren't already running a thread for the current or newer
1579
        // burnchain view
UNCOV
1580
        let Ok(sn) =
×
UNCOV
1581
            SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn()).inspect_err(|e| {
×
1582
                error!("Relayer: failed to read canonical burnchain sortition: {e:?}");
×
1583
            })
×
1584
        else {
1585
            return Ok(());
×
1586
        };
1587

UNCOV
1588
        if let Some(miner_thread_burn_view) = self.miner_thread_burn_view.as_ref() {
×
1589
            // a miner thread is already running.  If its burn view is the same as the canonical
1590
            // tip, then do nothing
1591
            if sn.consensus_hash == miner_thread_burn_view.consensus_hash {
×
1592
                info!("Relayer: will not tenure extend -- the current miner thread's burn view matches the sortition tip"; "sortition tip" => %sn.consensus_hash);
×
1593
                return Ok(());
×
1594
            }
×
UNCOV
1595
        }
×
1596

1597
        // Get the necessary snapshots and state
UNCOV
1598
        let burn_tip =
×
UNCOV
1599
            SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &new_burn_view)?
×
UNCOV
1600
                .ok_or_else(|| {
×
1601
                    error!("Relayer: failed to get block snapshot for new burn view");
×
1602
                    NakamotoNodeError::SnapshotNotFoundForChainTip
×
1603
                })?;
×
UNCOV
1604
        let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
×
UNCOV
1605
            SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn()).unwrap();
×
UNCOV
1606
        let canonical_stacks_tip =
×
UNCOV
1607
            StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh);
×
UNCOV
1608
        let canonical_stacks_snapshot = SortitionDB::get_block_snapshot_consensus(
×
UNCOV
1609
            self.sortdb.conn(),
×
UNCOV
1610
            &canonical_stacks_tip_ch,
×
1611
        )?
×
UNCOV
1612
        .ok_or_else(|| {
×
1613
            error!("Relayer: failed to get block snapshot for canonical tip");
×
1614
            NakamotoNodeError::SnapshotNotFoundForChainTip
×
1615
        })?;
×
UNCOV
1616
        let reason = MinerReason::Extended {
×
UNCOV
1617
            burn_view_consensus_hash: new_burn_view.clone(),
×
UNCOV
1618
        };
×
1619

UNCOV
1620
        if let Err(e) = self.start_new_tenure(
×
UNCOV
1621
            canonical_stacks_tip.clone(),
×
UNCOV
1622
            canonical_stacks_snapshot.clone(),
×
UNCOV
1623
            burn_tip.clone(),
×
UNCOV
1624
            reason.clone(),
×
UNCOV
1625
            &new_burn_view,
×
UNCOV
1626
        ) {
×
1627
            error!("Relayer: Failed to start new tenure: {e:?}");
×
1628
        } else {
UNCOV
1629
            debug!("Relayer: successfully started new tenure.";
×
1630
                   "parent_tenure_start" => %canonical_stacks_tip,
1631
                   "burn_tip" => %burn_tip.consensus_hash,
1632
                   "burn_view_snapshot" => %burn_tip.consensus_hash,
1633
                   "block_election_snapshot" => %canonical_stacks_snapshot.consensus_hash,
1634
                   "reason" => %reason);
1635
        }
UNCOV
1636
        Ok(())
×
UNCOV
1637
    }
×
1638

UNCOV
1639
    fn handle_sortition(
×
UNCOV
1640
        &mut self,
×
UNCOV
1641
        consensus_hash: ConsensusHash,
×
UNCOV
1642
        burn_hash: BurnchainHeaderHash,
×
UNCOV
1643
        committed_index_hash: StacksBlockId,
×
UNCOV
1644
    ) -> bool {
×
UNCOV
1645
        let miner_instruction =
×
UNCOV
1646
            match self.process_sortition(consensus_hash, burn_hash, committed_index_hash) {
×
UNCOV
1647
                Some(miner_instruction) => miner_instruction,
×
1648
                None => {
UNCOV
1649
                    return true;
×
1650
                }
1651
            };
1652

UNCOV
1653
        match miner_instruction {
×
1654
            MinerDirective::BeginTenure {
UNCOV
1655
                parent_tenure_start,
×
UNCOV
1656
                burnchain_tip,
×
UNCOV
1657
                election_block,
×
UNCOV
1658
                late,
×
UNCOV
1659
            } => match self.start_new_tenure(
×
UNCOV
1660
                parent_tenure_start.clone(),
×
UNCOV
1661
                election_block.clone(),
×
UNCOV
1662
                election_block.clone(),
×
UNCOV
1663
                MinerReason::BlockFound { late },
×
UNCOV
1664
                &burnchain_tip.consensus_hash,
×
UNCOV
1665
            ) {
×
1666
                Ok(()) => {
UNCOV
1667
                    debug!("Relayer: successfully started new tenure.";
×
1668
                           "parent_tenure_start" => %parent_tenure_start,
1669
                           "burn_tip" => %burnchain_tip.consensus_hash,
1670
                           "burn_view_snapshot" => %burnchain_tip.consensus_hash,
1671
                           "block_election_snapshot" => %burnchain_tip.consensus_hash,
1672
                           "reason" => %MinerReason::BlockFound { late });
×
1673
                }
1674
                Err(e) => {
×
1675
                    error!("Relayer: Failed to start new tenure: {e:?}");
×
1676
                }
1677
            },
UNCOV
1678
            MinerDirective::ContinueTenure { new_burn_view } => {
×
UNCOV
1679
                match self.continue_tenure(new_burn_view) {
×
1680
                    Ok(()) => {
UNCOV
1681
                        debug!("Relayer: successfully handled continue tenure.");
×
1682
                    }
1683
                    Err(e) => {
×
1684
                        error!("Relayer: Failed to continue tenure: {e:?}");
×
1685
                        return false;
×
1686
                    }
1687
                }
1688
            }
UNCOV
1689
            MinerDirective::StopTenure => match self.stop_tenure() {
×
1690
                Ok(()) => {
UNCOV
1691
                    debug!("Relayer: successfully stopped tenure.");
×
1692
                }
1693
                Err(e) => {
×
1694
                    error!("Relayer: Failed to stop tenure: {e:?}");
×
1695
                }
1696
            },
1697
        }
1698

UNCOV
1699
        self.globals.counters.bump_naka_miner_directives();
×
UNCOV
1700
        true
×
UNCOV
1701
    }
×
1702

1703
    #[cfg(test)]
UNCOV
1704
    fn fault_injection_skip_block_commit(&self) -> bool {
×
UNCOV
1705
        self.globals.counters.skip_commit_op.get()
×
UNCOV
1706
    }
×
1707

1708
    #[cfg(not(test))]
1709
    fn fault_injection_skip_block_commit(&self) -> bool {
1710
        false
1711
    }
1712

1713
    /// Get the canonical tip for the miner to commit to.
1714
    /// This is provided as a separate function so that it can be overridden for testing.
1715
    #[cfg(not(test))]
1716
    fn fault_injection_get_tip_for_commit(&self) -> Option<(ConsensusHash, BlockHeaderHash)> {
1717
        None
1718
    }
1719

1720
    #[cfg(test)]
UNCOV
1721
    fn fault_injection_get_tip_for_commit(&self) -> Option<(ConsensusHash, BlockHeaderHash)> {
×
UNCOV
1722
        TEST_MINER_COMMIT_TIP.get()
×
UNCOV
1723
    }
×
1724

UNCOV
1725
    fn get_commit_for_tip(&mut self) -> Result<(ConsensusHash, BlockHeaderHash), DbError> {
×
UNCOV
1726
        if let Some((consensus_hash, block_header_hash)) = self.fault_injection_get_tip_for_commit()
×
1727
        {
UNCOV
1728
            info!("Relayer: using test tip for commit";
×
1729
                "consensus_hash" => %consensus_hash,
1730
                "block_header_hash" => %block_header_hash,
1731
            );
UNCOV
1732
            Ok((consensus_hash, block_header_hash))
×
1733
        } else {
UNCOV
1734
            SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn())
×
1735
        }
UNCOV
1736
    }
×
1737

1738
    /// Generate and submit the next block-commit, and record it locally
UNCOV
1739
    fn issue_block_commit(&mut self) -> Result<(), NakamotoNodeError> {
×
UNCOV
1740
        if self.fault_injection_skip_block_commit() {
×
UNCOV
1741
            debug!(
×
1742
                "Relayer: not submitting block-commit to bitcoin network due to test directive."
1743
            );
UNCOV
1744
            return Ok(());
×
UNCOV
1745
        }
×
UNCOV
1746
        let (tip_block_ch, tip_block_bh) = self.get_commit_for_tip().unwrap_or_else(|e| {
×
1747
            panic!("Failed to load canonical stacks tip: {e:?}");
×
1748
        });
UNCOV
1749
        let mut last_committed = self.make_block_commit(&tip_block_ch, &tip_block_bh)?;
×
1750

UNCOV
1751
        let Some(tip_height) = NakamotoChainState::get_block_header(
×
UNCOV
1752
            self.chainstate.db(),
×
UNCOV
1753
            &StacksBlockId::new(&tip_block_ch, &tip_block_bh),
×
1754
        )
UNCOV
1755
        .map_err(|e| {
×
1756
            warn!("Relayer: failed to load tip {tip_block_ch}/{tip_block_bh}: {e:?}");
×
1757
            NakamotoNodeError::ParentNotFound
×
1758
        })?
×
UNCOV
1759
        .map(|header| header.stacks_block_height) else {
×
1760
            warn!(
×
1761
                "Relayer: failed to load height for tip {tip_block_ch}/{tip_block_bh} (got None)"
1762
            );
1763
            return Err(NakamotoNodeError::ParentNotFound);
×
1764
        };
1765

1766
        // sign and broadcast
UNCOV
1767
        let mut op_signer = self.keychain.generate_op_signer();
×
UNCOV
1768
        let res = self.bitcoin_controller.submit_operation(
×
UNCOV
1769
            *last_committed.get_epoch_id(),
×
UNCOV
1770
            BlockstackOperationType::LeaderBlockCommit(last_committed.get_block_commit().clone()),
×
UNCOV
1771
            &mut op_signer,
×
1772
        );
UNCOV
1773
        let txid = match res {
×
UNCOV
1774
            Ok(txid) => txid,
×
UNCOV
1775
            Err(e) => {
×
UNCOV
1776
                if self.config.node.mock_mining {
×
UNCOV
1777
                    debug!("Relayer: Mock-mining enabled; not sending Bitcoin transaction");
×
UNCOV
1778
                    return Ok(());
×
UNCOV
1779
                }
×
UNCOV
1780
                warn!("Failed to submit block-commit bitcoin transaction: {e}");
×
UNCOV
1781
                return Err(NakamotoNodeError::BurnchainSubmissionFailed(e));
×
1782
            }
1783
        };
1784

UNCOV
1785
        info!(
×
1786
            "Relayer: Submitted block-commit";
1787
            "tip_consensus_hash" => %tip_block_ch,
1788
            "tip_block_hash" => %tip_block_bh,
1789
            "tip_height" => %tip_height,
UNCOV
1790
            "tip_block_id" => %StacksBlockId::new(&tip_block_ch, &tip_block_bh),
×
1791
            "txid" => %txid,
1792
        );
1793

1794
        // update local state
UNCOV
1795
        last_committed.set_txid(&txid);
×
UNCOV
1796
        self.globals.counters.bump_naka_submitted_commits(
×
UNCOV
1797
            last_committed.burn_tip.block_height,
×
UNCOV
1798
            tip_height,
×
UNCOV
1799
            last_committed.block_commit.burn_fee,
×
UNCOV
1800
            &last_committed.tenure_consensus_hash,
×
1801
        );
UNCOV
1802
        self.last_committed = Some(last_committed);
×
1803

UNCOV
1804
        Ok(())
×
UNCOV
1805
    }
×
1806

1807
    /// Determine what the relayer should do to advance the chain.
1808
    /// * If this isn't a miner, then it's always nothing.
1809
    /// * Otherwise, if we haven't done so already, go register a VRF public key
1810
    /// * If the stacks chain tip or burnchain tip has changed, then issue a block-commit
1811
    /// * If the last burn view we started a miner for is not the canonical burn view, then
1812
    /// try and start a new tenure (or continue an existing one).
UNCOV
1813
    fn initiative(&mut self) -> Result<Option<RelayerDirective>, NakamotoNodeError> {
×
UNCOV
1814
        if !self.is_miner {
×
UNCOV
1815
            return Ok(None);
×
UNCOV
1816
        }
×
1817

UNCOV
1818
        match self.globals.get_leader_key_registration_state() {
×
1819
            // do we need a VRF key registration?
1820
            LeaderKeyRegistrationState::Inactive => {
UNCOV
1821
                let sort_tip = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())?;
×
UNCOV
1822
                return Ok(Some(RelayerDirective::RegisterKey(sort_tip)));
×
1823
            }
1824
            // are we still waiting on a pending registration?
1825
            LeaderKeyRegistrationState::Pending(..) => {
UNCOV
1826
                return Ok(None);
×
1827
            }
UNCOV
1828
            LeaderKeyRegistrationState::Active(_) => {}
×
1829
        };
1830

1831
        // load up canonical sortition and stacks tips
UNCOV
1832
        let sort_tip = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())?;
×
1833

1834
        // NOTE: this may be an epoch2x tip
UNCOV
1835
        let (stacks_tip_ch, stacks_tip_bh) =
×
UNCOV
1836
            SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn())?;
×
UNCOV
1837
        let stacks_tip = StacksBlockId::new(&stacks_tip_ch, &stacks_tip_bh);
×
1838

1839
        // check stacks and sortition tips to see if any chainstate change has happened.
1840
        // did our view of the sortition history change?
1841
        // if so, then let's try and confirm the highest tenure so far.
UNCOV
1842
        let burnchain_changed = self
×
UNCOV
1843
            .last_committed
×
UNCOV
1844
            .as_ref()
×
UNCOV
1845
            .map(|cmt| cmt.get_burn_tip().consensus_hash != sort_tip.consensus_hash)
×
UNCOV
1846
            .unwrap_or(true);
×
1847

UNCOV
1848
        let highest_tenure_changed = self
×
UNCOV
1849
            .last_committed
×
UNCOV
1850
            .as_ref()
×
UNCOV
1851
            .map(|cmt| cmt.get_tenure_id() != &stacks_tip_ch)
×
UNCOV
1852
            .unwrap_or(true);
×
1853

UNCOV
1854
        debug!("Relayer: initiative to commit";
×
1855
               "sortititon tip" => %sort_tip.consensus_hash,
1856
               "stacks tip" => %stacks_tip,
1857
               "stacks_tip_ch" => %stacks_tip_ch,
1858
               "stacks_tip_bh" => %stacks_tip_bh,
1859
               "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()),
×
1860
               "last-commit ongoing tenure" => %self.last_committed.as_ref().map(|cmt| cmt.get_tenure_id().to_string()).unwrap_or("(not set)".to_string()),
×
1861
               "burnchain view changed?" => %burnchain_changed,
1862
               "highest tenure changed?" => %highest_tenure_changed);
1863

1864
        // If the miner spend or config has changed, we want to RBF with new config values.
UNCOV
1865
        let (burnchain_config_changed, _) = self.check_burnchain_config_changed();
×
UNCOV
1866
        let miner_config_changed = self.check_miner_config_changed();
×
1867

UNCOV
1868
        if burnchain_config_changed || miner_config_changed {
×
UNCOV
1869
            info!("Miner spend or config changed; issuing block commit with new values";
×
1870
                "miner_spend_changed" => %burnchain_config_changed,
1871
                "miner_config_changed" => %miner_config_changed,
1872
            );
UNCOV
1873
            return Ok(Some(RelayerDirective::IssueBlockCommit(
×
UNCOV
1874
                stacks_tip_ch,
×
UNCOV
1875
                stacks_tip_bh,
×
UNCOV
1876
            )));
×
UNCOV
1877
        }
×
1878

UNCOV
1879
        if !burnchain_changed && !highest_tenure_changed {
×
1880
            // nothing to do
UNCOV
1881
            return Ok(None);
×
UNCOV
1882
        }
×
1883

UNCOV
1884
        if highest_tenure_changed {
×
1885
            // highest-tenure view changed, so we need to send (or RBF) a commit
UNCOV
1886
            return Ok(Some(RelayerDirective::IssueBlockCommit(
×
UNCOV
1887
                stacks_tip_ch,
×
UNCOV
1888
                stacks_tip_bh,
×
UNCOV
1889
            )));
×
UNCOV
1890
        }
×
1891

UNCOV
1892
        debug!("Relayer: burnchain view changed, but highest tenure did not");
×
1893
        // First, check if the changed burnchain view includes any
1894
        // sortitions. If it doesn't submit a block commit immediately.
1895
        //
1896
        // If it does, then wait a bit for the first block in the new
1897
        // tenure to arrive. This is to avoid submitting a block
1898
        // commit that will be immediately RBFed when the first
1899
        // block arrives.
UNCOV
1900
        if let Some(last_committed) = self.last_committed.as_ref() {
×
1901
            // check if all the sortitions after `last_tenure` are empty sortitions. if they are,
1902
            //  we don't need to wait at all to submit a commit
UNCOV
1903
            let last_tenure_tip_height = SortitionDB::get_consensus_hash_height(
×
UNCOV
1904
                &self.sortdb,
×
UNCOV
1905
                last_committed.get_tenure_id(),
×
1906
            )?
×
UNCOV
1907
            .ok_or_else(|| NakamotoNodeError::ParentNotFound)?;
×
UNCOV
1908
            let no_sortitions_after_last_tenure = self
×
UNCOV
1909
                .sortdb
×
UNCOV
1910
                .find_in_canonical::<_, _, NakamotoNodeError>(|cursor| {
×
UNCOV
1911
                    if cursor.block_height <= last_tenure_tip_height {
×
UNCOV
1912
                        return Ok(FindIter::Halt);
×
UNCOV
1913
                    }
×
UNCOV
1914
                    if cursor.sortition {
×
UNCOV
1915
                        return Ok(FindIter::Found(()));
×
UNCOV
1916
                    }
×
UNCOV
1917
                    Ok(FindIter::Continue)
×
UNCOV
1918
                })?
×
UNCOV
1919
                .is_none();
×
UNCOV
1920
            if no_sortitions_after_last_tenure {
×
UNCOV
1921
                return Ok(Some(RelayerDirective::IssueBlockCommit(
×
UNCOV
1922
                    stacks_tip_ch,
×
UNCOV
1923
                    stacks_tip_bh,
×
UNCOV
1924
                )));
×
UNCOV
1925
            }
×
1926
        }
×
1927

UNCOV
1928
        if self.new_tenure_timeout.is_ready(
×
UNCOV
1929
            &sort_tip.consensus_hash,
×
UNCOV
1930
            &self.config.miner.block_commit_delay,
×
1931
        ) {
UNCOV
1932
            return Ok(Some(RelayerDirective::IssueBlockCommit(
×
UNCOV
1933
                stacks_tip_ch,
×
UNCOV
1934
                stacks_tip_bh,
×
UNCOV
1935
            )));
×
1936
        } else {
UNCOV
1937
            if let Some(deadline) = self
×
UNCOV
1938
                .new_tenure_timeout
×
UNCOV
1939
                .deadline(&self.config.miner.block_commit_delay)
×
UNCOV
1940
            {
×
UNCOV
1941
                self.next_initiative = std::cmp::min(self.next_initiative, deadline);
×
UNCOV
1942
            }
×
1943

UNCOV
1944
            return Ok(None);
×
1945
        }
UNCOV
1946
    }
×
1947

1948
    /// Try to start up a tenure-extend if the tenure_extend_time has expired.
1949
    ///
1950
    /// Will check if the tenure-extend time was set and has expired. If so, will
1951
    /// check if the current miner thread needs to issue a BlockFound or if it can
1952
    /// immediately tenure-extend.
1953
    ///
1954
    /// Note: tenure_extend_time is only set to Some(_) if during sortition processing, the sortition
1955
    /// winner commit is corrupted or the winning miner has yet to produce a block.
UNCOV
1956
    fn check_tenure_timers(&mut self) {
×
1957
        // Should begin a tenure-extend?
UNCOV
1958
        let Some(tenure_extend_time) = self.tenure_extend_time.clone() else {
×
1959
            // No tenure extend time set, so nothing to do.
UNCOV
1960
            return;
×
1961
        };
UNCOV
1962
        if !tenure_extend_time.should_extend() {
×
UNCOV
1963
            test_debug!(
×
1964
                "Relayer: will not try to tenure-extend yet ({} <= {})",
1965
                tenure_extend_time.elapsed().as_secs(),
×
1966
                tenure_extend_time.timeout().as_secs()
×
1967
            );
UNCOV
1968
            return;
×
UNCOV
1969
        }
×
1970

UNCOV
1971
        let Some(mining_pkh) = self.get_mining_key_pkh() else {
×
1972
            // This shouldn't really ever hit, but just in case.
1973
            warn!("Will not tenure extend -- no mining key");
×
1974
            // If we don't have a mining key set, don't bother checking again.
1975
            self.tenure_extend_time = None;
×
1976
            return;
×
1977
        };
1978
        // reset timer so we can try again if for some reason a miner was already running (e.g. a
1979
        // blockfound from earlier).
UNCOV
1980
        self.tenure_extend_time
×
UNCOV
1981
            .as_mut()
×
UNCOV
1982
            .map(|t| t.refresh(self.config.miner.tenure_extend_poll_timeout));
×
1983
        // try to extend, but only if we aren't already running a thread for the current or newer
1984
        // burnchain view
UNCOV
1985
        let Ok(burn_tip) = SortitionDB::get_canonical_burn_chain_tip(self.sortdb.conn())
×
UNCOV
1986
            .inspect_err(|e| {
×
1987
                error!("Failed to read canonical burnchain sortition: {e:?}");
×
1988
            })
×
1989
        else {
1990
            return;
×
1991
        };
1992

UNCOV
1993
        if let Some(miner_thread_burn_view) = self.miner_thread_burn_view.as_ref() {
×
1994
            // a miner thread is already running.  If its burn view is the same as the canonical
1995
            // tip, then do nothing for now
UNCOV
1996
            if burn_tip.consensus_hash == miner_thread_burn_view.consensus_hash {
×
1997
                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);
×
1998
                // Do not reset the timer, as we may be able to extend later.
1999
                return;
×
UNCOV
2000
            }
×
UNCOV
2001
        }
×
2002

UNCOV
2003
        let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
×
UNCOV
2004
            SortitionDB::get_canonical_stacks_chain_tip_hash(self.sortdb.conn())
×
UNCOV
2005
                .expect("FATAL: failed to query sortition DB for stacks tip");
×
UNCOV
2006
        let canonical_stacks_tip =
×
UNCOV
2007
            StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh);
×
UNCOV
2008
        let canonical_stacks_snapshot =
×
UNCOV
2009
            SortitionDB::get_block_snapshot_consensus(self.sortdb.conn(), &canonical_stacks_tip_ch)
×
UNCOV
2010
                .expect("FATAL: failed to query sortiiton DB for epoch")
×
UNCOV
2011
                .expect("FATAL: no sortition for canonical stacks tip");
×
2012

UNCOV
2013
        match tenure_extend_time.reason() {
×
2014
            TenureExtendReason::BadSortitionWinner | TenureExtendReason::EmptySortition => {
2015
                // Before we try to extend, check if we need to issue a BlockFound
UNCOV
2016
                let Ok(last_winning_snapshot) =
×
UNCOV
2017
                    Self::get_last_winning_snapshot(&self.sortdb, &burn_tip).inspect_err(|e| {
×
2018
                        warn!("Failed to load last winning snapshot: {e:?}");
×
2019
                    })
×
2020
                else {
2021
                    // this should be unreachable, but don't tempt fate.
2022
                    info!("No prior snapshots have a winning sortition. Will not try to mine.");
×
2023
                    self.tenure_extend_time = None;
×
2024
                    return;
×
2025
                };
UNCOV
2026
                let won_last_winning_snapshot =
×
UNCOV
2027
                    last_winning_snapshot.miner_pk_hash.as_ref() == Some(&mining_pkh);
×
UNCOV
2028
                if won_last_winning_snapshot
×
UNCOV
2029
                    && Self::need_block_found(&canonical_stacks_snapshot, &last_winning_snapshot)
×
2030
                {
UNCOV
2031
                    info!("Will not tenure extend yet -- need to issue a BlockFound first");
×
2032
                    // We may manage to extend later, so don't set the timer to None.
UNCOV
2033
                    return;
×
UNCOV
2034
                }
×
2035
            }
UNCOV
2036
            TenureExtendReason::UnresponsiveWinner => {}
×
2037
        }
2038

UNCOV
2039
        let won_ongoing_tenure_sortition =
×
UNCOV
2040
            canonical_stacks_snapshot.miner_pk_hash.as_ref() == Some(&mining_pkh);
×
UNCOV
2041
        if !won_ongoing_tenure_sortition {
×
UNCOV
2042
            debug!("Will not tenure extend. Did not win ongoing tenure sortition";
×
2043
                "burn_chain_sortition_tip_ch" => %burn_tip.consensus_hash,
2044
                "canonical_stacks_tip_ch" => %canonical_stacks_tip_ch,
2045
                "burn_chain_sortition_tip_mining_pk" => ?burn_tip.miner_pk_hash,
2046
                "mining_pk" => %mining_pkh
2047
            );
UNCOV
2048
            self.tenure_extend_time = None;
×
UNCOV
2049
            return;
×
UNCOV
2050
        }
×
2051
        // 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.
2052
        // 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.
UNCOV
2053
        if let Err(e) = self.stop_tenure() {
×
2054
            error!("Relayer: Failed to stop tenure: {e:?}");
×
2055
            return;
×
UNCOV
2056
        }
×
UNCOV
2057
        let reason = MinerReason::Extended {
×
UNCOV
2058
            burn_view_consensus_hash: burn_tip.consensus_hash.clone(),
×
UNCOV
2059
        };
×
UNCOV
2060
        debug!("Relayer: successfully stopped tenure; will try to continue.");
×
UNCOV
2061
        if let Err(e) = self.start_new_tenure(
×
UNCOV
2062
            canonical_stacks_tip.clone(),
×
UNCOV
2063
            canonical_stacks_snapshot.clone(),
×
UNCOV
2064
            burn_tip.clone(),
×
UNCOV
2065
            reason.clone(),
×
UNCOV
2066
            &burn_tip.consensus_hash,
×
UNCOV
2067
        ) {
×
2068
            error!("Relayer: Failed to start new tenure: {e:?}");
×
2069
        } else {
UNCOV
2070
            debug!("Relayer: successfully started new tenure.";
×
2071
                   "parent_tenure_start" => %canonical_stacks_tip,
2072
                   "burn_tip" => %burn_tip.consensus_hash,
2073
                   "burn_view_snapshot" => %burn_tip.consensus_hash,
2074
                   "block_election_snapshot" => %canonical_stacks_snapshot.consensus_hash,
2075
                   "reason" => %reason);
UNCOV
2076
            self.tenure_extend_time = None;
×
2077
        }
UNCOV
2078
    }
×
2079

2080
    /// Main loop of the relayer.
2081
    /// Runs in a separate thread.
2082
    /// Continuously receives from `relay_rcv`.
2083
    /// Wakes up once per second to see if we need to continue mining an ongoing tenure.
UNCOV
2084
    pub fn main(mut self, relay_rcv: Receiver<RelayerDirective>) {
×
UNCOV
2085
        debug!("relayer thread ID is {:?}", std::thread::current().id());
×
2086

UNCOV
2087
        self.next_initiative =
×
UNCOV
2088
            Instant::now() + Duration::from_millis(self.config.node.next_initiative_delay);
×
2089

2090
        // how often we perform a loop pass below
UNCOV
2091
        let poll_frequency_ms = 1_000;
×
2092

UNCOV
2093
        while self.globals.keep_running() {
×
UNCOV
2094
            self.check_tenure_timers();
×
UNCOV
2095
            let raised_initiative = self.globals.take_initiative();
×
UNCOV
2096
            let timed_out = Instant::now() >= self.next_initiative;
×
UNCOV
2097
            let initiative_directive = if raised_initiative.is_some() || timed_out {
×
UNCOV
2098
                self.next_initiative =
×
UNCOV
2099
                    Instant::now() + Duration::from_millis(self.config.node.next_initiative_delay);
×
UNCOV
2100
                self.initiative()
×
UNCOV
2101
                    .inspect_err(|e| {
×
2102
                        error!("Error while getting directive from initiative()"; "err" => ?e);
×
2103
                    })
×
UNCOV
2104
                    .ok()
×
UNCOV
2105
                    .flatten()
×
2106
            } else {
UNCOV
2107
                None
×
2108
            };
2109

UNCOV
2110
            let directive_opt = initiative_directive.or_else(|| {
×
2111
                // do a time-bound recv on the relayer channel so that we can hit the `initiative()` invocation
2112
                //  and keep_running() checks on each loop iteration
UNCOV
2113
                match relay_rcv.recv_timeout(Duration::from_millis(poll_frequency_ms)) {
×
UNCOV
2114
                    Ok(directive) => {
×
2115
                        // only do this once, so we can call .initiative() again
UNCOV
2116
                        Some(directive)
×
2117
                    }
UNCOV
2118
                    Err(RecvTimeoutError::Timeout) => None,
×
2119
                    Err(RecvTimeoutError::Disconnected) => {
2120
                        warn!("Relayer receive channel disconnected. Exiting relayer thread");
×
2121
                        Some(RelayerDirective::Exit)
×
2122
                    }
2123
                }
UNCOV
2124
            });
×
2125

UNCOV
2126
            if let Some(directive) = directive_opt {
×
UNCOV
2127
                debug!("Relayer: main loop directive";
×
2128
                       "directive" => %directive,
2129
                       "raised_initiative" => ?raised_initiative,
2130
                       "timed_out" => %timed_out);
2131

UNCOV
2132
                if !self.handle_directive(directive) {
×
UNCOV
2133
                    break;
×
UNCOV
2134
                }
×
UNCOV
2135
            }
×
2136
        }
2137

2138
        // kill miner if it's running
UNCOV
2139
        signal_mining_blocked(self.globals.get_miner_status());
×
2140

2141
        // set termination flag so other threads die
UNCOV
2142
        self.globals.signal_stop();
×
2143

UNCOV
2144
        debug!("Relayer exit!");
×
UNCOV
2145
    }
×
2146

2147
    /// Try loading up a saved VRF key
2148
    pub(crate) fn load_saved_vrf_key(path: &str, pubkey_hash: &Hash160) -> Option<RegisteredKey> {
5✔
2149
        let mut f = match fs::File::open(path) {
5✔
2150
            Ok(f) => f,
4✔
2151
            Err(e) => {
1✔
2152
                warn!("Could not open {path}: {e:?}");
1✔
2153
                return None;
1✔
2154
            }
2155
        };
2156
        let mut registered_key_bytes = vec![];
4✔
2157
        if let Err(e) = f.read_to_end(&mut registered_key_bytes) {
4✔
2158
            warn!("Failed to read registered key bytes from {path}: {e:?}");
×
2159
            return None;
×
2160
        }
4✔
2161

2162
        let Ok(registered_key) = serde_json::from_slice::<RegisteredKey>(&registered_key_bytes)
4✔
2163
        else {
2164
            warn!("Did not load registered key from {path}: could not decode JSON");
2✔
2165
            return None;
2✔
2166
        };
2167

2168
        // Check that the loaded key's memo matches the current miner's key
2169
        if registered_key.memo != pubkey_hash.as_ref() {
2✔
2170
            warn!("Loaded VRF key does not match mining key");
1✔
2171
            return None;
1✔
2172
        }
1✔
2173

2174
        info!("Loaded registered key from {path}");
1✔
2175
        Some(registered_key)
1✔
2176
    }
5✔
2177

2178
    /// Top-level dispatcher
UNCOV
2179
    pub fn handle_directive(&mut self, directive: RelayerDirective) -> bool {
×
UNCOV
2180
        debug!("Relayer: handling directive"; "directive" => %directive);
×
UNCOV
2181
        let continue_running = match directive {
×
UNCOV
2182
            RelayerDirective::HandleNetResult(net_result) => {
×
UNCOV
2183
                self.process_network_result(net_result);
×
UNCOV
2184
                true
×
2185
            }
2186
            // RegisterKey directives mean that the relayer should try to register a new VRF key.
2187
            // These are triggered by the relayer waking up without an active VRF key.
UNCOV
2188
            RelayerDirective::RegisterKey(last_burn_block) => {
×
UNCOV
2189
                if !self.is_miner {
×
2190
                    return true;
×
UNCOV
2191
                }
×
UNCOV
2192
                if self.globals.in_initial_block_download() {
×
2193
                    info!("In initial block download, will not submit VRF registration");
×
2194
                    return true;
×
UNCOV
2195
                }
×
UNCOV
2196
                let mut saved_key_opt = None;
×
UNCOV
2197
                if let Some(path) = self.config.miner.activated_vrf_key_path.as_ref() {
×
UNCOV
2198
                    saved_key_opt =
×
UNCOV
2199
                        Self::load_saved_vrf_key(path, &self.keychain.get_nakamoto_pkh());
×
UNCOV
2200
                }
×
UNCOV
2201
                if let Some(saved_key) = saved_key_opt {
×
UNCOV
2202
                    debug!("Relayer: resuming VRF key");
×
UNCOV
2203
                    self.globals.resume_leader_key(saved_key);
×
2204
                } else {
UNCOV
2205
                    self.rotate_vrf_and_register(&last_burn_block);
×
UNCOV
2206
                    debug!("Relayer: directive Registered VRF key");
×
2207
                }
UNCOV
2208
                self.globals.counters.bump_blocks_processed();
×
UNCOV
2209
                true
×
2210
            }
2211
            // ProcessedBurnBlock directives correspond to a new sortition perhaps occurring.
2212
            //  relayer should invoke `handle_sortition` to determine if they won the sortition,
2213
            //  and to start their miner, or stop their miner if an active tenure is now ending
UNCOV
2214
            RelayerDirective::ProcessedBurnBlock(consensus_hash, burn_hash, block_header_hash) => {
×
UNCOV
2215
                if !self.is_miner {
×
2216
                    return true;
×
UNCOV
2217
                }
×
UNCOV
2218
                if self.globals.in_initial_block_download() {
×
2219
                    debug!("In initial block download, will not check sortition for miner");
×
2220
                    return true;
×
UNCOV
2221
                }
×
UNCOV
2222
                self.handle_sortition(
×
UNCOV
2223
                    consensus_hash,
×
UNCOV
2224
                    burn_hash,
×
UNCOV
2225
                    StacksBlockId(block_header_hash.0),
×
2226
                )
2227
            }
2228
            // These are triggered by the relayer waking up, seeing a new consensus hash *or* a new first tenure block
2229
            RelayerDirective::IssueBlockCommit(..) => {
UNCOV
2230
                if !self.is_miner {
×
2231
                    return true;
×
UNCOV
2232
                }
×
UNCOV
2233
                if self.globals.in_initial_block_download() {
×
2234
                    debug!("In initial block download, will not issue block commit");
×
2235
                    return true;
×
UNCOV
2236
                }
×
UNCOV
2237
                if let Err(e) = self.issue_block_commit() {
×
UNCOV
2238
                    warn!("Relayer failed to issue block commit"; "err" => ?e);
×
UNCOV
2239
                }
×
UNCOV
2240
                true
×
2241
            }
2242
            RelayerDirective::Exit => false,
×
2243
        };
UNCOV
2244
        debug!("Relayer: handled directive"; "continue_running" => continue_running);
×
UNCOV
2245
        continue_running
×
UNCOV
2246
    }
×
2247

2248
    /// Reload config.burnchain to see if burn_fee_cap has changed.
2249
    /// If it has, update the miner spend amount and return true.
UNCOV
2250
    pub fn check_burnchain_config_changed(&self) -> (bool, BurnchainConfig) {
×
UNCOV
2251
        let burnchain_config = self.config.get_burnchain_config();
×
UNCOV
2252
        let last_burnchain_config_opt = self.globals.get_last_burnchain_config();
×
UNCOV
2253
        let burnchain_config_changed =
×
UNCOV
2254
            if let Some(last_burnchain_config) = last_burnchain_config_opt {
×
UNCOV
2255
                last_burnchain_config != burnchain_config
×
2256
            } else {
UNCOV
2257
                false
×
2258
            };
2259

UNCOV
2260
        self.globals
×
UNCOV
2261
            .set_last_miner_spend_amount(burnchain_config.burn_fee_cap);
×
UNCOV
2262
        self.globals
×
UNCOV
2263
            .set_last_burnchain_config(burnchain_config.clone());
×
2264

UNCOV
2265
        set_mining_spend_amount(
×
UNCOV
2266
            self.globals.get_miner_status(),
×
UNCOV
2267
            burnchain_config.burn_fee_cap,
×
2268
        );
2269

UNCOV
2270
        (burnchain_config_changed, burnchain_config)
×
UNCOV
2271
    }
×
2272

UNCOV
2273
    pub fn check_miner_config_changed(&self) -> bool {
×
UNCOV
2274
        let miner_config = self.config.get_miner_config();
×
UNCOV
2275
        let last_miner_config_opt = self.globals.get_last_miner_config();
×
UNCOV
2276
        let miner_config_changed = if let Some(last_miner_config) = last_miner_config_opt {
×
UNCOV
2277
            last_miner_config != miner_config
×
2278
        } else {
UNCOV
2279
            false
×
2280
        };
2281

UNCOV
2282
        self.globals.set_last_miner_config(miner_config);
×
2283

UNCOV
2284
        miner_config_changed
×
UNCOV
2285
    }
×
2286
}
2287

2288
#[cfg(test)]
2289
pub mod test {
2290
    use std::fs::File;
2291
    use std::io::Write;
2292
    use std::path::Path;
2293
    use std::time::Duration;
2294
    use std::u64;
2295

2296
    use rand::{thread_rng, Rng};
2297
    use stacks::burnchains::Txid;
2298
    use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash, OpsHash, SortitionHash};
2299
    use stacks::types::chainstate::{BlockHeaderHash, BurnchainHeaderHash, SortitionId, TrieHash};
2300
    use stacks::util::hash::Hash160;
2301
    use stacks::util::secp256k1::Secp256k1PublicKey;
2302
    use stacks::util::vrf::VRFPublicKey;
2303

2304
    use super::{BurnBlockCommitTimer, RelayerThread};
2305
    use crate::nakamoto_node::save_activated_vrf_key;
2306
    use crate::run_loop::RegisteredKey;
2307
    use crate::Keychain;
2308

2309
    #[test]
2310
    fn load_nonexistent_vrf_key() {
1✔
2311
        let keychain = Keychain::default(vec![0u8; 32]);
1✔
2312
        let pk = Secp256k1PublicKey::from_private(keychain.get_nakamoto_sk());
1✔
2313
        let pubkey_hash = Hash160::from_node_public_key(&pk);
1✔
2314

2315
        let path = "/tmp/does_not_exist.json";
1✔
2316
        _ = std::fs::remove_file(path);
1✔
2317

2318
        let res = RelayerThread::load_saved_vrf_key(path, &pubkey_hash);
1✔
2319
        assert!(res.is_none());
1✔
2320
    }
1✔
2321

2322
    #[test]
2323
    fn load_empty_vrf_key() {
1✔
2324
        let keychain = Keychain::default(vec![0u8; 32]);
1✔
2325
        let pk = Secp256k1PublicKey::from_private(keychain.get_nakamoto_sk());
1✔
2326
        let pubkey_hash = Hash160::from_node_public_key(&pk);
1✔
2327

2328
        let path = "/tmp/empty.json";
1✔
2329
        File::create(path).expect("Failed to create test file");
1✔
2330
        assert!(Path::new(path).exists());
1✔
2331

2332
        let res = RelayerThread::load_saved_vrf_key(path, &pubkey_hash);
1✔
2333
        assert!(res.is_none());
1✔
2334

2335
        std::fs::remove_file(path).expect("Failed to delete test file");
1✔
2336
    }
1✔
2337

2338
    #[test]
2339
    fn load_bad_vrf_key() {
1✔
2340
        let keychain = Keychain::default(vec![0u8; 32]);
1✔
2341
        let pk = Secp256k1PublicKey::from_private(keychain.get_nakamoto_sk());
1✔
2342
        let pubkey_hash = Hash160::from_node_public_key(&pk);
1✔
2343

2344
        let path = "/tmp/invalid_saved_key.json";
1✔
2345
        let json_content = r#"{ "hello": "world" }"#;
1✔
2346

2347
        // Write the JSON content to the file
2348
        let mut file = File::create(path).expect("Failed to create test file");
1✔
2349
        file.write_all(json_content.as_bytes())
1✔
2350
            .expect("Failed to write to test file");
1✔
2351
        assert!(Path::new(path).exists());
1✔
2352

2353
        let res = RelayerThread::load_saved_vrf_key(path, &pubkey_hash);
1✔
2354
        assert!(res.is_none());
1✔
2355

2356
        std::fs::remove_file(path).expect("Failed to delete test file");
1✔
2357
    }
1✔
2358

2359
    #[test]
2360
    fn save_load_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
        let key = RegisteredKey {
1✔
2365
            target_block_height: 101,
1✔
2366
            block_height: 102,
1✔
2367
            op_vtxindex: 1,
1✔
2368
            vrf_public_key: VRFPublicKey::from_hex(
1✔
2369
                "1da75863a7e1ef86f0f550d92b1f77dc60af23694b884b2816b703137ff94e71",
1✔
2370
            )
1✔
2371
            .unwrap(),
1✔
2372
            memo: pubkey_hash.as_ref().to_vec(),
1✔
2373
        };
1✔
2374
        let path = "/tmp/vrf_key.json";
1✔
2375
        save_activated_vrf_key(path, &key);
1✔
2376

2377
        let res = RelayerThread::load_saved_vrf_key(path, &pubkey_hash);
1✔
2378
        assert!(res.is_some());
1✔
2379

2380
        std::fs::remove_file(path).expect("Failed to delete test file");
1✔
2381
    }
1✔
2382

2383
    #[test]
2384
    fn invalid_saved_memo() {
1✔
2385
        let keychain = Keychain::default(vec![0u8; 32]);
1✔
2386
        let pk = Secp256k1PublicKey::from_private(keychain.get_nakamoto_sk());
1✔
2387
        let pubkey_hash = Hash160::from_node_public_key(&pk);
1✔
2388
        let key = RegisteredKey {
1✔
2389
            target_block_height: 101,
1✔
2390
            block_height: 102,
1✔
2391
            op_vtxindex: 1,
1✔
2392
            vrf_public_key: VRFPublicKey::from_hex(
1✔
2393
                "1da75863a7e1ef86f0f550d92b1f77dc60af23694b884b2816b703137ff94e71",
1✔
2394
            )
1✔
2395
            .unwrap(),
1✔
2396
            memo: pubkey_hash.as_ref().to_vec(),
1✔
2397
        };
1✔
2398
        let path = "/tmp/vrf_key.json";
1✔
2399
        save_activated_vrf_key(path, &key);
1✔
2400

2401
        let keychain = Keychain::default(vec![1u8; 32]);
1✔
2402
        let pk = Secp256k1PublicKey::from_private(keychain.get_nakamoto_sk());
1✔
2403
        let pubkey_hash = Hash160::from_node_public_key(&pk);
1✔
2404

2405
        let res = RelayerThread::load_saved_vrf_key(path, &pubkey_hash);
1✔
2406
        assert!(res.is_none());
1✔
2407

2408
        std::fs::remove_file(path).expect("Failed to delete test file");
1✔
2409
    }
1✔
2410

2411
    #[test]
2412
    fn check_need_block_found() {
1✔
2413
        let consensus_hash_byte = thread_rng().gen();
1✔
2414
        let canonical_stacks_snapshot = BlockSnapshot {
1✔
2415
            block_height: thread_rng().gen::<u64>().wrapping_add(1), // Add one to ensure we can always decrease by 1 without underflowing.
1✔
2416
            burn_header_timestamp: thread_rng().gen(),
1✔
2417
            burn_header_hash: BurnchainHeaderHash([thread_rng().gen(); 32]),
1✔
2418
            consensus_hash: ConsensusHash([consensus_hash_byte; 20]),
1✔
2419
            parent_burn_header_hash: BurnchainHeaderHash([thread_rng().gen(); 32]),
1✔
2420
            ops_hash: OpsHash([thread_rng().gen(); 32]),
1✔
2421
            total_burn: thread_rng().gen(),
1✔
2422
            sortition: true,
1✔
2423
            sortition_hash: SortitionHash([thread_rng().gen(); 32]),
1✔
2424
            winning_block_txid: Txid([thread_rng().gen(); 32]),
1✔
2425
            winning_stacks_block_hash: BlockHeaderHash([thread_rng().gen(); 32]),
1✔
2426
            index_root: TrieHash([thread_rng().gen(); 32]),
1✔
2427
            num_sortitions: thread_rng().gen(),
1✔
2428
            stacks_block_accepted: true,
1✔
2429
            stacks_block_height: thread_rng().gen(),
1✔
2430
            arrival_index: thread_rng().gen(),
1✔
2431
            canonical_stacks_tip_consensus_hash: ConsensusHash([thread_rng().gen(); 20]),
1✔
2432
            canonical_stacks_tip_hash: BlockHeaderHash([thread_rng().gen(); 32]),
1✔
2433
            canonical_stacks_tip_height: thread_rng().gen(),
1✔
2434
            sortition_id: SortitionId([thread_rng().gen(); 32]),
1✔
2435
            parent_sortition_id: SortitionId([thread_rng().gen(); 32]),
1✔
2436
            pox_valid: true,
1✔
2437
            accumulated_coinbase_ustx: thread_rng().gen::<u64>() as u128,
1✔
2438
            miner_pk_hash: Some(Hash160([thread_rng().gen(); 20])),
1✔
2439
        };
1✔
2440

2441
        // The consensus_hashes are the same, and the block heights are the same. Therefore, don't need a block found.
2442
        let last_winning_block_snapshot = canonical_stacks_snapshot.clone();
1✔
2443
        assert!(!RelayerThread::need_block_found(
1✔
2444
            &canonical_stacks_snapshot,
1✔
2445
            &last_winning_block_snapshot
1✔
2446
        ));
1✔
2447

2448
        // The block height of the canonical tip is higher than the last winning snapshot. We already issued a block found.
2449
        let mut canonical_stacks_snapshot_is_higher_than_last_winning_snapshot =
1✔
2450
            last_winning_block_snapshot.clone();
1✔
2451
        canonical_stacks_snapshot_is_higher_than_last_winning_snapshot.block_height =
1✔
2452
            canonical_stacks_snapshot.block_height.saturating_sub(1);
1✔
2453
        assert!(!RelayerThread::need_block_found(
1✔
2454
            &canonical_stacks_snapshot,
1✔
2455
            &canonical_stacks_snapshot_is_higher_than_last_winning_snapshot
1✔
2456
        ));
1✔
2457

2458
        // The block height is the same, but we have different consensus hashes. We need to issue a block found.
2459
        let mut tip_consensus_hash_mismatch = last_winning_block_snapshot.clone();
1✔
2460
        tip_consensus_hash_mismatch.consensus_hash =
1✔
2461
            ConsensusHash([consensus_hash_byte.wrapping_add(1); 20]);
1✔
2462
        assert!(RelayerThread::need_block_found(
1✔
2463
            &canonical_stacks_snapshot,
1✔
2464
            &tip_consensus_hash_mismatch
1✔
2465
        ));
2466

2467
        // The block height is the same, but we have different consensus hashes. We need to issue a block found.
2468
        let mut tip_consensus_hash_mismatch = last_winning_block_snapshot.clone();
1✔
2469
        tip_consensus_hash_mismatch.consensus_hash =
1✔
2470
            ConsensusHash([consensus_hash_byte.wrapping_add(1); 20]);
1✔
2471
        assert!(RelayerThread::need_block_found(
1✔
2472
            &canonical_stacks_snapshot,
1✔
2473
            &tip_consensus_hash_mismatch
1✔
2474
        ));
2475

2476
        // The block height of the canonical tip is lower than the last winning snapshot blockheight. We need to issue a block found.
2477
        let mut canonical_stacks_snapshot_is_lower_than_last_winning_snapshot =
1✔
2478
            last_winning_block_snapshot.clone();
1✔
2479
        canonical_stacks_snapshot_is_lower_than_last_winning_snapshot.block_height =
1✔
2480
            canonical_stacks_snapshot.block_height.saturating_add(1);
1✔
2481
        assert!(RelayerThread::need_block_found(
1✔
2482
            &canonical_stacks_snapshot,
1✔
2483
            &canonical_stacks_snapshot_is_lower_than_last_winning_snapshot
1✔
2484
        ));
2485
    }
1✔
2486

2487
    #[test]
2488
    fn burn_block_commit_timer_units() {
1✔
2489
        let mut burn_block_timer = BurnBlockCommitTimer::NotSet;
1✔
2490
        assert_eq!(burn_block_timer.elapsed_secs(), 0);
1✔
2491

2492
        let ch_0 = ConsensusHash([0; 20]);
1✔
2493
        let ch_1 = ConsensusHash([1; 20]);
1✔
2494
        let ch_2 = ConsensusHash([2; 20]);
1✔
2495

2496
        assert!(!burn_block_timer.is_ready(&ch_0, &Duration::from_secs(1)));
1✔
2497
        let BurnBlockCommitTimer::Set { burn_tip, .. } = &burn_block_timer else {
1✔
2498
            panic!("The burn block timer should be set");
×
2499
        };
2500
        assert_eq!(burn_tip, &ch_0);
1✔
2501

2502
        std::thread::sleep(Duration::from_secs(1));
1✔
2503

2504
        assert!(burn_block_timer.is_ready(&ch_0, &Duration::from_secs(0)));
1✔
2505
        let BurnBlockCommitTimer::Set { burn_tip, .. } = &burn_block_timer else {
1✔
2506
            panic!("The burn block timer should be set");
×
2507
        };
2508
        assert_eq!(burn_tip, &ch_0);
1✔
2509

2510
        assert!(!burn_block_timer.is_ready(&ch_1, &Duration::from_secs(0)));
1✔
2511
        let BurnBlockCommitTimer::Set { burn_tip, .. } = &burn_block_timer else {
1✔
2512
            panic!("The burn block timer should be set");
×
2513
        };
2514
        assert_eq!(burn_tip, &ch_1);
1✔
2515

2516
        assert!(!burn_block_timer.is_ready(&ch_1, &Duration::from_secs(u64::MAX)));
1✔
2517
        let BurnBlockCommitTimer::Set { burn_tip, .. } = &burn_block_timer else {
1✔
2518
            panic!("The burn block timer should be set");
×
2519
        };
2520
        assert_eq!(burn_tip, &ch_1);
1✔
2521

2522
        std::thread::sleep(Duration::from_secs(1));
1✔
2523
        assert!(!burn_block_timer.is_ready(&ch_2, &Duration::from_secs(0)));
1✔
2524
        let BurnBlockCommitTimer::Set { burn_tip, .. } = &burn_block_timer else {
1✔
2525
            panic!("The burn block timer should be set");
×
2526
        };
2527
        assert_eq!(burn_tip, &ch_2);
1✔
2528
    }
1✔
2529
}
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