@@ -50,6 +50,7 @@ use stacks::net::db::LocalPeer;
50
50
use stacks:: net:: p2p:: NetworkHandle ;
51
51
use stacks:: net:: relay:: Relayer ;
52
52
use stacks:: net:: NetworkResult ;
53
+ use stacks:: util_lib:: db:: Error as DbError ;
53
54
use stacks_common:: types:: chainstate:: {
54
55
BlockHeaderHash , BurnchainHeaderHash , StacksBlockId , StacksPublicKey , VRFSeed ,
55
56
} ;
@@ -82,6 +83,11 @@ pub static TEST_MINER_THREAD_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(Tes
82
83
pub static TEST_MINER_THREAD_START_STALL : LazyLock < TestFlag < bool > > =
83
84
LazyLock :: new ( TestFlag :: default) ;
84
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
+
85
91
/// Command types for the Nakamoto relayer thread, issued to it by other threads
86
92
#[ allow( clippy:: large_enum_variant) ]
87
93
pub enum RelayerDirective {
@@ -616,7 +622,10 @@ impl RelayerThread {
616
622
/// Specifically:
617
623
///
618
624
/// If we won the given sortition `sn`, then we can start mining immediately with a `BlockFound`
619
- /// tenure-change. Otherwise, if we won the tenure which started the ongoing Stacks tenure
625
+ /// tenure-change. The exception is if we won the sortition, but the sortition's winning commit
626
+ /// does not commit to the ongoing tenure. In this case, we instead extend the current tenure.
627
+ ///
628
+ /// Otherwise, if we did not win `sn`, if we won the tenure which started the ongoing Stacks tenure
620
629
/// (i.e. we're the active miner), then we _may_ start mining after a timeout _if_ the winning
621
630
/// miner (not us) fails to submit a `BlockFound` tenure-change block for `sn`.
622
631
fn choose_directive_sortition_with_winner (
@@ -626,6 +635,57 @@ impl RelayerThread {
626
635
committed_index_hash : StacksBlockId ,
627
636
) -> MinerDirective {
628
637
let won_sortition = sn. miner_pk_hash . as_ref ( ) == Some ( mining_pkh) ;
638
+
639
+ let ( canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
640
+ SortitionDB :: get_canonical_stacks_chain_tip_hash ( self . sortdb . conn ( ) )
641
+ . expect ( "FATAL: failed to query sortition DB for stacks tip" ) ;
642
+ let canonical_stacks_snapshot =
643
+ SortitionDB :: get_block_snapshot_consensus ( self . sortdb . conn ( ) , & canonical_stacks_tip_ch)
644
+ . expect ( "FATAL: failed to query sortiiton DB for epoch" )
645
+ . expect ( "FATAL: no sortition for canonical stacks tip" ) ;
646
+
647
+ // If we won the sortition, ensure that the sortition's winning commit actually commits to
648
+ // the ongoing tenure. If it does not (i.e. commit is "stale" and points to N-1 when we are
649
+ // currently in N), and if we are also the ongoing tenure's miner, then we must not attempt
650
+ // a tenure change (which would reorg our own signed blocks). Instead, we should immediately
651
+ // extend the tenure.
652
+ if won_sortition && !self . config . get_node_config ( false ) . mock_mining {
653
+ let canonical_stacks_tip =
654
+ StacksBlockId :: new ( & canonical_stacks_tip_ch, & canonical_stacks_tip_bh) ;
655
+
656
+ let commits_to_tip_tenure = Self :: sortition_commits_to_stacks_tip_tenure (
657
+ & mut self . chainstate ,
658
+ & canonical_stacks_tip,
659
+ & canonical_stacks_snapshot,
660
+ & sn,
661
+ ) . unwrap_or_else ( |e| {
662
+ warn ! (
663
+ "Relayer: Failed to determine if winning sortition commits to current tenure: {e:?}" ;
664
+ "sortition_ch" => %sn. consensus_hash,
665
+ "stacks_tip_ch" => %canonical_stacks_tip_ch
666
+ ) ;
667
+ false
668
+ } ) ;
669
+
670
+ if !commits_to_tip_tenure {
671
+ let won_ongoing_tenure_sortition =
672
+ canonical_stacks_snapshot. miner_pk_hash . as_ref ( ) == Some ( mining_pkh) ;
673
+
674
+ if won_ongoing_tenure_sortition {
675
+ info ! (
676
+ "Relayer: Won sortition, but commit does not target ongoing tenure. Will extend instead of starting a new tenure." ;
677
+ "winning_sortition" => %sn. consensus_hash,
678
+ "ongoing_tenure" => %canonical_stacks_snapshot. consensus_hash,
679
+ "commits_to_tip_tenure?" => commits_to_tip_tenure
680
+ ) ;
681
+ // Extend tenure to the new burn view instead of attempting BlockFound
682
+ return MinerDirective :: ContinueTenure {
683
+ new_burn_view : sn. consensus_hash ,
684
+ } ;
685
+ }
686
+ }
687
+ }
688
+
629
689
if won_sortition || self . config . get_node_config ( false ) . mock_mining {
630
690
// a sortition happenend, and we won
631
691
info ! ( "Won sortition; begin tenure." ;
@@ -643,13 +703,6 @@ impl RelayerThread {
643
703
"Relayer: did not win sortition {}, so stopping tenure" ,
644
704
& sn. sortition
645
705
) ;
646
- let ( canonical_stacks_tip_ch, _) =
647
- SortitionDB :: get_canonical_stacks_chain_tip_hash ( self . sortdb . conn ( ) )
648
- . expect ( "FATAL: failed to query sortition DB for stacks tip" ) ;
649
- let canonical_stacks_snapshot =
650
- SortitionDB :: get_block_snapshot_consensus ( self . sortdb . conn ( ) , & canonical_stacks_tip_ch)
651
- . expect ( "FATAL: failed to query sortiiton DB for epoch" )
652
- . expect ( "FATAL: no sortition for canonical stacks tip" ) ;
653
706
654
707
let won_ongoing_tenure_sortition =
655
708
canonical_stacks_snapshot. miner_pk_hash . as_ref ( ) == Some ( mining_pkh) ;
@@ -1633,6 +1686,31 @@ impl RelayerThread {
1633
1686
false
1634
1687
}
1635
1688
1689
+ /// Get the canonical tip for the miner to commit to.
1690
+ /// This is provided as a separate function so that it can be overridden for testing.
1691
+ #[ cfg( not( test) ) ]
1692
+ fn fault_injection_get_tip_for_commit ( & self ) -> Option < ( ConsensusHash , BlockHeaderHash ) > {
1693
+ None
1694
+ }
1695
+
1696
+ #[ cfg( test) ]
1697
+ fn fault_injection_get_tip_for_commit ( & self ) -> Option < ( ConsensusHash , BlockHeaderHash ) > {
1698
+ TEST_MINER_COMMIT_TIP . get ( )
1699
+ }
1700
+
1701
+ fn get_commit_for_tip ( & mut self ) -> Result < ( ConsensusHash , BlockHeaderHash ) , DbError > {
1702
+ if let Some ( ( consensus_hash, block_header_hash) ) = self . fault_injection_get_tip_for_commit ( )
1703
+ {
1704
+ info ! ( "Relayer: using test tip for commit" ;
1705
+ "consensus_hash" => %consensus_hash,
1706
+ "block_header_hash" => %block_header_hash,
1707
+ ) ;
1708
+ Ok ( ( consensus_hash, block_header_hash) )
1709
+ } else {
1710
+ SortitionDB :: get_canonical_stacks_chain_tip_hash ( self . sortdb . conn ( ) )
1711
+ }
1712
+ }
1713
+
1636
1714
/// Generate and submit the next block-commit, and record it locally
1637
1715
fn issue_block_commit ( & mut self ) -> Result < ( ) , NakamotoNodeError > {
1638
1716
if self . fault_injection_skip_block_commit ( ) {
@@ -1641,10 +1719,7 @@ impl RelayerThread {
1641
1719
) ;
1642
1720
return Ok ( ( ) ) ;
1643
1721
}
1644
- let ( tip_block_ch, tip_block_bh) = SortitionDB :: get_canonical_stacks_chain_tip_hash (
1645
- self . sortdb . conn ( ) ,
1646
- )
1647
- . unwrap_or_else ( |e| {
1722
+ let ( tip_block_ch, tip_block_bh) = self . get_commit_for_tip ( ) . unwrap_or_else ( |e| {
1648
1723
panic ! ( "Failed to load canonical stacks tip: {e:?}" ) ;
1649
1724
} ) ;
1650
1725
let mut last_committed = self . make_block_commit ( & tip_block_ch, & tip_block_bh) ?;
0 commit comments