|
15 | 15 | use std::collections::HashMap;
|
16 | 16 | use std::fmt::Debug;
|
17 | 17 | use std::sync::mpsc::Sender;
|
| 18 | +use std::time::{Duration, Instant}; |
18 | 19 |
|
19 | 20 | use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader};
|
20 | 21 | use blockstack_lib::net::api::postblock_proposal::{
|
@@ -85,6 +86,11 @@ pub struct Signer {
|
85 | 86 | pub signer_db: SignerDb,
|
86 | 87 | /// Configuration for proposal evaluation
|
87 | 88 | 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)>, |
88 | 94 | }
|
89 | 95 |
|
90 | 96 | impl std::fmt::Display for Signer {
|
@@ -127,6 +133,7 @@ impl SignerTrait<SignerMessage> for Signer {
|
127 | 133 | if event_parity == Some(other_signer_parity) {
|
128 | 134 | return;
|
129 | 135 | }
|
| 136 | + self.check_submitted_block_proposal(); |
130 | 137 | debug!("{self}: Processing event: {event:?}");
|
131 | 138 | let Some(event) = event else {
|
132 | 139 | // No event. Do nothing.
|
@@ -274,6 +281,8 @@ impl From<SignerConfig> for Signer {
|
274 | 281 | reward_cycle: signer_config.reward_cycle,
|
275 | 282 | signer_db,
|
276 | 283 | proposal_config,
|
| 284 | + submitted_block_proposal: None, |
| 285 | + block_proposal_validation_timeout: signer_config.block_proposal_validation_timeout, |
277 | 286 | }
|
278 | 287 | }
|
279 | 288 | }
|
@@ -355,7 +364,7 @@ impl Signer {
|
355 | 364 | }
|
356 | 365 |
|
357 | 366 | 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."; |
359 | 368 | "signer_sighash" => %signer_signature_hash,
|
360 | 369 | "block_id" => %block_proposal.block.block_id(),
|
361 | 370 | "block_height" => block_proposal.block.header.chain_length,
|
@@ -456,14 +465,35 @@ impl Signer {
|
456 | 465 | Ok(_) => debug!("{self}: Block rejection accepted by stacker-db"),
|
457 | 466 | }
|
458 | 467 | } 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 | + } |
466 | 495 |
|
| 496 | + // Do not store KNOWN invalid blocks as this could DOS the signer. We only store blocks that are valid or unknown. |
467 | 497 | self.signer_db
|
468 | 498 | .insert_block(&block_info)
|
469 | 499 | .unwrap_or_else(|_| panic!("{self}: Failed to insert block in DB"));
|
@@ -493,6 +523,16 @@ impl Signer {
|
493 | 523 | ) -> Option<BlockResponse> {
|
494 | 524 | crate::monitoring::increment_block_validation_responses(true);
|
495 | 525 | 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 | + } |
496 | 536 | // For mutability reasons, we need to take the block_info out of the map and add it back after processing
|
497 | 537 | let mut block_info = match self
|
498 | 538 | .signer_db
|
@@ -542,6 +582,16 @@ impl Signer {
|
542 | 582 | ) -> Option<BlockResponse> {
|
543 | 583 | crate::monitoring::increment_block_validation_responses(false);
|
544 | 584 | 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 | + } |
545 | 595 | let mut block_info = match self
|
546 | 596 | .signer_db
|
547 | 597 | .block_lookup(self.reward_cycle, &signer_signature_hash)
|
@@ -617,6 +667,81 @@ impl Signer {
|
617 | 667 | }
|
618 | 668 | }
|
619 | 669 |
|
| 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 | + |
620 | 745 | /// Compute the signing weight, given a list of signatures
|
621 | 746 | fn compute_signature_signing_weight<'a>(
|
622 | 747 | &self,
|
@@ -723,6 +848,15 @@ impl Signer {
|
723 | 848 | error!("{self}: Failed to update block state: {e:?}",);
|
724 | 849 | panic!("{self} Failed to update block state: {e}");
|
725 | 850 | }
|
| 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 | + } |
726 | 860 | }
|
727 | 861 |
|
728 | 862 | /// Handle an observed signature from another signer
|
@@ -865,6 +999,15 @@ impl Signer {
|
865 | 999 | }
|
866 | 1000 | }
|
867 | 1001 | 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 | + } |
868 | 1011 | }
|
869 | 1012 |
|
870 | 1013 | fn broadcast_signed_block(
|
|
0 commit comments