Skip to content

Commit d912e8f

Browse files
committed
Merge branch 'develop' into feat/rpc-health-v3
2 parents 30ae53d + 14230bf commit d912e8f

File tree

22 files changed

+926
-350
lines changed

22 files changed

+926
-350
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1111

1212
- Added a new RPC endpoint `/v3/health` to query the node's health status. The endpoint returns a 200 status code with relevant synchronization information (including the node's current Stacks tip height, the maximum Stacks tip height among its neighbors, and the difference between these two). A user can use the `difference_from_max_peer` value to decide what is a good threshold for them before considering the node out of sync. The endpoint returns a 500 status code if the query cannot retrieve viable data.
1313

14+
## [3.1.0.0.11]
15+
16+
- Hotfix for p2p stack misbehavior in mempool syncing conditions
17+
1418
## [3.1.0.0.10]
1519

1620
### Added

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/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ use chainstate::SortitionsView;
5353
use config::GlobalConfig;
5454
use libsigner::{SignerEvent, SignerEventReceiver, SignerEventTrait, VERSION_STRING};
5555
use runloop::SignerResult;
56+
use signerdb::BlockInfo;
5657
use stacks_common::{info, warn};
5758
use v0::signer_state::LocalStateMachine;
5859

@@ -81,6 +82,8 @@ pub trait Signer<T: SignerEventTrait>: Debug + Display {
8182
fn get_local_state_machine(&self) -> &LocalStateMachine;
8283
/// Get the number of pending block proposals
8384
fn get_pending_proposals_count(&self) -> u64;
85+
/// Get canonical block according to this signer's db
86+
fn get_canonical_tip(&self) -> Option<BlockInfo>;
8487
}
8588

8689
/// A wrapper around the running signer type for the signer

stacks-signer/src/runloop.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use stacks_common::{debug, error, info, warn};
2525
use crate::chainstate::SortitionsView;
2626
use crate::client::{retry_with_exponential_backoff, ClientError, StacksClient};
2727
use crate::config::{GlobalConfig, SignerConfig, SignerConfigMode};
28+
use crate::signerdb::BlockInfo;
2829
use crate::v0::signer_state::LocalStateMachine;
2930
#[cfg(any(test, feature = "testing"))]
3031
use crate::v0::tests::TEST_SKIP_SIGNER_CLEANUP;
@@ -59,6 +60,9 @@ pub struct StateInfo {
5960
pub signer_state_machines: Vec<(u64, Option<LocalStateMachine>)>,
6061
/// The number of pending block proposals for this signer
6162
pub pending_proposals_count: u64,
63+
/// The canonical tip block info according to the running signers
64+
/// as a pair of (reward-cycle, block-info)
65+
pub signer_canonical_tips: Vec<(u64, Option<BlockInfo>)>,
6266
}
6367

6468
/// The signer result that can be sent across threads
@@ -544,6 +548,16 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug>
544548
}
545549
})
546550
.unwrap_or(0),
551+
signer_canonical_tips: self
552+
.stacks_signers
553+
.iter()
554+
.map(|(reward_cycle, signer)| {
555+
let ConfiguredSigner::RegisteredSigner(ref signer) = signer else {
556+
return (*reward_cycle, None);
557+
};
558+
(*reward_cycle, signer.get_canonical_tip())
559+
})
560+
.collect(),
547561
};
548562
info!("Signer status check requested: {state_info:?}");
549563

stacks-signer/src/signerdb.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ impl FromRow<BurnBlockInfo> for BurnBlockInfo {
9898
}
9999
}
100100

101-
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
101+
#[derive(Serialize, Deserialize, Debug, PartialEq, Default, Clone)]
102102
/// Store extra version-specific info in `BlockInfo`
103103
pub enum ExtraBlockInfo {
104104
#[default]
@@ -167,7 +167,7 @@ impl TryFrom<&str> for BlockState {
167167
}
168168

169169
/// Additional Info about a proposed block
170-
#[derive(Serialize, Deserialize, Debug, PartialEq)]
170+
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
171171
pub struct BlockInfo {
172172
/// The block we are considering
173173
pub block: NakamotoBlock,

0 commit comments

Comments
 (0)