Skip to content

Commit a3cbcea

Browse files
authored
Merge branch 'develop' into chore/stacks-block-time
2 parents 61f9a62 + 93fd7b6 commit a3cbcea

File tree

5 files changed

+487
-137
lines changed

5 files changed

+487
-137
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
2727
- `block-time`
2828
- `to-ascii?`
2929
- Added `contract_cost_limit_percentage` to the miner config file — sets the percentage of a block’s execution cost at which, if a large non-boot contract call would cause a BlockTooBigError, the miner will stop adding further non-boot contract calls and only include STX transfers and boot contract calls for the remainder of the block.
30+
- Fixed a bug caused by a miner winning a sortition with a block commit that pointed to a previous tip, which would cause the miner to try and reorg itself. [#6481](https://github.com/stacks-network/stacks-core/issues/6481)
3031

3132
### Changed
3233

stacks-node/src/nakamoto_node/relayer.rs

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ use stacks::net::db::LocalPeer;
5050
use stacks::net::p2p::NetworkHandle;
5151
use stacks::net::relay::Relayer;
5252
use stacks::net::NetworkResult;
53+
use stacks::util_lib::db::Error as DbError;
5354
use stacks_common::types::chainstate::{
5455
BlockHeaderHash, BurnchainHeaderHash, StacksBlockId, StacksPublicKey, VRFSeed,
5556
};
@@ -82,6 +83,11 @@ pub static TEST_MINER_THREAD_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(Tes
8283
pub static TEST_MINER_THREAD_START_STALL: LazyLock<TestFlag<bool>> =
8384
LazyLock::new(TestFlag::default);
8485

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+
8591
/// Command types for the Nakamoto relayer thread, issued to it by other threads
8692
#[allow(clippy::large_enum_variant)]
8793
pub enum RelayerDirective {
@@ -616,7 +622,10 @@ impl RelayerThread {
616622
/// Specifically:
617623
///
618624
/// 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
620629
/// (i.e. we're the active miner), then we _may_ start mining after a timeout _if_ the winning
621630
/// miner (not us) fails to submit a `BlockFound` tenure-change block for `sn`.
622631
fn choose_directive_sortition_with_winner(
@@ -626,6 +635,57 @@ impl RelayerThread {
626635
committed_index_hash: StacksBlockId,
627636
) -> MinerDirective {
628637
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+
629689
if won_sortition || self.config.get_node_config(false).mock_mining {
630690
// a sortition happenend, and we won
631691
info!("Won sortition; begin tenure.";
@@ -643,13 +703,6 @@ impl RelayerThread {
643703
"Relayer: did not win sortition {}, so stopping tenure",
644704
&sn.sortition
645705
);
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");
653706

654707
let won_ongoing_tenure_sortition =
655708
canonical_stacks_snapshot.miner_pk_hash.as_ref() == Some(mining_pkh);
@@ -1633,6 +1686,31 @@ impl RelayerThread {
16331686
false
16341687
}
16351688

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+
16361714
/// Generate and submit the next block-commit, and record it locally
16371715
fn issue_block_commit(&mut self) -> Result<(), NakamotoNodeError> {
16381716
if self.fault_injection_skip_block_commit() {
@@ -1641,10 +1719,7 @@ impl RelayerThread {
16411719
);
16421720
return Ok(());
16431721
}
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| {
16481723
panic!("Failed to load canonical stacks tip: {e:?}");
16491724
});
16501725
let mut last_committed = self.make_block_commit(&tip_block_ch, &tip_block_bh)?;

stacks-node/src/tests/nakamoto_integrations.rs

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5300,8 +5300,8 @@ fn burn_ops_integration_test() {
53005300
/// Miner B starts its tenure, Miner B produces a Stacks block b_0, but miner C submits its block commit before b_0 is broadcasted.
53015301
/// Bitcoin block C, containing Miner C's block commit, is mined BEFORE miner C has a chance to update their block commit with b_0's information.
53025302
/// This test asserts:
5303-
/// * tenure C ignores b_0, and correctly builds off of block a_x.
5304-
fn forked_tenure_is_ignored() {
5303+
/// * tenure C correctly extends b_0, building off of block B despite the commit being submitted before b_0 was broadcasted.
5304+
fn bad_commit_does_not_trigger_fork() {
53055305
if env::var("BITCOIND_TEST") != Ok("1".into()) {
53065306
return;
53075307
}
@@ -5493,27 +5493,46 @@ fn forked_tenure_is_ignored() {
54935493
.lock()
54945494
.expect("Mutex poisoned")
54955495
.get_stacks_blocks_processed();
5496-
let block_in_tenure = get_last_block_in_current_tenure(&sortdb, &chainstate).is_some();
5496+
// We don't expect a block in this tenure, because the miner should instead be building off
5497+
// of a previous tenure
5498+
let no_block_in_tenure = get_last_block_in_current_tenure(&sortdb, &chainstate).is_none();
54975499
Ok(commits_count > commits_before
54985500
&& blocks_count > blocks_before
54995501
&& blocks_processed > blocks_processed_before
5500-
&& block_in_tenure)
5502+
&& no_block_in_tenure)
55015503
})
5502-
.unwrap();
5504+
.unwrap_or_else(|_| {
5505+
let commits_count = commits_submitted.load(Ordering::SeqCst);
5506+
let blocks_count = mined_blocks.load(Ordering::SeqCst);
5507+
let blocks_processed = coord_channel
5508+
.lock()
5509+
.expect("Mutex poisoned")
5510+
.get_stacks_blocks_processed();
5511+
let no_block_in_tenure = get_last_block_in_current_tenure(&sortdb, &chainstate).is_none();
5512+
error!("Tenure C shouldn't have produced a block";
5513+
"commits_count" => commits_count,
5514+
"commits_before" => commits_before,
5515+
"blocks_count" => blocks_count,
5516+
"blocks_before" => blocks_before,
5517+
"blocks_processed" => blocks_processed,
5518+
"blocks_processed_before" => blocks_processed_before,
5519+
"no_block_in_tenure" => no_block_in_tenure,
5520+
);
5521+
panic!("Tenure C shouldn't have produced a block");
5522+
});
55035523

5504-
info!("Tenure C produced a block!");
5524+
info!("Tenure C did not produce a block");
55055525

5506-
let block_tenure_c = get_last_block_in_current_tenure(&sortdb, &chainstate).unwrap();
5526+
let block_tenure_c = get_last_block_in_current_tenure(&sortdb, &chainstate);
5527+
assert!(block_tenure_c.is_none());
55075528
let blocks = test_observer::get_mined_nakamoto_blocks();
55085529
let block_c = blocks.last().unwrap();
5509-
info!("Tenure C tip block: {}", &block_tenure_c.index_block_hash());
55105530
info!("Tenure C last block: {}", &block_c.block_id);
5511-
assert_ne!(block_tenure_b.block_id(), block_tenure_c.index_block_hash());
55125531

55135532
// Block C was built AFTER Block B was built, but BEFORE it was broadcasted (processed), so it should be built off of Block A
55145533
assert_eq!(
5515-
block_tenure_c.stacks_block_height,
5516-
block_tenure_a.stacks_block_height + 1
5534+
block_c.stacks_height,
5535+
block_tenure_b.header.chain_length + 1
55175536
);
55185537

55195538
// Now let's produce a second block for tenure C and ensure it builds off of block C.
@@ -5556,14 +5575,11 @@ fn forked_tenure_is_ignored() {
55565575

55575576
info!("Tenure C produced a second block!");
55585577

5559-
let block_2_tenure_c = get_last_block_in_current_tenure(&sortdb, &chainstate).unwrap();
5578+
let block_2_tenure_c = get_last_block_in_current_tenure(&sortdb, &chainstate);
5579+
assert!(block_2_tenure_c.is_none());
55605580
let blocks = test_observer::get_mined_nakamoto_blocks();
55615581
let block_2_c = blocks.last().unwrap();
55625582

5563-
info!(
5564-
"Tenure C tip block: {}",
5565-
&block_2_tenure_c.index_block_hash()
5566-
);
55675583
info!("Tenure C last block: {}", &block_2_c.block_id);
55685584

55695585
info!("Starting tenure D.");
@@ -5595,8 +5611,6 @@ fn forked_tenure_is_ignored() {
55955611
info!("Tenure D last block: {}", block_d.block_id);
55965612

55975613
assert_ne!(block_tenure_b.block_id(), block_tenure_a.index_block_hash());
5598-
assert_ne!(block_tenure_b.block_id(), block_tenure_c.index_block_hash());
5599-
assert_ne!(block_tenure_c, block_tenure_a);
56005614

56015615
// Block B was built atop block A
56025616
assert_eq!(
@@ -5608,39 +5622,29 @@ fn forked_tenure_is_ignored() {
56085622
block_tenure_a.index_block_hash().to_string()
56095623
);
56105624

5611-
// Block C was built AFTER Block B was built, but BEFORE it was broadcasted, so it should be built off of Block A
5625+
// Block C was built AFTER Block B was built, but BEFORE it was broadcasted, so it should be built off of Block B
56125626
assert_eq!(
5613-
block_tenure_c.stacks_block_height,
5614-
block_tenure_a.stacks_block_height + 1
5627+
block_c.stacks_height,
5628+
block_tenure_b.header.chain_length + 1
56155629
);
56165630
assert_eq!(
56175631
block_c.parent_block_id,
5618-
block_tenure_a.index_block_hash().to_string()
5632+
block_tenure_b.header.block_id().to_string()
56195633
);
56205634

5621-
assert_ne!(block_tenure_c, block_2_tenure_c);
5622-
assert_ne!(block_2_tenure_c, block_tenure_d);
5623-
assert_ne!(block_tenure_c, block_tenure_d);
5624-
56255635
// Second block of tenure C builds off of block C
56265636
assert_eq!(
5627-
block_2_tenure_c.stacks_block_height,
5628-
block_tenure_c.stacks_block_height + 1,
5629-
);
5630-
assert_eq!(
5631-
block_2_c.parent_block_id,
5632-
block_tenure_c.index_block_hash().to_string()
5637+
block_2_c.stacks_height,
5638+
block_tenure_b.header.chain_length + 2,
56335639
);
5640+
assert_eq!(block_2_c.parent_block_id, block_c.block_id);
56345641

56355642
// Tenure D builds off of the second block of tenure C
56365643
assert_eq!(
56375644
block_tenure_d.stacks_block_height,
5638-
block_2_tenure_c.stacks_block_height + 1,
5639-
);
5640-
assert_eq!(
5641-
block_d.parent_block_id,
5642-
block_2_tenure_c.index_block_hash().to_string()
5645+
block_2_c.stacks_height + 1,
56435646
);
5647+
assert_eq!(block_d.parent_block_id, block_2_c.block_id);
56445648

56455649
coord_channel
56465650
.lock()

0 commit comments

Comments
 (0)