Skip to content

Commit 443ae92

Browse files
committed
Fix reorging_signers_capitulate_to_nonreorging_signers_during_tenure_fork and mark_miner_as_invalid_if_reorg_is_rejected
Signed-off-by: Jacinta Ferrant <[email protected]>
1 parent 1644a3c commit 443ae92

File tree

1 file changed

+215
-72
lines changed
  • testnet/stacks-node/src/tests/signer

1 file changed

+215
-72
lines changed

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

Lines changed: 215 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -12046,10 +12046,10 @@ fn mark_miner_as_invalid_if_reorg_is_rejected() {
1204612046
.expect("Failed to get block proposal N+1'");
1204712047
// Stall the miner from proposing again until we're ready
1204812048
TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_1]);
12049-
12049+
// Due to reorging signers capitulating to the majority rejection of the reorg...all signers will update their state to reject
1205012050
miners
1205112051
.signer_test
12052-
.check_signer_states_reorg(&approving_signers, &rejecting_signers);
12052+
.check_signer_states_reorg(&[], &all_signers);
1205312053

1205412054
info!("------------------------- Wait for 3 acceptances and 2 rejections -------------------------");
1205512055
let signer_signature_hash = block_n_1_prime.header.signer_signature_hash();
@@ -13391,81 +13391,194 @@ fn signers_send_state_message_updates() {
1339113391

1339213392
#[test]
1339313393
#[ignore]
13394-
fn reorging_capitulate_to_nonreorging_signers_during_tenure_fork() {
13394+
fn reorging_signers_capitulate_to_nonreorging_signers_during_tenure_fork() {
1339513395
if env::var("BITCOIND_TEST") != Ok("1".into()) {
1339613396
return;
1339713397
}
1339813398

13399-
let result = forked_tenure_testing(
13400-
Duration::from_secs(360),
13401-
Some(Duration::from_secs(5)),
13402-
Duration::from_secs(7),
13403-
false,
13404-
);
13399+
let num_signers = 5;
13400+
let num_txs = 5;
1340513401

13406-
assert_ne!(
13407-
result.tip_b.index_block_hash(),
13408-
result.tip_a.index_block_hash()
13409-
);
13410-
assert_eq!(
13411-
result.tip_b.index_block_hash(),
13412-
result.tip_c.index_block_hash()
13402+
let disallow_reorg_proposal_timeout = Duration::from_secs(10);
13403+
let allow_reorg_proposal_timeout = Duration::from_secs(360);
13404+
let post_btc_block_pause =
13405+
disallow_reorg_proposal_timeout.saturating_add(Duration::from_secs(1));
13406+
let mut miners = MultipleMinerTest::new_with_config_modifications(
13407+
num_signers,
13408+
num_txs,
13409+
|config| {
13410+
config.first_proposal_burn_block_timing = if config.endpoint.port() % 2 == 1 {
13411+
// 2/5 or 40% of signers will allow the reorg
13412+
allow_reorg_proposal_timeout
13413+
} else {
13414+
// 3/5 or 60% of signers will reject the reorg
13415+
disallow_reorg_proposal_timeout
13416+
};
13417+
// don't allow signers to post signed blocks (limits the amount of fault injection we
13418+
// need)
13419+
TEST_SKIP_BLOCK_BROADCAST.set(true);
13420+
},
13421+
|config| {
13422+
config.burnchain.pox_reward_length = Some(30);
13423+
config.miner.tenure_cost_limit_per_block_percentage = None;
13424+
// this test relies on the miner submitting these timed out commits.
13425+
// the test still passes without this override, but the default timeout
13426+
// makes the test take longer than strictly necessary
13427+
config.miner.block_commit_delay = Duration::from_secs(10);
13428+
},
13429+
|_| {},
1341313430
);
13414-
assert_ne!(result.tip_c, result.tip_a);
13431+
let rl1_skip_commit_op = miners
13432+
.signer_test
13433+
.running_nodes
13434+
.counters
13435+
.naka_skip_commit_op
13436+
.clone();
13437+
13438+
let rl2_skip_commit_op = miners.rl2_counters.naka_skip_commit_op.clone();
13439+
13440+
let (conf_1, _) = miners.get_node_configs();
13441+
let (miner_pkh_1, miner_pkh_2) = miners.get_miner_public_key_hashes();
13442+
let (miner_pk_1, miner_pk_2) = miners.get_miner_public_keys();
13443+
13444+
info!("------------------------- Pause Miner 2's Block Commits -------------------------");
13445+
13446+
// Make sure Miner 2 cannot win a sortition at first.
13447+
rl2_skip_commit_op.set(true);
13448+
13449+
miners.boot_to_epoch_3();
13450+
13451+
let burnchain = conf_1.get_burnchain();
13452+
let sortdb = burnchain.open_sortition_db(true).unwrap();
13453+
let (chainstate, _) = StacksChainState::open(
13454+
conf_1.is_mainnet(),
13455+
conf_1.burnchain.chain_id,
13456+
&conf_1.get_chainstate_path_str(),
13457+
None,
13458+
)
13459+
.unwrap();
13460+
info!("------------------------- Pause Miner 1's Block Commit -------------------------");
13461+
13462+
// Make sure miner 1 doesn't submit any further block commits for the next tenure BEFORE mining the bitcoin block
13463+
rl1_skip_commit_op.set(true);
13464+
13465+
info!("------------------------- Miner 1 Wins Normal Tenure A -------------------------");
13466+
miners
13467+
.mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30)
13468+
.expect("Failed to mine BTC block followed by tenure change tx");
13469+
verify_sortition_winner(&sortdb, &miner_pkh_1);
13470+
13471+
info!("------------------------- Miner 1 Mines Another Block -------------------------");
13472+
miners
13473+
.send_and_mine_transfer_tx(30)
13474+
.expect("Failed to mine tx");
13475+
13476+
let tip_a = NakamotoChainState::get_canonical_block_header(chainstate.db(), &sortdb)
13477+
.unwrap()
13478+
.unwrap();
13479+
13480+
info!("------------------------- Pause Block Proposals -------------------------");
13481+
// For the next tenure, submit the commit op but do not allow any stacks blocks to be broadcasted
13482+
TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_1]);
13483+
TEST_BLOCK_ANNOUNCE_STALL.set(true);
13484+
13485+
miners.submit_commit_miner_1(&sortdb);
13486+
13487+
info!("------------------------- Miner 1 Wins Tenure B -------------------------");
13488+
miners
13489+
.mine_bitcoin_blocks_and_confirm(&sortdb, 1, 30)
13490+
.expect("Failed to mine BTC block");
13491+
// assure we have a successful sortition that miner 1 won
13492+
verify_sortition_winner(&sortdb, &miner_pkh_1);
13493+
13494+
info!("----------------- Miner 2 Submits Block Commit for Tenure C Before Any Tenure B Blocks Produced ------------------");
13495+
miners.submit_commit_miner_2(&sortdb);
13496+
13497+
info!("----------------------------- Resume Block Production for Tenure B -----------------------------");
13498+
13499+
let stacks_height_before = miners.get_peer_stacks_tip_height();
13500+
TEST_BROADCAST_PROPOSAL_STALL.set(vec![]);
13501+
13502+
let tenure_b_block_proposal =
13503+
wait_for_block_proposal(30, stacks_height_before + 1, &miner_pk_1)
13504+
.expect("Timed out waiting for Tenure B block to be proposed");
13505+
info!("Tenure B broadcasted a block. Wait {post_btc_block_pause:?}, issue the next bitcoin block, and un-stall block commits.");
13506+
thread::sleep(post_btc_block_pause);
13507+
13508+
// the block will be stored, not processed, so load it out of staging
13509+
let tip_sn = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
13510+
.expect("Failed to get sortition tip");
13511+
13512+
let tenure_b_block = chainstate
13513+
.nakamoto_blocks_db()
13514+
.get_nakamoto_tenure_start_blocks(&tip_sn.consensus_hash)
13515+
.unwrap()
13516+
.first()
13517+
.cloned()
13518+
.unwrap();
13519+
13520+
// synthesize a StacksHeaderInfo from this unprocessed block
13521+
let tip_b = StacksHeaderInfo {
13522+
anchored_header: StacksBlockHeaderTypes::Nakamoto(tenure_b_block.header.clone()),
13523+
microblock_tail: None,
13524+
stacks_block_height: tenure_b_block.header.chain_length,
13525+
index_root: TrieHash([0x00; 32]), // we can't know this yet since the block hasn't been processed
13526+
consensus_hash: tenure_b_block.header.consensus_hash,
13527+
burn_header_hash: tip_sn.burn_header_hash,
13528+
burn_header_height: tip_sn.block_height as u32,
13529+
burn_header_timestamp: tip_sn.burn_header_timestamp,
13530+
anchored_block_size: tenure_b_block.serialize_to_vec().len() as u64,
13531+
burn_view: Some(tenure_b_block.header.consensus_hash),
13532+
};
1341513533

1341613534
// Block B was built atop block A
13535+
assert_ne!(tip_b.index_block_hash(), tip_a.index_block_hash());
13536+
assert_eq!(tip_b.stacks_block_height, tip_a.stacks_block_height + 1);
1341713537
assert_eq!(
13418-
result.tip_b.stacks_block_height,
13419-
result.tip_a.stacks_block_height + 1
13420-
);
13421-
assert_eq!(
13422-
result.mined_b.parent_block_id,
13423-
result.tip_a.index_block_hash().to_string()
13538+
tenure_b_block.header.parent_block_id,
13539+
tip_a.index_block_hash()
1342413540
);
13541+
assert_ne!(tip_b, tip_a);
1342513542

13426-
// Block C was built AFTER Block B was built, but BEFORE it was broadcasted, so it should be built off of Block A
13427-
assert_eq!(
13428-
result.mined_c.parent_block_id,
13429-
result.tip_a.index_block_hash().to_string()
13430-
);
13431-
assert_ne!(
13432-
result
13433-
.tip_c
13434-
.anchored_header
13435-
.as_stacks_nakamoto()
13436-
.unwrap()
13437-
.signer_signature_hash(),
13438-
result.mined_c.signer_signature_hash,
13439-
"Mined block during tenure C should not have become the chain tip"
13440-
);
13543+
let chain_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
13544+
let burn_height_before = chain_tip.block_height;
1344113545

13442-
assert!(result.tip_c_2.is_none());
13443-
assert!(result.mined_c_2.is_none());
13546+
// allow B to process, so it'll be distinct from C
13547+
TEST_BLOCK_ANNOUNCE_STALL.set(false);
13548+
sleep_ms(1000);
1344413549

13445-
// Tenure D should continue progress
13446-
assert_ne!(result.tip_c, result.tip_d);
13447-
assert_ne!(
13448-
result.tip_b.index_block_hash(),
13449-
result.tip_d.index_block_hash()
13450-
);
13451-
assert_ne!(result.tip_a, result.tip_d);
13550+
info!("--------------- Miner 2 Wins Tenure C With Old Block Commit ----------------");
13551+
info!("Prevent Miner 1 from extending at first");
13552+
TEST_BROADCAST_PROPOSAL_STALL.set(vec![miner_pk_1]);
13553+
13554+
test_observer::clear();
13555+
13556+
miners
13557+
.mine_bitcoin_blocks_and_confirm(&sortdb, 1, 60)
13558+
.expect("Failed to mine bitcoin block");
13559+
// assure we have a successful sortition that miner 2
13560+
verify_sortition_winner(&sortdb, &miner_pkh_2);
13561+
13562+
// Note tenure C block will attempt to reorg the prior miner so its expected height should be the same as prior to block B processing.
13563+
let tenure_c_block_proposal =
13564+
wait_for_block_proposal(30, tip_b.stacks_block_height, &miner_pk_2)
13565+
.expect("Timed out waiting for miner 2's Tenure C block");
13566+
13567+
assert_ne!(tenure_c_block_proposal, tenure_b_block_proposal);
13568+
13569+
let tip_c = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
1345213570

13453-
// Tenure D builds off of Tenure B
13454-
assert_eq!(
13455-
result.tip_d.stacks_block_height,
13456-
result.tip_b.stacks_block_height + 1,
13457-
);
1345813571
assert_eq!(
13459-
result.mined_d.parent_block_id,
13460-
result.tip_b.index_block_hash().to_string()
13572+
tip_b.index_block_hash(),
13573+
tip_c.get_canonical_stacks_block_id()
1346113574
);
13575+
assert_ne!(tip_c.consensus_hash, tip_a.consensus_hash);
13576+
assert_ne!(tip_c.burn_header_hash, tip_a.burn_header_hash);
13577+
assert_eq!(tip_c.block_height, burn_height_before + 1);
1346213578

13463-
// We should see all signers rejecting the forked block
13464-
wait_for_block_global_rejection(60, result.mined_c.signer_signature_hash, 5).unwrap();
13465-
13466-
wait_for(60, || {
13579+
wait_for(30, || {
13580+
let mut nmb_matches = 0;
1346713581
let stackerdb_events = test_observer::get_stackerdb_chunks();
13468-
let mut collected = HashSet::new();
1346913582
for chunk in stackerdb_events
1347013583
.into_iter()
1347113584
.flat_map(|chunk| chunk.modified_slots)
@@ -13480,26 +13593,56 @@ fn reorging_capitulate_to_nonreorging_signers_during_tenure_fork() {
1348013593
burn_block_height,
1348113594
current_miner:
1348213595
StateMachineUpdateMinerState::ActiveMiner {
13483-
tenure_id,
13484-
parent_tenure_id,
13485-
..
13596+
current_miner_pkh, ..
1348613597
},
13487-
} = &update.content
13598+
..
13599+
} = update.content
1348813600
else {
1348913601
continue;
1349013602
};
13491-
if *burn_block_height != result.tip_c.burn_header_height as u64
13492-
|| *burn_block != result.tip_c.consensus_hash
13493-
|| *tenure_id != result.tip_d.consensus_hash
13494-
|| *parent_tenure_id != result.tip_a.consensus_hash
13603+
if burn_block == tenure_c_block_proposal.header.consensus_hash
13604+
&& burn_block_height == burn_height_before + 1
13605+
&& current_miner_pkh == miner_pkh_1
1349513606
{
13496-
continue;
13607+
nmb_matches += 1;
1349713608
}
13498-
collected.insert(chunk.sig);
1349913609
}
13500-
Ok(collected.len() == 5)
13610+
Ok(nmb_matches == 5)
1350113611
})
13502-
.expect(
13503-
"Timed out waiting for the expected updates from signers to arrive due to capitulation",
13612+
.unwrap();
13613+
13614+
info!("--------------- Miner 1 Extends Tenure B over Tenure C ---------------");
13615+
TEST_BROADCAST_PROPOSAL_STALL.set(vec![]);
13616+
let tenure_extend_block =
13617+
wait_for_block_proposal(30, tip_b.stacks_block_height + 1, &miner_pk_1)
13618+
.expect("Timed out waiting for miner 1's tenure extend block");
13619+
wait_for_block_acceptance_from_signers(
13620+
3,
13621+
&tenure_extend_block.header.signer_signature_hash(),
13622+
&miners.signer_test.signer_test_pks(),
13623+
)
13624+
.expect("Expected all signers to accept the extend");
13625+
13626+
info!("------------------------- Miner 1 Mines Another Block -------------------------");
13627+
miners
13628+
.send_and_mine_transfer_tx(30)
13629+
.expect("Failed to mine tx");
13630+
13631+
info!("------------------------- Miner 2 Mines the Next Tenure -------------------------");
13632+
miners.submit_commit_miner_2(&sortdb);
13633+
13634+
miners
13635+
.mine_bitcoin_block_and_tenure_change_tx(&sortdb, TenureChangeCause::BlockFound, 30)
13636+
.expect("Failed to mine BTC block followed by tenure change tx");
13637+
13638+
// assure we have a successful sortition that miner 2 won and it had a block found tenure change
13639+
verify_sortition_winner(&sortdb, &miner_pkh_2);
13640+
13641+
miners.shutdown();
13642+
13643+
// Block C was built AFTER Block B was built, but BEFORE it was broadcasted, so it should be built off of Block A
13644+
assert_eq!(
13645+
tenure_c_block_proposal.header.parent_block_id,
13646+
tip_a.index_block_hash()
1350413647
);
1350513648
}

0 commit comments

Comments
 (0)