Skip to content

Commit b21f156

Browse files
committed
feat: stop waiting for signatures if the chain tip advances
This could happen at the beginning of a tenure, if the miner for tenure T+1 proposes its first block before it sees the last block from tenure T, so the block it proposed is reorging that last block. Once it sees this block, it should immediately stop waiting for signatures and instead build a new block off of the new tip. Fixes: #5751
1 parent 6651b31 commit b21f156

File tree

6 files changed

+522
-33
lines changed

6 files changed

+522
-33
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ jobs:
151151
- tests::signer::v0::block_proposal_timeout
152152
- tests::signer::v0::rejected_blocks_count_towards_miner_validity
153153
- tests::signer::v0::allow_reorg_within_first_proposal_burn_block_timing_secs
154+
- tests::signer::v0::interrupt_miner_on_new_stacks_tip
154155
- tests::nakamoto_integrations::burn_ops_integration_test
155156
- tests::nakamoto_integrations::check_block_heights
156157
- tests::nakamoto_integrations::clarity_burn_state

stacks-signer/src/v0/signer.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ impl SignerTrait<SignerMessage> for Signer {
207207
"block_height" => b.header.chain_length,
208208
"signer_sighash" => %b.header.signer_signature_hash(),
209209
);
210+
#[cfg(any(test, feature = "testing"))]
211+
if self.test_skip_block_broadcast(b) {
212+
return;
213+
}
210214
stacks_client.post_block_until_ok(self, b);
211215
}
212216
SignerMessage::MockProposal(mock_proposal) => {

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

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,20 @@ use crate::run_loop::RegisteredKey;
6565
pub static TEST_MINE_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
6666
#[cfg(test)]
6767
/// Test flag to stall block proposal broadcasting
68-
pub static TEST_BROADCAST_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
68+
pub static TEST_BROADCAST_PROPOSAL_STALL: LazyLock<TestFlag<bool>> =
69+
LazyLock::new(TestFlag::default);
6970
#[cfg(test)]
71+
// Test flag to stall the miner from announcing a block while this flag is true
7072
pub static TEST_BLOCK_ANNOUNCE_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
7173
#[cfg(test)]
72-
pub static TEST_SKIP_P2P_BROADCAST: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
74+
// Test flag to skip broadcasting blocks over the p2p network
75+
pub static TEST_P2P_BROADCAST_SKIP: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
76+
#[cfg(test)]
77+
// Test flag to stall broadcasting blocks over the p2p network
78+
pub static TEST_P2P_BROADCAST_STALL: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
79+
#[cfg(test)]
80+
// Test flag to skip pushing blocks to the signers
81+
pub static TEST_BLOCK_PUSH_SKIP: LazyLock<TestFlag<bool>> = LazyLock::new(TestFlag::default);
7382

7483
/// If the miner was interrupted while mining a block, how long should the
7584
/// miner thread sleep before trying again?
@@ -252,19 +261,19 @@ impl BlockMinerThread {
252261
}
253262

254263
#[cfg(test)]
255-
fn fault_injection_block_broadcast_stall(new_block: &NakamotoBlock) {
256-
if TEST_BROADCAST_STALL.get() {
264+
fn fault_injection_block_proposal_stall(new_block: &NakamotoBlock) {
265+
if TEST_BROADCAST_PROPOSAL_STALL.get() {
257266
// Do an extra check just so we don't log EVERY time.
258-
warn!("Fault injection: Broadcasting is stalled due to testing directive.";
267+
warn!("Fault injection: Block proposal broadcast is stalled due to testing directive.";
259268
"stacks_block_id" => %new_block.block_id(),
260269
"stacks_block_hash" => %new_block.header.block_hash(),
261270
"height" => new_block.header.chain_length,
262271
"consensus_hash" => %new_block.header.consensus_hash
263272
);
264-
while TEST_BROADCAST_STALL.get() {
273+
while TEST_BROADCAST_PROPOSAL_STALL.get() {
265274
std::thread::sleep(std::time::Duration::from_millis(10));
266275
}
267-
info!("Fault injection: Broadcasting is no longer stalled due to testing directive.";
276+
info!("Fault injection: Block proposal broadcast is no longer stalled due to testing directive.";
268277
"block_id" => %new_block.block_id(),
269278
"height" => new_block.header.chain_length,
270279
"consensus_hash" => %new_block.header.consensus_hash
@@ -273,7 +282,7 @@ impl BlockMinerThread {
273282
}
274283

275284
#[cfg(not(test))]
276-
fn fault_injection_block_broadcast_stall(_ignored: &NakamotoBlock) {}
285+
fn fault_injection_block_proposal_stall(_ignored: &NakamotoBlock) {}
277286

278287
#[cfg(test)]
279288
fn fault_injection_block_announce_stall(new_block: &NakamotoBlock) {
@@ -301,17 +310,48 @@ impl BlockMinerThread {
301310

302311
#[cfg(test)]
303312
fn fault_injection_skip_block_broadcast() -> bool {
304-
if TEST_SKIP_P2P_BROADCAST.get() {
305-
return true;
306-
}
307-
false
313+
TEST_P2P_BROADCAST_SKIP.get()
308314
}
309315

310316
#[cfg(not(test))]
311317
fn fault_injection_skip_block_broadcast() -> bool {
312318
false
313319
}
314320

321+
#[cfg(test)]
322+
fn fault_injection_block_broadcast_stall(new_block: &NakamotoBlock) {
323+
if TEST_P2P_BROADCAST_STALL.get() {
324+
// Do an extra check just so we don't log EVERY time.
325+
warn!("Fault injection: P2P block broadcast is stalled due to testing directive.";
326+
"stacks_block_id" => %new_block.block_id(),
327+
"stacks_block_hash" => %new_block.header.block_hash(),
328+
"height" => new_block.header.chain_length,
329+
"consensus_hash" => %new_block.header.consensus_hash
330+
);
331+
while TEST_P2P_BROADCAST_STALL.get() {
332+
std::thread::sleep(std::time::Duration::from_millis(10));
333+
}
334+
info!("Fault injection: P2P block broadcast is no longer stalled due to testing directive.";
335+
"block_id" => %new_block.block_id(),
336+
"height" => new_block.header.chain_length,
337+
"consensus_hash" => %new_block.header.consensus_hash
338+
);
339+
}
340+
}
341+
342+
#[cfg(not(test))]
343+
fn fault_injection_block_broadcast_stall(_ignored: &NakamotoBlock) {}
344+
345+
#[cfg(test)]
346+
fn fault_injection_skip_block_push() -> bool {
347+
TEST_BLOCK_PUSH_SKIP.get()
348+
}
349+
350+
#[cfg(not(test))]
351+
fn fault_injection_skip_block_push() -> bool {
352+
false
353+
}
354+
315355
/// Stop a miner tenure by blocking the miner and then joining the tenure thread
316356
#[cfg(test)]
317357
fn fault_injection_stall_miner() {
@@ -516,7 +556,7 @@ impl BlockMinerThread {
516556
};
517557

518558
if let Some(mut new_block) = new_block {
519-
Self::fault_injection_block_broadcast_stall(&new_block);
559+
Self::fault_injection_block_proposal_stall(&new_block);
520560

521561
let signer_signature = match self.propose_block(
522562
coordinator,
@@ -532,7 +572,7 @@ impl BlockMinerThread {
532572
"block_height" => new_block.header.chain_length,
533573
"consensus_hash" => %new_block.header.consensus_hash,
534574
);
535-
return Err(e);
575+
return Ok(());
536576
}
537577
NakamotoNodeError::BurnchainTipChanged => {
538578
info!("Burnchain tip changed while waiting for signatures";
@@ -739,6 +779,7 @@ impl BlockMinerThread {
739779
);
740780
return Ok(());
741781
}
782+
Self::fault_injection_block_broadcast_stall(block);
742783

743784
let parent_block_info =
744785
NakamotoChainState::get_block_header(chain_state.db(), &block.header.parent_block_id)?
@@ -834,6 +875,14 @@ impl BlockMinerThread {
834875
let miners_contract_id = boot_code_id(MINERS_NAME, chain_state.mainnet);
835876
let mut miners_session = StackerDBSession::new(&rpc_socket.to_string(), miners_contract_id);
836877

878+
if Self::fault_injection_skip_block_push() {
879+
warn!(
880+
"Fault injection: Skipping block push for {}",
881+
block.block_id()
882+
);
883+
return Ok(());
884+
}
885+
837886
SignerCoordinator::send_miners_message(
838887
miner_privkey,
839888
&sort_db,

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ impl SignerCoordinator {
288288
self.get_block_status(
289289
&block.header.signer_signature_hash(),
290290
&block.block_id(),
291+
block.header.parent_block_id,
291292
chain_state,
292293
sortdb,
293294
counters,
@@ -303,6 +304,7 @@ impl SignerCoordinator {
303304
&self,
304305
block_signer_sighash: &Sha512Trunc256Sum,
305306
block_id: &StacksBlockId,
307+
parent_block_id: StacksBlockId,
306308
chain_state: &mut StacksChainState,
307309
sortdb: &SortitionDB,
308310
counters: &Counters,
@@ -384,6 +386,17 @@ impl SignerCoordinator {
384386
));
385387
}
386388

389+
// Check if a new Stacks block has arrived
390+
let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
391+
SortitionDB::get_canonical_stacks_chain_tip_hash(sortdb.conn()).unwrap();
392+
let canonical_stacks_tip =
393+
StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh);
394+
info!("SignCoordinator: Current stacks tip: {canonical_stacks_tip}, parent: {parent_block_id}");
395+
if canonical_stacks_tip != parent_block_id {
396+
debug!("SignCoordinator: Exiting due to new stacks tip");
397+
return Err(NakamotoNodeError::StacksTipChanged);
398+
}
399+
387400
continue;
388401
}
389402
};

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ use stacks_signer::v0::SpawnedSigner;
9797

9898
use super::bitcoin_regtest::BitcoinCoreController;
9999
use crate::nakamoto_node::miner::{
100-
TEST_BLOCK_ANNOUNCE_STALL, TEST_BROADCAST_STALL, TEST_MINE_STALL, TEST_SKIP_P2P_BROADCAST,
100+
TEST_BLOCK_ANNOUNCE_STALL, TEST_BROADCAST_PROPOSAL_STALL, TEST_MINE_STALL,
101+
TEST_P2P_BROADCAST_SKIP,
101102
};
102103
use crate::nakamoto_node::relayer::{RelayerThread, TEST_MINER_THREAD_STALL};
103104
use crate::neon::{Counters, RunLoopCounter};
@@ -5186,7 +5187,7 @@ fn forked_tenure_is_ignored() {
51865187

51875188
// For the next tenure, submit the commit op but do not allow any stacks blocks to be broadcasted.
51885189
// Stall the miner thread; only wait until the number of submitted commits increases.
5189-
TEST_BROADCAST_STALL.set(true);
5190+
TEST_BROADCAST_PROPOSAL_STALL.set(true);
51905191
TEST_BLOCK_ANNOUNCE_STALL.set(true);
51915192

51925193
let blocks_before = mined_blocks.load(Ordering::SeqCst);
@@ -5205,7 +5206,7 @@ fn forked_tenure_is_ignored() {
52055206
// Unpause the broadcast of Tenure B's block, do not submit commits, and do not allow blocks to
52065207
// be processed
52075208
test_skip_commit_op.set(true);
5208-
TEST_BROADCAST_STALL.set(false);
5209+
TEST_BROADCAST_PROPOSAL_STALL.set(false);
52095210

52105211
// Wait for a stacks block to be broadcasted.
52115212
// However, it will not be processed.
@@ -9876,7 +9877,7 @@ fn skip_mining_long_tx() {
98769877
})
98779878
.unwrap();
98789879

9879-
TEST_SKIP_P2P_BROADCAST.set(true);
9880+
TEST_P2P_BROADCAST_SKIP.set(true);
98809881
let tx = make_contract_publish(
98819882
&sender_2_sk,
98829883
0,
@@ -9903,7 +9904,7 @@ fn skip_mining_long_tx() {
99039904
})
99049905
.unwrap();
99059906

9906-
TEST_SKIP_P2P_BROADCAST.set(false);
9907+
TEST_P2P_BROADCAST_SKIP.set(false);
99079908
} else {
99089909
let transfer_tx = make_stacks_transfer(
99099910
&sender_1_sk,

0 commit comments

Comments
 (0)