Skip to content

Commit 14230bf

Browse files
authored
Merge pull request #6139 from kantai/feat/extend-bitcoin-reorg
Feat: handle burn reorgs during tenure extends
2 parents fe9c291 + cd41b5e commit 14230bf

File tree

12 files changed

+640
-187
lines changed

12 files changed

+640
-187
lines changed

stacks-signer/src/chainstate.rs

Lines changed: 74 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -389,9 +389,9 @@ impl SortitionsView {
389389
)?;
390390
} else {
391391
// check if the new block confirms the last block in the current tenure
392-
let confirms_latest_in_tenure =
393-
Self::confirms_latest_block_in_same_tenure(block, signer_db)
394-
.map_err(SignerChainstateError::from)?;
392+
let confirms_latest_in_tenure = self
393+
.confirms_latest_block_in_same_tenure(block, signer_db, client)
394+
.map_err(SignerChainstateError::from)?;
395395
if !confirms_latest_in_tenure {
396396
return Err(RejectReason::InvalidParentBlock);
397397
}
@@ -562,8 +562,7 @@ impl SortitionsView {
562562
Ok(true)
563563
}
564564

565-
/// Get the last block from the given tenure
566-
/// Returns the last locally accepted block if it is not timed out, otherwise it will return the last globally accepted block.
565+
/// Get the last locally signed block from the given tenure if it has not timed out
567566
pub fn get_tenure_last_block_info(
568567
consensus_hash: &ConsensusHash,
569568
signer_db: &SignerDb,
@@ -573,43 +572,45 @@ impl SortitionsView {
573572
let last_locally_accepted_block = signer_db
574573
.get_last_accepted_block(consensus_hash)
575574
.map_err(|e| ClientError::InvalidResponse(e.to_string()))?;
575+
let Some(local_info) = last_locally_accepted_block else {
576+
return Ok(None);
577+
};
576578

577-
if let Some(local_info) = last_locally_accepted_block {
578-
if let Some(signed_over_time) = local_info.signed_self {
579-
if signed_over_time.saturating_add(tenure_last_block_proposal_timeout.as_secs())
580-
> get_epoch_time_secs()
581-
{
582-
// The last locally accepted block is not timed out, return it
583-
return Ok(Some(local_info));
584-
}
585-
}
579+
let Some(signed_over_time) = local_info.signed_self else {
580+
return Ok(None);
581+
};
582+
583+
if signed_over_time.saturating_add(tenure_last_block_proposal_timeout.as_secs())
584+
> get_epoch_time_secs()
585+
{
586+
// The last locally accepted block is not timed out, return it
587+
Ok(Some(local_info))
588+
} else {
589+
// The last locally accepted block is timed out
590+
Ok(None)
586591
}
587-
// The last locally accepted block is timed out, get the last globally accepted block
588-
signer_db
589-
.get_last_globally_accepted_block(consensus_hash)
590-
.map_err(|e| ClientError::InvalidResponse(e.to_string()))
591592
}
592593

593-
/// Check if the tenure change block confirms the expected parent block
594-
/// (i.e., the last locally accepted block in the parent tenure, or if that block is timed out, the last globally accepted block in the parent tenure)
595-
/// It checks the local DB first, and if the block is not present in the local DB, it asks the
596-
/// Stacks node for the highest processed block header in the given tenure (and then caches it
597-
/// in the DB).
594+
/// Check whether or not `block` is higher than the highest block in `tenure_id`.
595+
/// returns `Ok(true)` if `block` is higher, `Ok(false)` if not.
598596
///
599-
/// The rationale here is that the signer DB can be out-of-sync with the node. For example,
600-
/// the signer may have been added to an already-running node.
601-
pub fn check_tenure_change_confirms_parent(
602-
tenure_change: &TenureChangePayload,
597+
/// If we can't look up `tenure_id`, assume `block` is higher.
598+
/// This assumption is safe because this proposal ultimately must be passed
599+
/// to the `stacks-node` for proposal processing: so, if we pass the block
600+
/// height check here, we are relying on the `stacks-node` proposal endpoint
601+
/// to do the validation on the chainstate data that it has.
602+
///
603+
/// This updates the activity timer for the miner of `block`.
604+
pub fn check_latest_block_in_tenure(
605+
tenure_id: &ConsensusHash,
603606
block: &NakamotoBlock,
604607
signer_db: &mut SignerDb,
605608
client: &StacksClient,
606609
tenure_last_block_proposal_timeout: Duration,
607610
reorg_attempts_activity_timeout: Duration,
608611
) -> Result<bool, ClientError> {
609-
// If the tenure change block confirms the expected parent block, it should confirm at least one more block than the last accepted block in the parent tenure.
610-
// NOTE: returns the locally accepted block if it is not timed out, otherwise it will return the last globally accepted block.
611612
let last_block_info = Self::get_tenure_last_block_info(
612-
&tenure_change.prev_tenure_consensus_hash,
613+
tenure_id,
613614
signer_db,
614615
tenure_last_block_proposal_timeout,
615616
)?;
@@ -636,7 +637,7 @@ impl SortitionsView {
636637
// to give the miner some extra buffer time to wait for its chain tip to advance
637638
// The miner may just be slow, so count this invalid block proposal towards valid miner activity.
638639
if let Err(e) = signer_db.update_last_activity_time(
639-
&tenure_change.tenure_consensus_hash,
640+
&block.header.consensus_hash,
640641
get_epoch_time_secs(),
641642
) {
642643
warn!("Failed to update last activity time: {e}");
@@ -646,16 +647,16 @@ impl SortitionsView {
646647
}
647648
}
648649

649-
let tip = match client.get_tenure_tip(&tenure_change.prev_tenure_consensus_hash) {
650+
let tip = match client.get_tenure_tip(tenure_id) {
650651
Ok(tip) => tip,
651652
Err(e) => {
652653
warn!(
653-
"Miner block proposal contains a tenure change, but failed to fetch the tenure tip for the parent tenure: {e:?}. Considering proposal invalid.";
654+
"Failed to fetch the tenure tip for the parent tenure: {e:?}. Assuming proposal is higher than the parent tenure for now.";
654655
"proposed_block_consensus_hash" => %block.header.consensus_hash,
655656
"signer_signature_hash" => %block.header.signer_signature_hash(),
656-
"parent_tenure" => %tenure_change.prev_tenure_consensus_hash,
657+
"parent_tenure" => %tenure_id,
657658
);
658-
return Ok(false);
659+
return Ok(true);
659660
}
660661
};
661662
if let Some(nakamoto_tip) = tip.as_stacks_nakamoto() {
@@ -673,19 +674,33 @@ impl SortitionsView {
673674
}
674675
}
675676
}
676-
let tip_height = tip.height();
677-
if block.header.chain_length > tip_height {
678-
Ok(true)
679-
} else {
680-
warn!(
681-
"Miner's block proposal does not confirm as many blocks as we expect";
682-
"proposed_block_consensus_hash" => %block.header.consensus_hash,
683-
"signer_signature_hash" => %block.header.signer_signature_hash(),
684-
"proposed_chain_length" => block.header.chain_length,
685-
"expected_at_least" => tip_height + 1,
686-
);
687-
Ok(false)
688-
}
677+
Ok(tip.height() < block.header.chain_length)
678+
}
679+
680+
/// Check if the tenure change block confirms the expected parent block
681+
/// (i.e., the last locally accepted block in the parent tenure, or if that block is timed out, the last globally accepted block in the parent tenure)
682+
/// It checks the local DB first, and if the block is not present in the local DB, it asks the
683+
/// Stacks node for the highest processed block header in the given tenure (and then caches it
684+
/// in the DB).
685+
///
686+
/// The rationale here is that the signer DB can be out-of-sync with the node. For example,
687+
/// the signer may have been added to an already-running node.
688+
pub fn check_tenure_change_confirms_parent(
689+
tenure_change: &TenureChangePayload,
690+
block: &NakamotoBlock,
691+
signer_db: &mut SignerDb,
692+
client: &StacksClient,
693+
tenure_last_block_proposal_timeout: Duration,
694+
reorg_attempts_activity_timeout: Duration,
695+
) -> Result<bool, ClientError> {
696+
Self::check_latest_block_in_tenure(
697+
&tenure_change.prev_tenure_consensus_hash,
698+
block,
699+
signer_db,
700+
client,
701+
tenure_last_block_proposal_timeout,
702+
reorg_attempts_activity_timeout,
703+
)
689704
}
690705

691706
/// in tenure changes, we need to check:
@@ -742,33 +757,19 @@ impl SortitionsView {
742757
}
743758

744759
fn confirms_latest_block_in_same_tenure(
760+
&self,
745761
block: &NakamotoBlock,
746-
signer_db: &SignerDb,
762+
signer_db: &mut SignerDb,
763+
client: &StacksClient,
747764
) -> Result<bool, ClientError> {
748-
let Some(last_known_block) = signer_db
749-
.get_last_accepted_block(&block.header.consensus_hash)
750-
.map_err(|e| ClientError::InvalidResponse(e.to_string()))?
751-
else {
752-
info!(
753-
"Have no accepted blocks in the tenure, assuming block confirmation is correct";
754-
"proposed_block_consensus_hash" => %block.header.consensus_hash,
755-
"signer_signature_hash" => %block.header.signer_signature_hash(),
756-
"proposed_block_height" => block.header.chain_length,
757-
);
758-
return Ok(true);
759-
};
760-
if block.header.chain_length > last_known_block.block.header.chain_length {
761-
Ok(true)
762-
} else {
763-
warn!(
764-
"Miner's block proposal does not confirm as many blocks as we expect";
765-
"proposed_block_consensus_hash" => %block.header.consensus_hash,
766-
"signer_signature_hash" => %block.header.signer_signature_hash(),
767-
"proposed_chain_length" => block.header.chain_length,
768-
"expected_at_least" => last_known_block.block.header.chain_length + 1,
769-
);
770-
Ok(false)
771-
}
765+
Self::check_latest_block_in_tenure(
766+
&block.header.consensus_hash,
767+
block,
768+
signer_db,
769+
client,
770+
self.config.tenure_last_block_proposal_timeout,
771+
self.config.reorg_attempts_activity_timeout,
772+
)
772773
}
773774

774775
/// Fetch a new view of the recent sortitions

stacks-signer/src/v0/signer.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,6 @@ impl Signer {
961961
proposed_block: &NakamotoBlock,
962962
) -> Option<BlockResponse> {
963963
let signer_signature_hash = proposed_block.header.signer_signature_hash();
964-
let proposed_block_consensus_hash = proposed_block.header.consensus_hash;
965964
// If this is a tenure change block, ensure that it confirms the correct number of blocks from the parent tenure.
966965
if let Some(tenure_change) = proposed_block.get_tenure_change_tx_payload() {
967966
// Ensure that the tenure change block confirms the expected parent block
@@ -973,7 +972,7 @@ impl Signer {
973972
self.proposal_config.tenure_last_block_proposal_timeout,
974973
self.proposal_config.reorg_attempts_activity_timeout,
975974
) {
976-
Ok(true) => {}
975+
Ok(true) => return None,
977976
Ok(false) => {
978977
return Some(self.create_block_rejection(
979978
RejectReason::SortitionViewMismatch,
@@ -996,40 +995,43 @@ impl Signer {
996995
}
997996

998997
// Ensure that the block is the last block in the chain of its current tenure.
999-
match self
1000-
.signer_db
1001-
.get_last_accepted_block(&proposed_block_consensus_hash)
1002-
{
1003-
Ok(Some(last_block_info)) => {
1004-
if proposed_block.header.chain_length <= last_block_info.block.header.chain_length {
998+
match SortitionsView::check_latest_block_in_tenure(
999+
&proposed_block.header.consensus_hash,
1000+
proposed_block,
1001+
&mut self.signer_db,
1002+
stacks_client,
1003+
self.proposal_config.tenure_last_block_proposal_timeout,
1004+
self.proposal_config.reorg_attempts_activity_timeout,
1005+
) {
1006+
Ok(is_latest) => {
1007+
if !is_latest {
10051008
warn!(
10061009
"Miner's block proposal does not confirm as many blocks as we expect";
1007-
"proposed_block_consensus_hash" => %proposed_block_consensus_hash,
1010+
"proposed_block_consensus_hash" => %proposed_block.header.consensus_hash,
10081011
"proposed_block_signer_signature_hash" => %signer_signature_hash,
10091012
"proposed_chain_length" => proposed_block.header.chain_length,
1010-
"expected_at_least" => last_block_info.block.header.chain_length + 1,
10111013
);
1012-
return Some(self.create_block_rejection(
1014+
Some(self.create_block_rejection(
10131015
RejectReason::SortitionViewMismatch,
10141016
proposed_block,
1015-
));
1017+
))
1018+
} else {
1019+
None
10161020
}
10171021
}
1018-
Ok(_) => {}
10191022
Err(e) => {
10201023
warn!("{self}: Failed to check block against signer db: {e}";
10211024
"signer_signature_hash" => %signer_signature_hash,
10221025
"block_id" => %proposed_block.block_id()
10231026
);
1024-
return Some(self.create_block_rejection(
1027+
Some(self.create_block_rejection(
10251028
RejectReason::ConnectivityIssues(
10261029
"failed to check block against signer db".to_string(),
10271030
),
10281031
proposed_block,
1029-
));
1032+
))
10301033
}
10311034
}
1032-
None
10331035
}
10341036

10351037
/// Handle the block validate ok response. Returns our block response if we have one

0 commit comments

Comments
 (0)