Skip to content

Commit 9161e04

Browse files
committed
Track the last submitted block proposal and do not submit a new one if we already are processing one
Signed-off-by: Jacinta Ferrant <[email protected]>
1 parent f23ffb6 commit 9161e04

File tree

8 files changed

+161
-8
lines changed

8 files changed

+161
-8
lines changed

stacks-signer/src/chainstate.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,16 @@ pub struct ProposalEvalConfig {
119119
pub first_proposal_burn_block_timing: Duration,
120120
/// Time between processing a sortition and proposing a block before the block is considered invalid
121121
pub block_proposal_timeout: Duration,
122+
/// How long to wait for a block proposal validation response
123+
pub block_proposal_validation_timeout: Duration,
122124
}
123125

124126
impl From<&SignerConfig> for ProposalEvalConfig {
125127
fn from(value: &SignerConfig) -> Self {
126128
Self {
127129
first_proposal_burn_block_timing: value.first_proposal_burn_block_timing,
128130
block_proposal_timeout: value.block_proposal_timeout,
131+
block_proposal_validation_timeout: value.block_proposal_validation_timeout,
129132
}
130133
}
131134
}

stacks-signer/src/client/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ pub(crate) mod tests {
411411
db_path: config.db_path.clone(),
412412
first_proposal_burn_block_timing: config.first_proposal_burn_block_timing,
413413
block_proposal_timeout: config.block_proposal_timeout,
414+
block_proposal_validation_timeout: config.block_proposal_validation_timeout,
414415
}
415416
}
416417

stacks-signer/src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ pub struct SignerConfig {
129129
pub first_proposal_burn_block_timing: Duration,
130130
/// How much time to wait for a miner to propose a block following a sortition
131131
pub block_proposal_timeout: Duration,
132+
/// How much time ot wait for a block proposal validation response before marking the block invalid
133+
pub block_proposal_validation_timeout: Duration,
132134
}
133135

134136
/// The parsed configuration for the signer

stacks-signer/src/runloop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug> RunLo
283283
mainnet: self.config.network.is_mainnet(),
284284
db_path: self.config.db_path.clone(),
285285
block_proposal_timeout: self.config.block_proposal_timeout,
286+
block_proposal_validation_timeout: self.config.block_proposal_validation_timeout,
286287
}))
287288
}
288289

stacks-signer/src/tests/chainstate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ fn setup_test_environment(
8989
config: ProposalEvalConfig {
9090
first_proposal_burn_block_timing: Duration::from_secs(30),
9191
block_proposal_timeout: Duration::from_secs(5),
92+
block_proposal_validation_timeout: Duration::from_secs(60),
9293
},
9394
};
9495

stacks-signer/src/v0/signer.rs

Lines changed: 149 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::Instant;
1819

1920
use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader};
2021
use blockstack_lib::net::api::postblock_proposal::{
@@ -85,6 +86,8 @@ pub struct Signer {
8586
pub signer_db: SignerDb,
8687
/// Configuration for proposal evaluation
8788
pub proposal_config: ProposalEvalConfig,
89+
/// The current submitted block proposal and its submission time
90+
pub submitted_block_proposal: Option<(BlockProposal, Instant)>,
8891
}
8992

9093
impl std::fmt::Display for Signer {
@@ -127,6 +130,7 @@ impl SignerTrait<SignerMessage> for Signer {
127130
if event_parity == Some(other_signer_parity) {
128131
return;
129132
}
133+
self.check_submitted_block_proposal();
130134
debug!("{self}: Processing event: {event:?}");
131135
let Some(event) = event else {
132136
// No event. Do nothing.
@@ -274,6 +278,7 @@ impl From<SignerConfig> for Signer {
274278
reward_cycle: signer_config.reward_cycle,
275279
signer_db,
276280
proposal_config,
281+
submitted_block_proposal: None,
277282
}
278283
}
279284
}
@@ -355,7 +360,7 @@ impl Signer {
355360
}
356361

357362
info!(
358-
"{self}: received a block proposal for a new block. Submit block for validation. ";
363+
"{self}: received a block proposal for a new block.";
359364
"signer_sighash" => %signer_signature_hash,
360365
"block_id" => %block_proposal.block.block_id(),
361366
"block_height" => block_proposal.block.header.chain_length,
@@ -456,14 +461,35 @@ impl Signer {
456461
Ok(_) => debug!("{self}: Block rejection accepted by stacker-db"),
457462
}
458463
} 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-
});
464+
// Just in case check if the last block validation submission timed out.
465+
self.check_submitted_block_proposal();
466+
if self.submitted_block_proposal.is_none() {
467+
// We don't know if proposal is valid, submit to stacks-node for further checks and store it locally.
468+
info!(
469+
"{self}: submitting block proposal for validation";
470+
"signer_sighash" => %signer_signature_hash,
471+
"block_id" => %block_proposal.block.block_id(),
472+
"block_height" => block_proposal.block.header.chain_length,
473+
"burn_height" => block_proposal.burn_height,
474+
);
475+
match stacks_client.submit_block_for_validation(block_info.block.clone()) {
476+
Ok(_) => {
477+
self.submitted_block_proposal =
478+
Some((block_proposal.clone(), Instant::now()));
479+
}
480+
Err(e) => {
481+
warn!("{self}: Failed to submit block for validation: {e:?}");
482+
}
483+
};
484+
} else {
485+
// Still store the block but log we can't submit it for validation. We may receive enough signatures/rejections
486+
// from other signers to push the proposed block into a global rejection/acceptance regardless of our participation.
487+
// However, we will not be able to participate beyond this until our block submission times out or we receive a response
488+
// from our node.
489+
warn!("{self}: cannot submit block proposal for validation as we are already waiting for a response for a prior submission")
490+
}
466491

492+
// Do not store KNOWN invalid blocks as this could DOS the signer. We only store blocks that are valid or unknown.
467493
self.signer_db
468494
.insert_block(&block_info)
469495
.unwrap_or_else(|_| panic!("{self}: Failed to insert block in DB"));
@@ -493,6 +519,16 @@ impl Signer {
493519
) -> Option<BlockResponse> {
494520
crate::monitoring::increment_block_validation_responses(true);
495521
let signer_signature_hash = block_validate_ok.signer_signature_hash;
522+
if self
523+
.submitted_block_proposal
524+
.as_ref()
525+
.map(|(proposal, _)| {
526+
proposal.block.header.signer_signature_hash() == signer_signature_hash
527+
})
528+
.unwrap_or(false)
529+
{
530+
self.submitted_block_proposal = None;
531+
}
496532
// For mutability reasons, we need to take the block_info out of the map and add it back after processing
497533
let mut block_info = match self
498534
.signer_db
@@ -542,6 +578,16 @@ impl Signer {
542578
) -> Option<BlockResponse> {
543579
crate::monitoring::increment_block_validation_responses(false);
544580
let signer_signature_hash = block_validate_reject.signer_signature_hash;
581+
if self
582+
.submitted_block_proposal
583+
.as_ref()
584+
.map(|(proposal, _)| {
585+
proposal.block.header.signer_signature_hash() == signer_signature_hash
586+
})
587+
.unwrap_or(false)
588+
{
589+
self.submitted_block_proposal = None;
590+
}
545591
let mut block_info = match self
546592
.signer_db
547593
.block_lookup(self.reward_cycle, &signer_signature_hash)
@@ -617,6 +663,85 @@ impl Signer {
617663
}
618664
}
619665

666+
/// Check the current tracked submitted block proposal to see if it has timed out.
667+
/// Broadcasts a rejection and marks the block locally rejected if it has.
668+
fn check_submitted_block_proposal(&mut self) {
669+
let Some((block_proposal, block_submission)) = self.submitted_block_proposal.clone() else {
670+
// Nothing to check.
671+
return;
672+
};
673+
if block_submission.elapsed() < self.proposal_config.block_proposal_validation_timeout {
674+
// Not expired yet.
675+
return;
676+
}
677+
// Let us immediately flush, even if we later encounter an error broadcasting our responses.
678+
// We should still attempt to handle a new proposal at this point.
679+
self.submitted_block_proposal = None;
680+
let signature_sighash = block_proposal.block.header.signer_signature_hash();
681+
// For mutability reasons, we need to take the block_info out of the map and add it back after processing
682+
let mut block_info = match self
683+
.signer_db
684+
.block_lookup(self.reward_cycle, &signature_sighash)
685+
{
686+
Ok(Some(block_info)) => {
687+
if block_info.state == BlockState::GloballyRejected
688+
|| block_info.state == BlockState::GloballyAccepted
689+
{
690+
// The block has already reached consensus. This should never really hit, but check just to be safe.
691+
self.submitted_block_proposal = None;
692+
return;
693+
}
694+
block_info
695+
}
696+
Ok(None) => {
697+
// This is weird. If this is reached, its probably an error in code logic or the db was flushed.
698+
// Why are we tracking a block submission for a block we have never seen / stored before.
699+
error!("{self}: tracking an unknown block validation submission.";
700+
"signer_sighash" => %signature_sighash,
701+
"block_id" => %block_proposal.block.block_id(),
702+
);
703+
self.submitted_block_proposal = None;
704+
return;
705+
}
706+
Err(e) => {
707+
error!("{self}: Failed to lookup block in signer db: {e:?}",);
708+
self.submitted_block_proposal = None;
709+
return;
710+
}
711+
};
712+
warn!(
713+
"{self}: Failed to receive block validation response within {} ms. Rejecting block.", self.proposal_config.block_proposal_validation_timeout.as_millis();
714+
"signer_sighash" => %signature_sighash,
715+
"block_id" => %block_proposal.block.block_id(),
716+
);
717+
let rejection = BlockResponse::rejected(
718+
block_proposal.block.header.signer_signature_hash(),
719+
RejectCode::ConnectivityIssues,
720+
&self.private_key,
721+
self.mainnet,
722+
);
723+
// We know proposal is invalid. Send rejection message, do not do further validation
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,14 @@ 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+
self.submitted_block_proposal = None;
858+
}
726859
}
727860

728861
/// Handle an observed signature from another signer
@@ -865,6 +998,14 @@ impl Signer {
865998
}
866999
}
8671000
self.broadcast_signed_block(stacks_client, block_info.block, &addrs_to_sigs);
1001+
if self
1002+
.submitted_block_proposal
1003+
.as_ref()
1004+
.map(|(proposal, _)| &proposal.block.header.signer_signature_hash() == block_hash)
1005+
.unwrap_or(false)
1006+
{
1007+
self.submitted_block_proposal = None;
1008+
}
8681009
}
8691010

8701011
fn broadcast_signed_block(

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6450,6 +6450,7 @@ fn signer_chainstate() {
64506450
let proposal_conf = ProposalEvalConfig {
64516451
first_proposal_burn_block_timing: Duration::from_secs(0),
64526452
block_proposal_timeout: Duration::from_secs(100),
6453+
block_proposal_validation_timeout: Duration::from_secs(100),
64536454
};
64546455
let mut sortitions_view =
64556456
SortitionsView::fetch_view(proposal_conf, &signer_client).unwrap();
@@ -6588,6 +6589,7 @@ fn signer_chainstate() {
65886589
let proposal_conf = ProposalEvalConfig {
65896590
first_proposal_burn_block_timing: Duration::from_secs(0),
65906591
block_proposal_timeout: Duration::from_secs(100),
6592+
block_proposal_validation_timeout: Duration::from_secs(100),
65916593
};
65926594
let burn_block_height = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
65936595
.unwrap()
@@ -6665,6 +6667,7 @@ fn signer_chainstate() {
66656667
let proposal_conf = ProposalEvalConfig {
66666668
first_proposal_burn_block_timing: Duration::from_secs(0),
66676669
block_proposal_timeout: Duration::from_secs(100),
6670+
block_proposal_validation_timeout: Duration::from_secs(100),
66686671
};
66696672
let mut sortitions_view = SortitionsView::fetch_view(proposal_conf, &signer_client).unwrap();
66706673
let burn_block_height = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())

testnet/stacks-node/src/tests/signer/v0.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ fn block_proposal_rejection() {
456456
let proposal_conf = ProposalEvalConfig {
457457
first_proposal_burn_block_timing: Duration::from_secs(0),
458458
block_proposal_timeout: Duration::from_secs(100),
459+
block_proposal_validation_timeout: Duration::from_secs(100),
459460
};
460461
let mut block = NakamotoBlock {
461462
header: NakamotoBlockHeader::empty(),

0 commit comments

Comments
 (0)