Skip to content

Commit 512669b

Browse files
committed
fix: miner should not count repeat rejections
Before this fix, when the miner is waiting for responses to its block proposal, if it received a block rejection more than once, the second and subsequent rejections would incorrectly count towards the 30% rejection threshold. This could cause the miner to prematurely treat the block as rejected, causing it to build and propose a new block. This change fixes that so that it only counts the response from each signer once.
1 parent e2eacd2 commit 512669b

File tree

3 files changed

+153
-26
lines changed

3 files changed

+153
-26
lines changed

stacks-signer/src/v0/signer.rs

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@
1515
use std::collections::HashMap;
1616
use std::fmt::Debug;
1717
use std::sync::mpsc::Sender;
18+
use std::sync::LazyLock;
1819
use std::time::{Duration, Instant};
1920

2021
use blockstack_lib::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader};
2122
use blockstack_lib::net::api::postblock_proposal::{
2223
BlockValidateOk, BlockValidateReject, BlockValidateResponse, TOO_MANY_REQUESTS_STATUS,
2324
};
2425
use blockstack_lib::util_lib::db::Error as DBError;
25-
use clarity::types::chainstate::StacksPrivateKey;
26+
use clarity::types::chainstate::{StacksPrivateKey, StacksPublicKey};
2627
use clarity::types::{PrivateKey, StacksEpochId};
2728
use clarity::util::hash::{MerkleHashFunc, Sha512Trunc256Sum};
2829
use clarity::util::secp256k1::Secp256k1PublicKey;
30+
use clarity::util::sleep_ms;
31+
use clarity::util::tests::TestFlag;
2932
use libsigner::v0::messages::{
3033
BlockAccepted, BlockRejection, BlockResponse, MessageSlotID, MockProposal, MockSignature,
3134
RejectReason, RejectReasonPrefix, SignerMessage,
@@ -44,6 +47,11 @@ use crate::runloop::SignerResult;
4447
use crate::signerdb::{BlockInfo, BlockState, SignerDb};
4548
use crate::Signer as SignerTrait;
4649

50+
/// A global variable that can be used to make signers repeat their proposal
51+
/// response if their public key is in the provided list
52+
pub static TEST_REPEAT_PROPOSAL_RESPONSE: LazyLock<TestFlag<Vec<StacksPublicKey>>> =
53+
LazyLock::new(TestFlag::default);
54+
4755
/// Signer running mode (whether dry-run or real)
4856
#[derive(Debug)]
4957
pub enum SignerMode {
@@ -464,6 +472,49 @@ impl Signer {
464472
}
465473
}
466474

475+
#[cfg(any(test, feature = "testing"))]
476+
fn send_block_response(&mut self, block_response: BlockResponse) {
477+
const NUM_REPEATS: usize = 1;
478+
let mut count = 0;
479+
let public_keys = TEST_REPEAT_PROPOSAL_RESPONSE.get();
480+
if !public_keys.contains(
481+
&stacks_common::types::chainstate::StacksPublicKey::from_private(&self.private_key),
482+
) {
483+
count = NUM_REPEATS;
484+
}
485+
while count <= NUM_REPEATS {
486+
let res = self
487+
.stackerdb
488+
.send_message_with_retry::<SignerMessage>(block_response.clone().into());
489+
match res {
490+
Err(e) => warn!("{self}: Failed to send block rejection to stacker-db: {e:?}"),
491+
Ok(ack) if !ack.accepted => warn!(
492+
"{self}: Block rejection not accepted by stacker-db: {:?}",
493+
ack.reason
494+
),
495+
Ok(_) => debug!("{self}: Block rejection accepted by stacker-db"),
496+
}
497+
498+
count += 1;
499+
sleep_ms(1000);
500+
}
501+
}
502+
503+
#[cfg(not(any(test, feature = "testing")))]
504+
fn send_block_response(&mut self, block_response: BlockResponse) {
505+
let res = self
506+
.stackerdb
507+
.send_message_with_retry::<SignerMessage>(block_response.clone().into());
508+
match res {
509+
Err(e) => warn!("{self}: Failed to send block rejection to stacker-db: {e:?}"),
510+
Ok(ack) if !ack.accepted => warn!(
511+
"{self}: Block rejection not accepted by stacker-db: {:?}",
512+
ack.reason
513+
),
514+
Ok(_) => debug!("{self}: Block rejection accepted by stacker-db"),
515+
}
516+
}
517+
467518
/// Handle block proposal messages submitted to signers stackerdb
468519
fn handle_block_proposal(
469520
&mut self,
@@ -575,18 +626,7 @@ impl Signer {
575626
if let Some(block_response) = block_response {
576627
// We know proposal is invalid. Send rejection message, do not do further validation and do not store it.
577628
debug!("{self}: Broadcasting a block response to stacks node: {block_response:?}");
578-
let res = self
579-
.stackerdb
580-
.send_message_with_retry::<SignerMessage>(block_response.into());
581-
582-
match res {
583-
Err(e) => warn!("{self}: Failed to send block rejection to stacker-db: {e:?}"),
584-
Ok(ack) if !ack.accepted => warn!(
585-
"{self}: Block rejection not accepted by stacker-db: {:?}",
586-
ack.reason
587-
),
588-
Ok(_) => debug!("{self}: Block rejection accepted by stacker-db"),
589-
}
629+
self.send_block_response(block_response);
590630
} else {
591631
// Just in case check if the last block validation submission timed out.
592632
self.check_submitted_block_proposal();

testnet/stacks-node/src/nakamoto_node/stackerdb_listener.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -384,11 +384,13 @@ impl StackerDBListener {
384384
continue;
385385
}
386386
};
387-
block.responded_signers.insert(rejected_pubkey);
388-
block.total_weight_rejected = block
389-
.total_weight_rejected
390-
.checked_add(signer_entry.weight)
391-
.expect("FATAL: total weight rejected exceeds u32::MAX");
387+
388+
if block.responded_signers.insert(rejected_pubkey) {
389+
block.total_weight_rejected = block
390+
.total_weight_rejected
391+
.checked_add(signer_entry.weight)
392+
.expect("FATAL: total weight rejected exceeds u32::MAX");
393+
}
392394

393395
info!("StackerDBListener: Signer rejected block";
394396
"block_signer_sighash" => %rejected_data.signer_signature_hash,

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

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ use stacks_signer::chainstate::{ProposalEvalConfig, SortitionsView};
6868
use stacks_signer::client::{SignerSlotID, StackerDB};
6969
use stacks_signer::config::{build_signer_config_tomls, GlobalConfig as SignerConfig, Network};
7070
use stacks_signer::signerdb::SignerDb;
71+
use stacks_signer::v0::signer::TEST_REPEAT_PROPOSAL_RESPONSE;
7172
use stacks_signer::v0::tests::{
7273
TEST_IGNORE_ALL_BLOCK_PROPOSALS, TEST_PAUSE_BLOCK_BROADCAST, TEST_REJECT_ALL_BLOCK_PROPOSAL,
7374
TEST_SKIP_BLOCK_BROADCAST, TEST_SKIP_SIGNER_CLEANUP, TEST_STALL_BLOCK_VALIDATION_SUBMISSION,
@@ -3038,12 +3039,13 @@ fn retry_on_rejection() {
30383039
submit_tx(&http_origin, &transfer_tx);
30393040

30403041
info!("Submitted transfer tx and waiting for block proposal");
3041-
loop {
3042+
wait_for(60, || {
30423043
if proposed_blocks.load(Ordering::SeqCst) > proposals_before {
3043-
break;
3044+
return Ok(true);
30443045
}
3045-
std::thread::sleep(Duration::from_millis(100));
3046-
}
3046+
Ok(false)
3047+
})
3048+
.expect("Timed out waiting for block proposal");
30473049

30483050
info!("Block proposed, verifying that it is not processed");
30493051
// Wait 10 seconds to be sure that the timeout has occurred
@@ -3053,12 +3055,15 @@ fn retry_on_rejection() {
30533055
// resume signing
30543056
info!("Disable unconditional rejection and wait for the block to be processed");
30553057
TEST_REJECT_ALL_BLOCK_PROPOSAL.set(vec![]);
3056-
loop {
3058+
3059+
wait_for(60, || {
30573060
if mined_blocks.load(Ordering::SeqCst) > blocks_before {
3058-
break;
3061+
return Ok(true);
30593062
}
3060-
std::thread::sleep(Duration::from_millis(100));
3061-
}
3063+
Ok(false)
3064+
})
3065+
.expect("Timed out waiting for block to be mined");
3066+
30623067
signer_test.shutdown();
30633068
}
30643069

@@ -12085,3 +12090,83 @@ fn mark_miner_as_invalid_if_reorg_is_rejected() {
1208512090
}
1208612091
miners.shutdown();
1208712092
}
12093+
12094+
#[test]
12095+
#[ignore]
12096+
/// This test checks that the miner ignore repeat block rejections.
12097+
fn repeated_rejection() {
12098+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
12099+
return;
12100+
}
12101+
tracing_subscriber::registry()
12102+
.with(fmt::layer())
12103+
.with(EnvFilter::from_default_env())
12104+
.init();
12105+
12106+
info!("------------------------- Test Setup -------------------------");
12107+
let num_signers = 5;
12108+
let sender_sk = Secp256k1PrivateKey::random();
12109+
let sender_addr = tests::to_addr(&sender_sk);
12110+
let send_amt = 100;
12111+
let send_fee = 180;
12112+
let recipient = PrincipalData::from(StacksAddress::burn_address(false));
12113+
let mut signer_test: SignerTest<SpawnedSigner> =
12114+
SignerTest::new(num_signers, vec![(sender_addr, (send_amt + send_fee) * 3)]);
12115+
let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
12116+
signer_test.boot_to_epoch_3();
12117+
12118+
let proposed_blocks = signer_test
12119+
.running_nodes
12120+
.counters
12121+
.naka_proposed_blocks
12122+
.clone();
12123+
12124+
signer_test.mine_nakamoto_block(Duration::from_secs(60), true);
12125+
12126+
// make signer[0] reject all proposals and to repeat the rejection
12127+
let rejecting_signer =
12128+
StacksPublicKey::from_private(&signer_test.signer_stacks_private_keys[0]);
12129+
TEST_REJECT_ALL_BLOCK_PROPOSAL.set(vec![rejecting_signer]);
12130+
TEST_REPEAT_PROPOSAL_RESPONSE.set(vec![rejecting_signer]);
12131+
12132+
// make signer[1] ignore all proposals
12133+
let ignoring_signer = StacksPublicKey::from_private(&signer_test.signer_stacks_private_keys[1]);
12134+
TEST_IGNORE_ALL_BLOCK_PROPOSALS.set(vec![ignoring_signer]);
12135+
12136+
let proposals_before = proposed_blocks.load(Ordering::SeqCst);
12137+
12138+
// submit a tx so that the miner will mine a block
12139+
let transfer_tx = make_stacks_transfer(
12140+
&sender_sk,
12141+
0,
12142+
send_fee,
12143+
signer_test.running_nodes.conf.burnchain.chain_id,
12144+
&recipient,
12145+
send_amt,
12146+
);
12147+
submit_tx(&http_origin, &transfer_tx);
12148+
12149+
info!("Submitted transfer tx and waiting for block proposal");
12150+
wait_for(60, || {
12151+
if proposed_blocks.load(Ordering::SeqCst) > proposals_before {
12152+
return Ok(true);
12153+
}
12154+
Ok(false)
12155+
})
12156+
.expect("Timed out waiting for block proposal");
12157+
12158+
let proposals_after = proposed_blocks.load(Ordering::SeqCst);
12159+
info!("Block proposed, verifying that it is not rejected");
12160+
12161+
// Ensure that the miner does not propose any more blocks
12162+
_ = wait_for(60, || {
12163+
assert_eq!(
12164+
proposed_blocks.load(Ordering::SeqCst),
12165+
proposals_after,
12166+
"Miner proposed another block"
12167+
);
12168+
Ok(false)
12169+
});
12170+
12171+
signer_test.shutdown();
12172+
}

0 commit comments

Comments
 (0)