Skip to content

Commit fb30553

Browse files
authored
Merge branch 'develop' into fix/reload-config-flaky-test
2 parents c2f79e9 + 3123ccb commit fb30553

File tree

8 files changed

+321
-35
lines changed

8 files changed

+321
-35
lines changed

.github/workflows/github-release.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,21 @@ jobs:
142142
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
143143
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
144144
dist: ${{ matrix.dist }}
145+
146+
## Create the downstream PR for the release branch to master,develop
147+
create-pr:
148+
if: |
149+
inputs.node_tag != '' ||
150+
inputs.signer_tag != ''
151+
name: Create Downstream PR (${{ github.ref_name }})
152+
runs-on: ubuntu-latest
153+
needs:
154+
- build-binaries
155+
- create-release
156+
- docker-image
157+
steps:
158+
- name: Open Downstream PR
159+
id: create-pr
160+
uses: stacks-network/actions/stacks-core/release/downstream-pr@main
161+
with:
162+
token: ${{ secrets.GH_TOKEN }}

libsigner/src/v0/messages.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ use clarity::util::hash::Sha256Sum;
5151
use clarity::util::retry::BoundReader;
5252
use clarity::util::secp256k1::MessageSignature;
5353
use clarity::vm::types::serialization::SerializationError;
54-
use clarity::vm::types::{QualifiedContractIdentifier, TupleData};
54+
use clarity::vm::types::{QualifiedContractIdentifier, ResponseData, TupleData};
5555
use clarity::vm::Value;
5656
use hashbrown::{HashMap, HashSet};
5757
use serde::{Deserialize, Serialize};
@@ -875,11 +875,11 @@ impl BlockResponse {
875875
}
876876
}
877877

878-
/// The signer signature hash for the block response
879-
pub fn signer_signature_hash(&self) -> Sha512Trunc256Sum {
878+
/// Get the block response data from the block response
879+
pub fn get_response_data(&self) -> &BlockResponseData {
880880
match self {
881-
BlockResponse::Accepted(accepted) => accepted.signer_signature_hash,
882-
BlockResponse::Rejected(rejection) => rejection.signer_signature_hash,
881+
BlockResponse::Accepted(accepted) => &accepted.response_data,
882+
BlockResponse::Rejected(rejection) => &rejection.response_data,
883883
}
884884
}
885885

stacks-signer/CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- For some rejection reasons, a signer will reconsider a block proposal that it previously rejected ([#5880](https://github.com/stacks-network/stacks-core/pull/5880))
13+
1014
## [3.1.0.0.7.0]
1115

12-
## Changed
16+
### Changed
1317

1418
- Add new reject codes to the signer response for better visibility into why a block was rejected.
1519
- When allowing a reorg within the `reorg_attempts_activity_timeout_ms`, the signer will now watch the responses from other signers and if >30% of them reject this reorg attempt, then the signer will mark the miner as invalid, reject further attempts to reorg and allow the previous miner to extend their tenure.
@@ -20,7 +24,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
2024

2125
## [3.1.0.0.6.0]
2226

23-
## Added
27+
### Added
2428

2529
- Introduced the `reorg_attempts_activity_timeout_ms` configuration option for signers which is used to determine the length of time after the last block of a tenure is confirmed that an incoming miner's attempts to reorg it are considered valid miner activity.
2630
- Add signer configuration option `tenure_idle_timeout_buffer_secs` to specify the number of seconds of buffer the signer will add to its tenure extend time that it sends to miners. The idea is to allow for some clock skew between the miner and signers, preventing the case where the miner attempts to tenure extend too early.

stacks-signer/src/signerdb.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ pub struct BlockInfo {
167167
pub validation_time_ms: Option<u64>,
168168
/// Extra data specific to v0, v1, etc.
169169
pub ext: ExtraBlockInfo,
170+
/// If this signer rejected this block, what was the reason
171+
pub reject_reason: Option<RejectReason>,
170172
}
171173

172174
impl From<BlockProposal> for BlockInfo {
@@ -184,6 +186,7 @@ impl From<BlockProposal> for BlockInfo {
184186
ext: ExtraBlockInfo::default(),
185187
state: BlockState::Unprocessed,
186188
validation_time_ms: None,
189+
reject_reason: None,
187190
}
188191
}
189192
}
@@ -2193,4 +2196,77 @@ mod tests {
21932196
.unwrap()
21942197
.is_none());
21952198
}
2199+
2200+
/// BlockInfo without the `reject_reason` field for backwards compatibility testing
2201+
#[derive(Serialize, Deserialize, Debug, PartialEq)]
2202+
pub struct BlockInfoPrev {
2203+
/// The block we are considering
2204+
pub block: NakamotoBlock,
2205+
/// The burn block height at which the block was proposed
2206+
pub burn_block_height: u64,
2207+
/// The reward cycle the block belongs to
2208+
pub reward_cycle: u64,
2209+
/// Our vote on the block if we have one yet
2210+
pub vote: Option<NakamotoBlockVote>,
2211+
/// Whether the block contents are valid
2212+
pub valid: Option<bool>,
2213+
/// Whether this block is already being signed over
2214+
pub signed_over: bool,
2215+
/// Time at which the proposal was received by this signer (epoch time in seconds)
2216+
pub proposed_time: u64,
2217+
/// Time at which the proposal was signed by this signer (epoch time in seconds)
2218+
pub signed_self: Option<u64>,
2219+
/// Time at which the proposal was signed by a threshold in the signer set (epoch time in seconds)
2220+
pub signed_group: Option<u64>,
2221+
/// The block state relative to the signer's view of the stacks blockchain
2222+
pub state: BlockState,
2223+
/// Consumed processing time in milliseconds to validate this block
2224+
pub validation_time_ms: Option<u64>,
2225+
/// Extra data specific to v0, v1, etc.
2226+
pub ext: ExtraBlockInfo,
2227+
}
2228+
2229+
/// Verify that we can deserialize the old BlockInfo struct into the new version
2230+
#[test]
2231+
fn deserialize_old_block_info() {
2232+
let block_info_prev = BlockInfoPrev {
2233+
block: NakamotoBlock {
2234+
header: NakamotoBlockHeader::genesis(),
2235+
txs: vec![],
2236+
},
2237+
burn_block_height: 2,
2238+
reward_cycle: 3,
2239+
vote: None,
2240+
valid: None,
2241+
signed_over: true,
2242+
proposed_time: 4,
2243+
signed_self: None,
2244+
signed_group: None,
2245+
state: BlockState::Unprocessed,
2246+
validation_time_ms: Some(5),
2247+
ext: ExtraBlockInfo::default(),
2248+
};
2249+
2250+
let block_info: BlockInfo =
2251+
serde_json::from_value(serde_json::to_value(&block_info_prev).unwrap()).unwrap();
2252+
assert_eq!(block_info.block, block_info_prev.block);
2253+
assert_eq!(
2254+
block_info.burn_block_height,
2255+
block_info_prev.burn_block_height
2256+
);
2257+
assert_eq!(block_info.reward_cycle, block_info_prev.reward_cycle);
2258+
assert_eq!(block_info.vote, block_info_prev.vote);
2259+
assert_eq!(block_info.valid, block_info_prev.valid);
2260+
assert_eq!(block_info.signed_over, block_info_prev.signed_over);
2261+
assert_eq!(block_info.proposed_time, block_info_prev.proposed_time);
2262+
assert_eq!(block_info.signed_self, block_info_prev.signed_self);
2263+
assert_eq!(block_info.signed_group, block_info_prev.signed_group);
2264+
assert_eq!(block_info.state, block_info_prev.state);
2265+
assert_eq!(
2266+
block_info.validation_time_ms,
2267+
block_info_prev.validation_time_ms
2268+
);
2269+
assert_eq!(block_info.ext, block_info_prev.ext);
2270+
assert!(block_info.reject_reason.is_none());
2271+
}
21962272
}

stacks-signer/src/v0/signer.rs

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ use std::time::{Duration, Instant};
2121

2222
use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader};
2323
use blockstack_lib::net::api::postblock_proposal::{
24-
BlockValidateOk, BlockValidateReject, BlockValidateResponse, TOO_MANY_REQUESTS_STATUS,
24+
BlockValidateOk, BlockValidateReject, BlockValidateResponse, ValidateRejectCode,
25+
TOO_MANY_REQUESTS_STATUS,
2526
};
2627
use blockstack_lib::util_lib::db::Error as DBError;
2728
use clarity::types::chainstate::StacksPrivateKey;
@@ -391,6 +392,7 @@ impl Signer {
391392
),
392393
)
393394
}
395+
394396
/// Create a block rejection response for a block with the given reject code
395397
pub fn create_block_rejection(
396398
&self,
@@ -411,6 +413,7 @@ impl Signer {
411413
),
412414
)
413415
}
416+
414417
/// Check if block should be rejected based on sortition state
415418
/// Will return a BlockResponse::Rejection if the block is invalid, none otherwise.
416419
fn check_block_against_sortition_state(
@@ -556,33 +559,19 @@ impl Signer {
556559
// 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.
557560
// 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
558561
let signer_signature_hash = block_proposal.block.header.signer_signature_hash();
559-
if let Some(block_info) = self.block_lookup_by_reward_cycle(&signer_signature_hash) {
560-
let Some(block_response) = self.determine_response(&block_info) else {
561-
// We are still waiting for a response for this block. Do nothing.
562-
debug!("{self}: Received a block proposal for a block we are already validating.";
563-
"signer_sighash" => %signer_signature_hash,
564-
"block_id" => %block_proposal.block.block_id()
565-
);
566-
return;
567-
};
568-
// Submit a proposal response to the .signers contract for miners
569-
debug!("{self}: Broadcasting a block response to stacks node: {block_response:?}");
570-
let accepted = matches!(block_response, BlockResponse::Accepted(..));
571-
match self
572-
.stackerdb
573-
.send_message_with_retry::<SignerMessage>(block_response.into())
574-
{
575-
Ok(_) => {
576-
crate::monitoring::actions::increment_block_responses_sent(accepted);
577-
crate::monitoring::actions::record_block_response_latency(
578-
&block_proposal.block,
579-
);
580-
}
581-
Err(e) => {
582-
warn!("{self}: Failed to send block response to stacker-db: {e:?}",);
583-
}
584-
}
585-
return;
562+
let prior_evaluation = self
563+
.block_lookup_by_reward_cycle(&signer_signature_hash)
564+
.and_then(|block_info| if should_reevaluate_block(&block_info) {
565+
debug!("Received a proposal for this block before, but our rejection reason allows us to reconsider";
566+
"reject_reason" => ?block_info.reject_reason);
567+
None
568+
} else {
569+
Some(block_info)
570+
});
571+
572+
// we previously considered this proposal, handle the status here
573+
if let Some(block_info) = prior_evaluation {
574+
return self.handle_prior_proposal_eval(&block_info);
586575
}
587576

588577
info!(
@@ -667,6 +656,32 @@ impl Signer {
667656
}
668657
}
669658

659+
fn handle_prior_proposal_eval(&mut self, block_info: &BlockInfo) {
660+
let Some(block_response) = self.determine_response(block_info) else {
661+
// We are still waiting for a response for this block. Do nothing.
662+
debug!(
663+
"{self}: Received a block proposal for a block we are already validating.";
664+
"signer_sighash" => %block_info.signer_signature_hash(),
665+
"block_id" => %block_info.block.block_id()
666+
);
667+
return;
668+
};
669+
670+
// Submit a proposal response to the .signers contract for miners
671+
debug!("{self}: Broadcasting a block response to stacks node: {block_response:?}");
672+
673+
let accepted = matches!(block_response, BlockResponse::Accepted(..));
674+
if let Err(e) = self
675+
.stackerdb
676+
.send_message_with_retry::<SignerMessage>(block_response.into())
677+
{
678+
warn!("{self}: Failed to send block response to stacker-db: {e:?}");
679+
} else {
680+
crate::monitoring::actions::increment_block_responses_sent(accepted);
681+
crate::monitoring::actions::record_block_response_latency(&block_info.block);
682+
}
683+
}
684+
670685
/// Handle block response messages from a signer
671686
fn handle_block_response(
672687
&mut self,
@@ -886,6 +901,8 @@ impl Signer {
886901
false,
887902
),
888903
);
904+
905+
block_info.reject_reason = Some(block_rejection.response_data.reject_reason.clone());
889906
self.signer_db
890907
.insert_block(&block_info)
891908
.unwrap_or_else(|e| self.handle_insert_block_error(e));
@@ -1015,6 +1032,7 @@ impl Signer {
10151032
),
10161033
&block_info.block,
10171034
);
1035+
block_info.reject_reason = Some(rejection.get_response_data().reject_reason.clone());
10181036
if let Err(e) = block_info.mark_locally_rejected() {
10191037
if !block_info.has_reached_consensus() {
10201038
warn!("{self}: Failed to mark block as locally rejected: {e:?}");
@@ -1035,6 +1053,7 @@ impl Signer {
10351053
),
10361054
Ok(_) => debug!("{self}: Block rejection accepted by stacker-db"),
10371055
}
1056+
10381057
self.signer_db
10391058
.insert_block(&block_info)
10401059
.unwrap_or_else(|e| self.handle_insert_block_error(e));
@@ -1128,6 +1147,7 @@ impl Signer {
11281147
) {
11291148
warn!("{self}: Failed to save block rejection signature: {e:?}",);
11301149
}
1150+
block_info.reject_reason = Some(rejection.response_data.reject_reason.clone());
11311151

11321152
// do we have enough signatures to mark a block a globally rejected?
11331153
// i.e. is (set-size) - (threshold) + 1 reached.
@@ -1408,3 +1428,33 @@ impl Signer {
14081428
}
14091429
}
14101430
}
1431+
1432+
/// Determine if a block should be re-evaluated based on its rejection reason˝
1433+
fn should_reevaluate_block(block_info: &BlockInfo) -> bool {
1434+
if let Some(reject_reason) = &block_info.reject_reason {
1435+
match reject_reason {
1436+
RejectReason::ValidationFailed(ValidateRejectCode::UnknownParent)
1437+
| RejectReason::NoSortitionView
1438+
| RejectReason::ConnectivityIssues(_)
1439+
| RejectReason::TestingDirective
1440+
| RejectReason::InvalidTenureExtend
1441+
| RejectReason::NotRejected
1442+
| RejectReason::Unknown(_) => true,
1443+
RejectReason::ValidationFailed(_)
1444+
| RejectReason::RejectedInPriorRound
1445+
| RejectReason::SortitionViewMismatch
1446+
| RejectReason::ReorgNotAllowed
1447+
| RejectReason::InvalidBitvec
1448+
| RejectReason::PubkeyHashMismatch
1449+
| RejectReason::InvalidMiner
1450+
| RejectReason::NotLatestSortitionWinner
1451+
| RejectReason::InvalidParentBlock
1452+
| RejectReason::DuplicateBlockFound => {
1453+
// No need to re-validate these types of rejections.
1454+
false
1455+
}
1456+
}
1457+
} else {
1458+
false
1459+
}
1460+
}

stacks-signer/src/v0/tests.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ impl Signer {
9292
warn!("{self}: Failed to mark block as locally rejected: {e:?}");
9393
}
9494
};
95+
96+
block_info.reject_reason = Some(RejectReason::TestingDirective);
97+
9598
// We must insert the block into the DB to prevent subsequent repeat proposals being accepted (should reject
9699
// as invalid since we rejected in a prior round if this crops up again)
97100
// in case this is the first time we saw this block. Safe to do since this is testing case only.

testnet/stacks-node/src/tests/nakamoto_integrations.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6642,6 +6642,7 @@ fn signer_chainstate() {
66426642
ext: ExtraBlockInfo::None,
66436643
state: BlockState::Unprocessed,
66446644
validation_time_ms: None,
6645+
reject_reason: None,
66456646
})
66466647
.unwrap();
66476648

@@ -6722,6 +6723,7 @@ fn signer_chainstate() {
67226723
ext: ExtraBlockInfo::None,
67236724
state: BlockState::GloballyAccepted,
67246725
validation_time_ms: Some(1000),
6726+
reject_reason: None,
67256727
})
67266728
.unwrap();
67276729

0 commit comments

Comments
 (0)