Skip to content

Commit 9b5621c

Browse files
authored
Merge pull request #5662 from stacks-network/fix/signers-verify-reward-cycle
Do not accept signatures, block proposals, or block responses for blocks from different reward cycles
2 parents f4db3c9 + a45de5f commit 9b5621c

File tree

7 files changed

+947
-125
lines changed

7 files changed

+947
-125
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ jobs:
136136
- tests::signer::v0::block_proposal_max_age_rejections
137137
- tests::signer::v0::global_acceptance_depends_on_block_announcement
138138
- tests::signer::v0::no_reorg_due_to_successive_block_validation_ok
139+
- tests::signer::v0::incoming_signers_ignore_block_proposals
140+
- tests::signer::v0::outgoing_signers_ignore_block_proposals
141+
- tests::signer::v0::injected_signatures_are_ignored_across_boundaries
139142
- tests::nakamoto_integrations::burn_ops_integration_test
140143
- tests::nakamoto_integrations::check_block_heights
141144
- tests::nakamoto_integrations::clarity_burn_state

stacks-signer/src/runloop.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ use stacks_common::{debug, error, info, warn};
2626
use crate::chainstate::SortitionsView;
2727
use crate::client::{retry_with_exponential_backoff, ClientError, StacksClient};
2828
use crate::config::{GlobalConfig, SignerConfig};
29+
#[cfg(any(test, feature = "testing"))]
30+
use crate::v0::tests::TEST_SKIP_SIGNER_CLEANUP;
2931
use crate::Signer as SignerTrait;
3032

3133
#[derive(thiserror::Error, Debug)]
@@ -46,6 +48,8 @@ pub struct StateInfo {
4648
pub runloop_state: State,
4749
/// the current reward cycle info
4850
pub reward_cycle_info: Option<RewardCycleInfo>,
51+
/// The current running signers reward cycles
52+
pub running_signers: Vec<u64>,
4953
}
5054

5155
/// The signer result that can be sent across threads
@@ -421,6 +425,11 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug> RunLo
421425
}
422426

423427
fn cleanup_stale_signers(&mut self, current_reward_cycle: u64) {
428+
#[cfg(any(test, feature = "testing"))]
429+
if TEST_SKIP_SIGNER_CLEANUP.get() {
430+
warn!("Skipping signer cleanup due to testing directive.");
431+
return;
432+
}
424433
let mut to_delete = Vec::new();
425434
for (idx, signer) in &mut self.stacks_signers {
426435
let reward_cycle = signer.reward_cycle();
@@ -467,6 +476,11 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug>
467476
if let Err(e) = res.send(vec![StateInfo {
468477
runloop_state: self.state,
469478
reward_cycle_info: self.current_reward_cycle_info,
479+
running_signers: self
480+
.stacks_signers
481+
.values()
482+
.map(|s| s.reward_cycle())
483+
.collect(),
470484
}
471485
.into()])
472486
{

stacks-signer/src/signerdb.rs

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -960,22 +960,6 @@ impl SignerDb {
960960
Ok(Some(broadcasted))
961961
}
962962

963-
/// Get the current state of a given block in the database
964-
pub fn get_block_state(
965-
&self,
966-
block_sighash: &Sha512Trunc256Sum,
967-
) -> Result<Option<BlockState>, DBError> {
968-
let qry = "SELECT state FROM blocks WHERE signer_signature_hash = ?1 LIMIT 1";
969-
let args = params![block_sighash];
970-
let state_opt: Option<String> = query_row(&self.db, qry, args)?;
971-
let Some(state) = state_opt else {
972-
return Ok(None);
973-
};
974-
Ok(Some(
975-
BlockState::try_from(state.as_str()).map_err(|_| DBError::Corruption)?,
976-
))
977-
}
978-
979963
/// Return the start time (epoch time in seconds) and the processing time in milliseconds of the tenure (idenfitied by consensus_hash).
980964
fn get_tenure_times(&self, tenure: &ConsensusHash) -> Result<(u64, u64), DBError> {
981965
let query = "SELECT tenure_change, proposed_time, validation_time_ms FROM blocks WHERE consensus_hash = ?1 AND state = ?2 ORDER BY stacks_height DESC";
@@ -1133,13 +1117,6 @@ mod tests {
11331117
.expect("Unable to get block from db");
11341118

11351119
assert_eq!(BlockInfo::from(block_proposal_2.clone()), block_info);
1136-
// test getting the block state
1137-
let block_state = db
1138-
.get_block_state(&block_proposal_1.block.header.signer_signature_hash())
1139-
.unwrap()
1140-
.expect("Unable to get block state from db");
1141-
1142-
assert_eq!(block_state, BlockInfo::from(block_proposal_1.clone()).state);
11431120
}
11441121

11451122
#[test]

stacks-signer/src/v0/signer.rs

Lines changed: 68 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use blockstack_lib::net::api::postblock_proposal::{
2424
use blockstack_lib::util_lib::db::Error as DBError;
2525
use clarity::types::chainstate::StacksPrivateKey;
2626
use clarity::types::{PrivateKey, StacksEpochId};
27-
use clarity::util::hash::MerkleHashFunc;
27+
use clarity::util::hash::{MerkleHashFunc, Sha512Trunc256Sum};
2828
use clarity::util::secp256k1::Secp256k1PublicKey;
2929
use libsigner::v0::messages::{
3030
BlockAccepted, BlockRejection, BlockResponse, MessageSlotID, MockProposal, MockSignature,
@@ -128,6 +128,17 @@ impl SignerTrait<SignerMessage> for Signer {
128128
debug!("{self}: No event received");
129129
return;
130130
};
131+
if self.reward_cycle > current_reward_cycle
132+
&& !matches!(
133+
event,
134+
SignerEvent::StatusCheck | SignerEvent::NewBurnBlock { .. }
135+
)
136+
{
137+
// The reward cycle has not yet started for this signer instance
138+
// Do not process any events other than status checks or new burn blocks
139+
debug!("{self}: Signer reward cycle has not yet started. Ignoring event.");
140+
return;
141+
}
131142
match event {
132143
SignerEvent::BlockValidationResponse(block_validate_response) => {
133144
debug!("{self}: Received a block proposal result from the stacks node...");
@@ -444,11 +455,7 @@ impl Signer {
444455
// TODO: should add a check to ignore an old burn block height if we know its outdated. Would require us to store the burn block height we last saw on the side.
445456
// the signer needs to be able to determine whether or not the block they're about to sign would conflict with an already-signed Stacks block
446457
let signer_signature_hash = block_proposal.block.header.signer_signature_hash();
447-
if let Some(block_info) = self
448-
.signer_db
449-
.block_lookup(&signer_signature_hash)
450-
.expect("Failed to connect to signer DB")
451-
{
458+
if let Some(block_info) = self.block_lookup_by_reward_cycle(&signer_signature_hash) {
452459
let Some(block_response) = self.determine_response(&block_info) else {
453460
// We are still waiting for a response for this block. Do nothing.
454461
debug!("{self}: Received a block proposal for a block we are already validating.";
@@ -677,32 +684,15 @@ impl Signer {
677684
self.submitted_block_proposal = None;
678685
}
679686
// For mutability reasons, we need to take the block_info out of the map and add it back after processing
680-
let mut block_info = match self.signer_db.block_lookup(&signer_signature_hash) {
681-
Ok(Some(block_info)) => {
682-
if block_info.reward_cycle != self.reward_cycle {
683-
// We are not signing for this reward cycle. Ignore the block.
684-
debug!(
685-
"{self}: Received a block validation response for a different reward cycle. Ignore it.";
686-
"requested_reward_cycle" => block_info.reward_cycle,
687-
);
688-
return None;
689-
}
690-
if block_info.is_locally_finalized() {
691-
debug!("{self}: Received block validation for a block that is already marked as {}. Ignoring...", block_info.state);
692-
return None;
693-
}
694-
block_info
695-
}
696-
Ok(None) => {
697-
// We have not seen this block before. Why are we getting a response for it?
698-
debug!("{self}: Received a block validate response for a block we have not seen before. Ignoring...");
699-
return None;
700-
}
701-
Err(e) => {
702-
error!("{self}: Failed to lookup block in signer db: {e:?}",);
703-
return None;
704-
}
687+
let Some(mut block_info) = self.block_lookup_by_reward_cycle(&signer_signature_hash) else {
688+
// We have not seen this block before. Why are we getting a response for it?
689+
debug!("{self}: Received a block validate response for a block we have not seen before. Ignoring...");
690+
return None;
705691
};
692+
if block_info.is_locally_finalized() {
693+
debug!("{self}: Received block validation for a block that is already marked as {}. Ignoring...", block_info.state);
694+
return None;
695+
}
706696

707697
if let Some(block_response) =
708698
self.check_block_against_signer_db_state(stacks_client, &block_info.block)
@@ -772,32 +762,15 @@ impl Signer {
772762
{
773763
self.submitted_block_proposal = None;
774764
}
775-
let mut block_info = match self.signer_db.block_lookup(&signer_signature_hash) {
776-
Ok(Some(block_info)) => {
777-
if block_info.reward_cycle != self.reward_cycle {
778-
// We are not signing for this reward cycle. Ignore the block.
779-
debug!(
780-
"{self}: Received a block validation response for a different reward cycle. Ignore it.";
781-
"requested_reward_cycle" => block_info.reward_cycle,
782-
);
783-
return None;
784-
}
785-
if block_info.is_locally_finalized() {
786-
debug!("{self}: Received block validation for a block that is already marked as {}. Ignoring...", block_info.state);
787-
return None;
788-
}
789-
block_info
790-
}
791-
Ok(None) => {
792-
// We have not seen this block before. Why are we getting a response for it?
793-
debug!("{self}: Received a block validate response for a block we have not seen before. Ignoring...");
794-
return None;
795-
}
796-
Err(e) => {
797-
error!("{self}: Failed to lookup block in signer db: {e:?}");
798-
return None;
799-
}
765+
let Some(mut block_info) = self.block_lookup_by_reward_cycle(&signer_signature_hash) else {
766+
// We have not seen this block before. Why are we getting a response for it?
767+
debug!("{self}: Received a block validate response for a block we have not seen before. Ignoring...");
768+
return None;
800769
};
770+
if block_info.is_locally_finalized() {
771+
debug!("{self}: Received block validation for a block that is already marked as {}. Ignoring...", block_info.state);
772+
return None;
773+
}
801774
if let Err(e) = block_info.mark_locally_rejected() {
802775
if !block_info.has_reached_consensus() {
803776
warn!("{self}: Failed to mark block as locally rejected: {e:?}",);
@@ -873,9 +846,7 @@ impl Signer {
873846
// For mutability reasons, we need to take the block_info out of the map and add it back after processing
874847
let mut block_info = match self.signer_db.block_lookup(&signature_sighash) {
875848
Ok(Some(block_info)) => {
876-
if block_info.state == BlockState::GloballyRejected
877-
|| block_info.state == BlockState::GloballyAccepted
878-
{
849+
if block_info.has_reached_consensus() {
879850
// The block has already reached consensus.
880851
return;
881852
}
@@ -952,25 +923,16 @@ impl Signer {
952923
let block_hash = &rejection.signer_signature_hash;
953924
let signature = &rejection.signature;
954925

955-
let mut block_info = match self.signer_db.block_lookup(block_hash) {
956-
Ok(Some(block_info)) => {
957-
if block_info.state == BlockState::GloballyRejected
958-
|| block_info.state == BlockState::GloballyAccepted
959-
{
960-
debug!("{self}: Received block rejection for a block that is already marked as {}. Ignoring...", block_info.state);
961-
return;
962-
}
963-
block_info
964-
}
965-
Ok(None) => {
966-
debug!("{self}: Received block rejection for a block we have not seen before. Ignoring...");
967-
return;
968-
}
969-
Err(e) => {
970-
warn!("{self}: Failed to load block state: {e:?}",);
971-
return;
972-
}
926+
let Some(mut block_info) = self.block_lookup_by_reward_cycle(block_hash) else {
927+
debug!(
928+
"{self}: Received block rejection for a block we have not seen before. Ignoring..."
929+
);
930+
return;
973931
};
932+
if block_info.has_reached_consensus() {
933+
debug!("{self}: Received block rejection for a block that is already marked as {}. Ignoring...", block_info.state);
934+
return;
935+
}
974936

975937
// recover public key
976938
let Ok(public_key) = rejection.recover_public_key() else {
@@ -1056,23 +1018,15 @@ impl Signer {
10561018
"{self}: Received a block-accept signature: ({block_hash}, {signature}, {})",
10571019
metadata.server_version
10581020
);
1059-
1060-
// Have we already processed this block?
1061-
match self.signer_db.get_block_state(block_hash) {
1062-
Ok(Some(state)) => {
1063-
if state == BlockState::GloballyAccepted || state == BlockState::GloballyRejected {
1064-
debug!("{self}: Received block signature for a block that is already marked as {}. Ignoring...", state);
1065-
return;
1066-
}
1067-
}
1068-
Ok(None) => {
1069-
debug!("{self}: Received block signature for a block we have not seen before. Ignoring...");
1070-
return;
1071-
}
1072-
Err(e) => {
1073-
warn!("{self}: Failed to load block state: {e:?}",);
1074-
return;
1075-
}
1021+
let Some(mut block_info) = self.block_lookup_by_reward_cycle(block_hash) else {
1022+
debug!(
1023+
"{self}: Received block signature for a block we have not seen before. Ignoring..."
1024+
);
1025+
return;
1026+
};
1027+
if block_info.has_reached_consensus() {
1028+
debug!("{self}: Received block signature for a block that is already marked as {}. Ignoring...", block_info.state);
1029+
return;
10761030
}
10771031

10781032
// recover public key
@@ -1140,12 +1094,6 @@ impl Signer {
11401094
}
11411095

11421096
// have enough signatures to broadcast!
1143-
let Ok(Some(mut block_info)) = self.signer_db.block_lookup(block_hash).inspect_err(|e| {
1144-
warn!("{self}: Failed to load block {block_hash}: {e:?})");
1145-
}) else {
1146-
warn!("{self}: No such block {block_hash}");
1147-
return;
1148-
};
11491097
// move block to LOCALLY accepted state.
11501098
// It is only considered globally accepted IFF we receive a new block event confirming it OR see the chain tip of the node advance to it.
11511099
if let Err(e) = block_info.mark_locally_accepted(true) {
@@ -1227,4 +1175,24 @@ impl Signer {
12271175
error!("{self}: Failed to insert block into signer-db: {e:?}");
12281176
panic!("{self} Failed to write block to signerdb: {e}");
12291177
}
1178+
1179+
/// Helper for getting the block info from the db while accommodating for reward cycle
1180+
pub fn block_lookup_by_reward_cycle(
1181+
&self,
1182+
block_hash: &Sha512Trunc256Sum,
1183+
) -> Option<BlockInfo> {
1184+
let block_info = self
1185+
.signer_db
1186+
.block_lookup(block_hash)
1187+
.inspect_err(|e| {
1188+
error!("{self}: Failed to lookup block hash {block_hash} in signer db: {e:?}");
1189+
})
1190+
.ok()
1191+
.flatten()?;
1192+
if block_info.reward_cycle == self.reward_cycle {
1193+
Some(block_info)
1194+
} else {
1195+
None
1196+
}
1197+
}
12301198
}

stacks-signer/src/v0/tests.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ pub static TEST_SKIP_BLOCK_BROADCAST: LazyLock<TestFlag<bool>> = LazyLock::new(T
4545
pub static TEST_STALL_BLOCK_VALIDATION_SUBMISSION: LazyLock<TestFlag<bool>> =
4646
LazyLock::new(TestFlag::default);
4747

48+
/// A global variable that can be used to prevent signer cleanup
49+
pub static TEST_SKIP_SIGNER_CLEANUP: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
50+
4851
impl Signer {
4952
/// Skip the block broadcast if the TEST_SKIP_BLOCK_BROADCAST flag is set
5053
pub fn test_skip_block_broadcast(&self, block: &NakamotoBlock) -> bool {

0 commit comments

Comments
 (0)