Skip to content

Commit 6e0bd5a

Browse files
authored
Merge pull request #5409 from stacks-network/feat/signer-track-validation-submission-with-config-timeout
Feat/signer track validation submission with config timeout
2 parents db35bfe + b54421d commit 6e0bd5a

File tree

7 files changed

+364
-40
lines changed

7 files changed

+364
-40
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ jobs:
125125
- tests::signer::v0::multiple_miners_with_custom_chain_id
126126
- tests::signer::v0::block_commit_delay
127127
- tests::signer::v0::continue_after_fast_block_no_sortition
128+
- tests::signer::v0::block_validation_response_timeout
128129
- tests::nakamoto_integrations::burn_ops_integration_test
129130
- tests::nakamoto_integrations::check_block_heights
130131
- tests::nakamoto_integrations::clarity_burn_state

stacks-signer/src/client/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ pub(crate) mod tests {
412412
first_proposal_burn_block_timing: config.first_proposal_burn_block_timing,
413413
block_proposal_timeout: config.block_proposal_timeout,
414414
tenure_last_block_proposal_timeout: config.tenure_last_block_proposal_timeout,
415+
block_proposal_validation_timeout: config.block_proposal_validation_timeout,
415416
}
416417
}
417418

stacks-signer/src/config.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use crate::client::SignerSlotID;
3535

3636
const EVENT_TIMEOUT_MS: u64 = 5000;
3737
const BLOCK_PROPOSAL_TIMEOUT_MS: u64 = 600_000;
38+
const BLOCK_PROPOSAL_VALIDATION_TIMEOUT_MS: u64 = 120_000;
3839
const DEFAULT_FIRST_PROPOSAL_BURN_BLOCK_TIMING_SECS: u64 = 60;
3940
const DEFAULT_TENURE_LAST_BLOCK_PROPOSAL_TIMEOUT_SECS: u64 = 30;
4041

@@ -132,6 +133,8 @@ pub struct SignerConfig {
132133
/// Time to wait for the last block of a tenure to be globally accepted or rejected
133134
/// before considering a new miner's block at the same height as potentially valid.
134135
pub tenure_last_block_proposal_timeout: Duration,
136+
/// How much time to wait for a block proposal validation response before marking the block invalid
137+
pub block_proposal_validation_timeout: Duration,
135138
}
136139

137140
/// The parsed configuration for the signer
@@ -165,6 +168,9 @@ pub struct GlobalConfig {
165168
/// Time to wait for the last block of a tenure to be globally accepted or rejected
166169
/// before considering a new miner's block at the same height as potentially valid.
167170
pub tenure_last_block_proposal_timeout: Duration,
171+
/// How long to wait for a response from a block proposal validation response from the node
172+
/// before marking that block as invalid and rejecting it
173+
pub block_proposal_validation_timeout: Duration,
168174
}
169175

170176
/// Internal struct for loading up the config file
@@ -187,16 +193,19 @@ struct RawConfigFile {
187193
pub db_path: String,
188194
/// Metrics endpoint
189195
pub metrics_endpoint: Option<String>,
190-
/// How much time must pass in seconds between the first block proposal in a tenure and the next bitcoin block
191-
/// before a subsequent miner isn't allowed to reorg the tenure
196+
/// How much time (in secs) must pass between the first block proposal in a tenure and the next bitcoin block
197+
/// before a subsequent miner isn't allowed to reorg the tenure
192198
pub first_proposal_burn_block_timing_secs: Option<u64>,
193-
/// How much time to wait for a miner to propose a block following a sortition in milliseconds
199+
/// How much time (in millisecs) to wait for a miner to propose a block following a sortition
194200
pub block_proposal_timeout_ms: Option<u64>,
195201
/// An optional custom Chain ID
196202
pub chain_id: Option<u32>,
197203
/// Time in seconds to wait for the last block of a tenure to be globally accepted or rejected
198204
/// before considering a new miner's block at the same height as potentially valid.
199205
pub tenure_last_block_proposal_timeout_secs: Option<u64>,
206+
/// How long to wait (in millisecs) for a response from a block proposal validation response from the node
207+
/// before marking that block as invalid and rejecting it
208+
pub block_proposal_validation_timeout_ms: Option<u64>,
200209
}
201210

202211
impl RawConfigFile {
@@ -282,6 +291,12 @@ impl TryFrom<RawConfigFile> for GlobalConfig {
282291
.unwrap_or(DEFAULT_TENURE_LAST_BLOCK_PROPOSAL_TIMEOUT_SECS),
283292
);
284293

294+
let block_proposal_validation_timeout = Duration::from_millis(
295+
raw_data
296+
.block_proposal_validation_timeout_ms
297+
.unwrap_or(BLOCK_PROPOSAL_VALIDATION_TIMEOUT_MS),
298+
);
299+
285300
Ok(Self {
286301
node_host: raw_data.node_host,
287302
endpoint,
@@ -296,6 +311,7 @@ impl TryFrom<RawConfigFile> for GlobalConfig {
296311
block_proposal_timeout,
297312
chain_id: raw_data.chain_id,
298313
tenure_last_block_proposal_timeout,
314+
block_proposal_validation_timeout,
299315
})
300316
}
301317
}

stacks-signer/src/runloop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug> RunLo
284284
db_path: self.config.db_path.clone(),
285285
block_proposal_timeout: self.config.block_proposal_timeout,
286286
tenure_last_block_proposal_timeout: self.config.tenure_last_block_proposal_timeout,
287+
block_proposal_validation_timeout: self.config.block_proposal_validation_timeout,
287288
}))
288289
}
289290

stacks-signer/src/v0/signer.rs

Lines changed: 151 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use std::collections::HashMap;
1616
use std::fmt::Debug;
1717
use std::sync::mpsc::Sender;
18+
use std::time::{Duration, Instant};
1819

1920
use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader};
2021
use blockstack_lib::net::api::postblock_proposal::{
@@ -85,6 +86,11 @@ pub struct Signer {
8586
pub signer_db: SignerDb,
8687
/// Configuration for proposal evaluation
8788
pub proposal_config: ProposalEvalConfig,
89+
/// How long to wait for a block proposal validation response to arrive before
90+
/// marking a submitted block as invalid
91+
pub block_proposal_validation_timeout: Duration,
92+
/// The current submitted block proposal and its submission time
93+
pub submitted_block_proposal: Option<(BlockProposal, Instant)>,
8894
}
8995

9096
impl std::fmt::Display for Signer {
@@ -127,6 +133,7 @@ impl SignerTrait<SignerMessage> for Signer {
127133
if event_parity == Some(other_signer_parity) {
128134
return;
129135
}
136+
self.check_submitted_block_proposal();
130137
debug!("{self}: Processing event: {event:?}");
131138
let Some(event) = event else {
132139
// No event. Do nothing.
@@ -274,6 +281,8 @@ impl From<SignerConfig> for Signer {
274281
reward_cycle: signer_config.reward_cycle,
275282
signer_db,
276283
proposal_config,
284+
submitted_block_proposal: None,
285+
block_proposal_validation_timeout: signer_config.block_proposal_validation_timeout,
277286
}
278287
}
279288
}
@@ -355,7 +364,7 @@ impl Signer {
355364
}
356365

357366
info!(
358-
"{self}: received a block proposal for a new block. Submit block for validation. ";
367+
"{self}: received a block proposal for a new block.";
359368
"signer_sighash" => %signer_signature_hash,
360369
"block_id" => %block_proposal.block.block_id(),
361370
"block_height" => block_proposal.block.header.chain_length,
@@ -456,14 +465,35 @@ impl Signer {
456465
Ok(_) => debug!("{self}: Block rejection accepted by stacker-db"),
457466
}
458467
} else {
459-
// We don't know if proposal is valid, submit to stacks-node for further checks and store it locally.
460-
// Do not store invalid blocks as this could DOS the signer. We only store blocks that are valid or unknown.
461-
stacks_client
462-
.submit_block_for_validation(block_info.block.clone())
463-
.unwrap_or_else(|e| {
464-
warn!("{self}: Failed to submit block for validation: {e:?}");
465-
});
468+
// Just in case check if the last block validation submission timed out.
469+
self.check_submitted_block_proposal();
470+
if self.submitted_block_proposal.is_none() {
471+
// We don't know if proposal is valid, submit to stacks-node for further checks and store it locally.
472+
info!(
473+
"{self}: submitting block proposal for validation";
474+
"signer_sighash" => %signer_signature_hash,
475+
"block_id" => %block_proposal.block.block_id(),
476+
"block_height" => block_proposal.block.header.chain_length,
477+
"burn_height" => block_proposal.burn_height,
478+
);
479+
match stacks_client.submit_block_for_validation(block_info.block.clone()) {
480+
Ok(_) => {
481+
self.submitted_block_proposal =
482+
Some((block_proposal.clone(), Instant::now()));
483+
}
484+
Err(e) => {
485+
warn!("{self}: Failed to submit block for validation: {e:?}");
486+
}
487+
};
488+
} else {
489+
// Still store the block but log we can't submit it for validation. We may receive enough signatures/rejections
490+
// from other signers to push the proposed block into a global rejection/acceptance regardless of our participation.
491+
// However, we will not be able to participate beyond this until our block submission times out or we receive a response
492+
// from our node.
493+
warn!("{self}: cannot submit block proposal for validation as we are already waiting for a response for a prior submission")
494+
}
466495

496+
// Do not store KNOWN invalid blocks as this could DOS the signer. We only store blocks that are valid or unknown.
467497
self.signer_db
468498
.insert_block(&block_info)
469499
.unwrap_or_else(|_| panic!("{self}: Failed to insert block in DB"));
@@ -493,6 +523,16 @@ impl Signer {
493523
) -> Option<BlockResponse> {
494524
crate::monitoring::increment_block_validation_responses(true);
495525
let signer_signature_hash = block_validate_ok.signer_signature_hash;
526+
if self
527+
.submitted_block_proposal
528+
.as_ref()
529+
.map(|(proposal, _)| {
530+
proposal.block.header.signer_signature_hash() == signer_signature_hash
531+
})
532+
.unwrap_or(false)
533+
{
534+
self.submitted_block_proposal = None;
535+
}
496536
// For mutability reasons, we need to take the block_info out of the map and add it back after processing
497537
let mut block_info = match self
498538
.signer_db
@@ -542,6 +582,16 @@ impl Signer {
542582
) -> Option<BlockResponse> {
543583
crate::monitoring::increment_block_validation_responses(false);
544584
let signer_signature_hash = block_validate_reject.signer_signature_hash;
585+
if self
586+
.submitted_block_proposal
587+
.as_ref()
588+
.map(|(proposal, _)| {
589+
proposal.block.header.signer_signature_hash() == signer_signature_hash
590+
})
591+
.unwrap_or(false)
592+
{
593+
self.submitted_block_proposal = None;
594+
}
545595
let mut block_info = match self
546596
.signer_db
547597
.block_lookup(self.reward_cycle, &signer_signature_hash)
@@ -617,6 +667,81 @@ impl Signer {
617667
}
618668
}
619669

670+
/// Check the current tracked submitted block proposal to see if it has timed out.
671+
/// Broadcasts a rejection and marks the block locally rejected if it has.
672+
fn check_submitted_block_proposal(&mut self) {
673+
let Some((block_proposal, block_submission)) = self.submitted_block_proposal.take() else {
674+
// Nothing to check.
675+
return;
676+
};
677+
if block_submission.elapsed() < self.block_proposal_validation_timeout {
678+
// Not expired yet. Put it back!
679+
self.submitted_block_proposal = Some((block_proposal, block_submission));
680+
return;
681+
}
682+
let signature_sighash = block_proposal.block.header.signer_signature_hash();
683+
// For mutability reasons, we need to take the block_info out of the map and add it back after processing
684+
let mut block_info = match self
685+
.signer_db
686+
.block_lookup(self.reward_cycle, &signature_sighash)
687+
{
688+
Ok(Some(block_info)) => {
689+
if block_info.state == BlockState::GloballyRejected
690+
|| block_info.state == BlockState::GloballyAccepted
691+
{
692+
// The block has already reached consensus.
693+
return;
694+
}
695+
block_info
696+
}
697+
Ok(None) => {
698+
// This is weird. If this is reached, its probably an error in code logic or the db was flushed.
699+
// Why are we tracking a block submission for a block we have never seen / stored before.
700+
error!("{self}: tracking an unknown block validation submission.";
701+
"signer_sighash" => %signature_sighash,
702+
"block_id" => %block_proposal.block.block_id(),
703+
);
704+
return;
705+
}
706+
Err(e) => {
707+
error!("{self}: Failed to lookup block in signer db: {e:?}",);
708+
return;
709+
}
710+
};
711+
// We cannot determine the validity of the block, but we have not reached consensus on it yet.
712+
// Reject it so we aren't holding up the network because of our inaction.
713+
warn!(
714+
"{self}: Failed to receive block validation response within {} ms. Rejecting block.", self.block_proposal_validation_timeout.as_millis();
715+
"signer_sighash" => %signature_sighash,
716+
"block_id" => %block_proposal.block.block_id(),
717+
);
718+
let rejection = BlockResponse::rejected(
719+
block_proposal.block.header.signer_signature_hash(),
720+
RejectCode::ConnectivityIssues,
721+
&self.private_key,
722+
self.mainnet,
723+
);
724+
if let Err(e) = block_info.mark_locally_rejected() {
725+
warn!("{self}: Failed to mark block as locally rejected: {e:?}",);
726+
};
727+
debug!("{self}: Broadcasting a block response to stacks node: {rejection:?}");
728+
let res = self
729+
.stackerdb
730+
.send_message_with_retry::<SignerMessage>(rejection.into());
731+
732+
match res {
733+
Err(e) => warn!("{self}: Failed to send block rejection to stacker-db: {e:?}"),
734+
Ok(ack) if !ack.accepted => warn!(
735+
"{self}: Block rejection not accepted by stacker-db: {:?}",
736+
ack.reason
737+
),
738+
Ok(_) => debug!("{self}: Block rejection accepted by stacker-db"),
739+
}
740+
self.signer_db
741+
.insert_block(&block_info)
742+
.unwrap_or_else(|_| panic!("{self}: Failed to insert block in DB"));
743+
}
744+
620745
/// Compute the signing weight, given a list of signatures
621746
fn compute_signature_signing_weight<'a>(
622747
&self,
@@ -723,6 +848,15 @@ impl Signer {
723848
error!("{self}: Failed to update block state: {e:?}",);
724849
panic!("{self} Failed to update block state: {e}");
725850
}
851+
if self
852+
.submitted_block_proposal
853+
.as_ref()
854+
.map(|(proposal, _)| &proposal.block.header.signer_signature_hash() == block_hash)
855+
.unwrap_or(false)
856+
{
857+
// Consensus reached! No longer bother tracking its validation submission to the node as we are too late to participate in the decision anyway.
858+
self.submitted_block_proposal = None;
859+
}
726860
}
727861

728862
/// Handle an observed signature from another signer
@@ -865,6 +999,15 @@ impl Signer {
865999
}
8661000
}
8671001
self.broadcast_signed_block(stacks_client, block_info.block, &addrs_to_sigs);
1002+
if self
1003+
.submitted_block_proposal
1004+
.as_ref()
1005+
.map(|(proposal, _)| &proposal.block.header.signer_signature_hash() == block_hash)
1006+
.unwrap_or(false)
1007+
{
1008+
// Consensus reached! No longer bother tracking its validation submission to the node as we are too late to participate in the decision anyway.
1009+
self.submitted_block_proposal = None;
1010+
}
8681011
}
8691012

8701013
fn broadcast_signed_block(

stackslib/src/net/api/postblock_proposal.rs

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,17 @@ impl NakamotoBlockProposal {
343343
sortdb: &SortitionDB,
344344
chainstate: &mut StacksChainState, // not directly used; used as a handle to open other chainstates
345345
) -> Result<BlockValidateOk, BlockValidateRejectReason> {
346+
#[cfg(any(test, feature = "testing"))]
347+
{
348+
if *TEST_VALIDATE_STALL.lock().unwrap() == Some(true) {
349+
// Do an extra check just so we don't log EVERY time.
350+
warn!("Block validation is stalled due to testing directive.");
351+
while *TEST_VALIDATE_STALL.lock().unwrap() == Some(true) {
352+
std::thread::sleep(std::time::Duration::from_millis(10));
353+
}
354+
info!("Block validation is no longer stalled due to testing directive.");
355+
}
356+
}
346357
let ts_start = get_epoch_time_ms();
347358
// Measure time from start of function
348359
let time_elapsed = || get_epoch_time_ms().saturating_sub(ts_start);
@@ -533,24 +544,6 @@ impl NakamotoBlockProposal {
533544
});
534545
}
535546

536-
#[cfg(any(test, feature = "testing"))]
537-
{
538-
if *TEST_VALIDATE_STALL.lock().unwrap() == Some(true) {
539-
// Do an extra check just so we don't log EVERY time.
540-
warn!("Block validation is stalled due to testing directive.";
541-
"block_id" => %block.block_id(),
542-
"height" => block.header.chain_length,
543-
);
544-
while *TEST_VALIDATE_STALL.lock().unwrap() == Some(true) {
545-
std::thread::sleep(std::time::Duration::from_millis(10));
546-
}
547-
info!("Block validation is no longer stalled due to testing directive.";
548-
"block_id" => %block.block_id(),
549-
"height" => block.header.chain_length,
550-
);
551-
}
552-
}
553-
554547
info!(
555548
"Participant: validated anchored block";
556549
"block_header_hash" => %computed_block_header_hash,

0 commit comments

Comments
 (0)