Skip to content

Commit 4c25a90

Browse files
committed
test: add allow_reorg_within_first_proposal_burn_block_timing_secs
1 parent aab9162 commit 4c25a90

File tree

2 files changed

+375
-0
lines changed

2 files changed

+375
-0
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ jobs:
139139
- tests::signer::v0::incoming_signers_ignore_block_proposals
140140
- tests::signer::v0::outgoing_signers_ignore_block_proposals
141141
- tests::signer::v0::injected_signatures_are_ignored_across_boundaries
142+
- tests::signer::v0::allow_reorg_within_first_proposal_burn_block_timing_secs
142143
- tests::nakamoto_integrations::burn_ops_integration_test
143144
- tests::nakamoto_integrations::check_block_heights
144145
- tests::nakamoto_integrations::clarity_burn_state

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

Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10815,3 +10815,377 @@ fn injected_signatures_are_ignored_across_boundaries() {
1081510815

1081610816
assert!(new_spawned_signer.stop().is_none());
1081710817
}
10818+
10819+
/// Test a scenario where:
10820+
/// Two miners boot to Nakamoto.
10821+
/// Sortition occurs. Miner 1 wins.
10822+
/// Miner 1 proposes a block N
10823+
/// Signers accept and the stacks tip advances to N
10824+
/// Miner 1's block commits are paused so it cannot confirm the next tenure.
10825+
/// Sortition occurs. Miner 2 wins.
10826+
/// Miner 2 proposes block N+1
10827+
/// Signers accept and the stacks tip advances to N+1
10828+
/// Sortition occurs quickly, within first_proposal_burn_block_timing_secs. Miner 1 wins.
10829+
/// Miner 1 proposes block N+1'
10830+
/// Signers approve N+1', saying "Miner is not building off of most recent tenure. A tenure they
10831+
/// reorg has already mined blocks, but the block was poorly timed, allowing the reorg."
10832+
/// Miner 1 proposes N+2 and it is accepted.
10833+
/// Asserts:
10834+
/// - N+1 is signed and broadcasted
10835+
/// - N+1' is signed and broadcasted
10836+
/// - The tip advances to N+1 (Signed by Miner 1)
10837+
/// - The tip advances to N+2 (Signed by Miner 1)
10838+
#[test]
10839+
#[ignore]
10840+
fn allow_reorg_within_first_proposal_burn_block_timing_secs() {
10841+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
10842+
return;
10843+
}
10844+
10845+
let num_signers = 5;
10846+
let recipient = PrincipalData::from(StacksAddress::burn_address(false));
10847+
let sender_sk = Secp256k1PrivateKey::new();
10848+
let sender_addr = tests::to_addr(&sender_sk);
10849+
let send_amt = 100;
10850+
let send_fee = 180;
10851+
let num_txs = 1;
10852+
10853+
let btc_miner_1_seed = vec![1, 1, 1, 1];
10854+
let btc_miner_2_seed = vec![2, 2, 2, 2];
10855+
let btc_miner_1_pk = Keychain::default(btc_miner_1_seed.clone()).get_pub_key();
10856+
let btc_miner_2_pk = Keychain::default(btc_miner_2_seed.clone()).get_pub_key();
10857+
10858+
let node_1_rpc = gen_random_port();
10859+
let node_1_p2p = gen_random_port();
10860+
let node_2_rpc = gen_random_port();
10861+
let node_2_p2p = gen_random_port();
10862+
10863+
let localhost = "127.0.0.1";
10864+
let node_1_rpc_bind = format!("{localhost}:{node_1_rpc}");
10865+
let node_2_rpc_bind = format!("{localhost}:{node_2_rpc}");
10866+
let mut node_2_listeners = Vec::new();
10867+
10868+
let max_nakamoto_tenures = 30;
10869+
10870+
info!("------------------------- Test Setup -------------------------");
10871+
// partition the signer set so that ~half are listening and using node 1 for RPC and events,
10872+
// and the rest are using node 2
10873+
10874+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
10875+
num_signers,
10876+
vec![(sender_addr, (send_amt + send_fee) * num_txs)],
10877+
|signer_config| {
10878+
// Lets make sure we never time out since we need to stall some things to force our scenario
10879+
signer_config.block_proposal_validation_timeout = Duration::from_secs(1800);
10880+
signer_config.tenure_last_block_proposal_timeout = Duration::from_secs(1800);
10881+
signer_config.first_proposal_burn_block_timing = Duration::from_secs(1800);
10882+
let node_host = if signer_config.endpoint.port() % 2 == 0 {
10883+
&node_1_rpc_bind
10884+
} else {
10885+
&node_2_rpc_bind
10886+
};
10887+
signer_config.node_host = node_host.to_string();
10888+
},
10889+
|config| {
10890+
config.node.rpc_bind = format!("{localhost}:{node_1_rpc}");
10891+
config.node.p2p_bind = format!("{localhost}:{node_1_p2p}");
10892+
config.node.data_url = format!("http://{localhost}:{node_1_rpc}");
10893+
config.node.p2p_address = format!("{localhost}:{node_1_p2p}");
10894+
config.miner.wait_on_interim_blocks = Duration::from_secs(5);
10895+
config.node.pox_sync_sample_secs = 30;
10896+
config.burnchain.pox_reward_length = Some(max_nakamoto_tenures);
10897+
10898+
config.node.seed = btc_miner_1_seed.clone();
10899+
config.node.local_peer_seed = btc_miner_1_seed.clone();
10900+
config.burnchain.local_mining_public_key = Some(btc_miner_1_pk.to_hex());
10901+
config.miner.mining_key = Some(Secp256k1PrivateKey::from_seed(&[1]));
10902+
10903+
config.events_observers.retain(|listener| {
10904+
let Ok(addr) = std::net::SocketAddr::from_str(&listener.endpoint) else {
10905+
warn!(
10906+
"Cannot parse {} to a socket, assuming it isn't a signer-listener binding",
10907+
listener.endpoint
10908+
);
10909+
return true;
10910+
};
10911+
if addr.port() % 2 == 0 || addr.port() == test_observer::EVENT_OBSERVER_PORT {
10912+
return true;
10913+
}
10914+
node_2_listeners.push(listener.clone());
10915+
false
10916+
})
10917+
},
10918+
Some(vec![btc_miner_1_pk, btc_miner_2_pk]),
10919+
None,
10920+
);
10921+
let conf = signer_test.running_nodes.conf.clone();
10922+
let mut conf_node_2 = conf.clone();
10923+
conf_node_2.node.rpc_bind = format!("{localhost}:{node_2_rpc}");
10924+
conf_node_2.node.p2p_bind = format!("{localhost}:{node_2_p2p}");
10925+
conf_node_2.node.data_url = format!("http://{localhost}:{node_2_rpc}");
10926+
conf_node_2.node.p2p_address = format!("{localhost}:{node_2_p2p}");
10927+
conf_node_2.node.seed = btc_miner_2_seed.clone();
10928+
conf_node_2.burnchain.local_mining_public_key = Some(btc_miner_2_pk.to_hex());
10929+
conf_node_2.node.local_peer_seed = btc_miner_2_seed.clone();
10930+
conf_node_2.miner.mining_key = Some(Secp256k1PrivateKey::from_seed(&[2]));
10931+
conf_node_2.node.miner = true;
10932+
conf_node_2.events_observers.clear();
10933+
conf_node_2.events_observers.extend(node_2_listeners);
10934+
assert!(!conf_node_2.events_observers.is_empty());
10935+
10936+
let node_1_sk = Secp256k1PrivateKey::from_seed(&conf.node.local_peer_seed);
10937+
let node_1_pk = StacksPublicKey::from_private(&node_1_sk);
10938+
10939+
conf_node_2.node.working_dir = format!("{}-1", conf_node_2.node.working_dir);
10940+
10941+
conf_node_2.node.set_bootstrap_nodes(
10942+
format!("{}@{}", &node_1_pk.to_hex(), conf.node.p2p_bind),
10943+
conf.burnchain.chain_id,
10944+
conf.burnchain.peer_version,
10945+
);
10946+
let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
10947+
10948+
let mut run_loop_2 = boot_nakamoto::BootRunLoop::new(conf_node_2.clone()).unwrap();
10949+
let run_loop_stopper_2 = run_loop_2.get_termination_switch();
10950+
let rl2_coord_channels = run_loop_2.coordinator_channels();
10951+
let Counters {
10952+
naka_submitted_commits: rl2_commits,
10953+
naka_skip_commit_op: rl2_skip_commit_op,
10954+
naka_mined_blocks: blocks_mined2,
10955+
..
10956+
} = run_loop_2.counters();
10957+
10958+
let blocks_mined1 = signer_test.running_nodes.nakamoto_blocks_mined.clone();
10959+
let rl1_commits = signer_test.running_nodes.commits_submitted.clone();
10960+
10961+
info!("------------------------- Pause Miner 2's Block Commits -------------------------");
10962+
10963+
// Make sure Miner 2 cannot win a sortition at first.
10964+
rl2_skip_commit_op.set(true);
10965+
10966+
info!("------------------------- Boot to Epoch 3.0 -------------------------");
10967+
10968+
let run_loop_2_thread = thread::Builder::new()
10969+
.name("run_loop_2".into())
10970+
.spawn(move || run_loop_2.start(None, 0))
10971+
.unwrap();
10972+
10973+
signer_test.boot_to_epoch_3();
10974+
10975+
wait_for(120, || {
10976+
let Some(node_1_info) = get_chain_info_opt(&conf) else {
10977+
return Ok(false);
10978+
};
10979+
let Some(node_2_info) = get_chain_info_opt(&conf_node_2) else {
10980+
return Ok(false);
10981+
};
10982+
Ok(node_1_info.stacks_tip_height == node_2_info.stacks_tip_height)
10983+
})
10984+
.expect("Timed out waiting for boostrapped node to catch up to the miner");
10985+
10986+
let mining_pk_1 = StacksPublicKey::from_private(&conf.miner.mining_key.unwrap());
10987+
let mining_pk_2 = StacksPublicKey::from_private(&conf_node_2.miner.mining_key.unwrap());
10988+
let mining_pkh_1 = Hash160::from_node_public_key(&mining_pk_1);
10989+
let mining_pkh_2 = Hash160::from_node_public_key(&mining_pk_2);
10990+
debug!("The mining key for miner 1 is {mining_pkh_1}");
10991+
debug!("The mining key for miner 2 is {mining_pkh_2}");
10992+
10993+
info!("------------------------- Reached Epoch 3.0 -------------------------");
10994+
10995+
let burnchain = signer_test.running_nodes.conf.get_burnchain();
10996+
let sortdb = burnchain.open_sortition_db(true).unwrap();
10997+
10998+
let get_burn_height = || {
10999+
SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
11000+
.unwrap()
11001+
.block_height
11002+
};
11003+
let starting_burn_height = get_burn_height();
11004+
11005+
info!("------------------------- Pause Miner 1's Block Commits -------------------------");
11006+
signer_test
11007+
.running_nodes
11008+
.nakamoto_test_skip_commit_op
11009+
.set(true);
11010+
11011+
info!("------------------------- Miner 1 Mines a Nakamoto Block N (Globally Accepted) -------------------------");
11012+
let blocks_processed_before_1 = blocks_mined1.load(Ordering::SeqCst);
11013+
let stacks_height_before = signer_test
11014+
.stacks_client
11015+
.get_peer_info()
11016+
.expect("Failed to get peer info")
11017+
.stacks_tip_height;
11018+
let info_before = get_chain_info(&conf);
11019+
let mined_before = test_observer::get_mined_nakamoto_blocks().len();
11020+
11021+
next_block_and(
11022+
&mut signer_test.running_nodes.btc_regtest_controller,
11023+
30,
11024+
|| {
11025+
Ok(get_burn_height() > starting_burn_height
11026+
&& signer_test
11027+
.stacks_client
11028+
.get_peer_info()
11029+
.expect("Failed to get peer info")
11030+
.stacks_tip_height
11031+
> stacks_height_before
11032+
&& blocks_mined1.load(Ordering::SeqCst) > blocks_processed_before_1
11033+
&& get_chain_info(&conf).stacks_tip_height > info_before.stacks_tip_height
11034+
&& test_observer::get_mined_nakamoto_blocks().len() > mined_before)
11035+
},
11036+
)
11037+
.expect("Timed out waiting for Miner 1 to Mine Block N");
11038+
11039+
let blocks = test_observer::get_mined_nakamoto_blocks();
11040+
let block_n = blocks.last().unwrap().clone();
11041+
let block_n_signature_hash = block_n.signer_signature_hash;
11042+
11043+
let info_after = get_chain_info(&conf);
11044+
assert_eq!(info_after.stacks_tip.to_string(), block_n.block_hash);
11045+
assert_eq!(block_n.signer_signature_hash, block_n_signature_hash);
11046+
assert_eq!(
11047+
info_after.stacks_tip_height,
11048+
info_before.stacks_tip_height + 1
11049+
);
11050+
11051+
// assure we have a successful sortition that miner 1 won
11052+
let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
11053+
assert!(tip.sortition);
11054+
assert_eq!(tip.miner_pk_hash.unwrap(), mining_pkh_1);
11055+
11056+
debug!("Miner 1 mined block N: {block_n_signature_hash}");
11057+
11058+
info!("------------------------- Miner 2 Submits a Block Commit -------------------------");
11059+
let rl2_commits_before = rl2_commits.load(Ordering::SeqCst);
11060+
rl2_skip_commit_op.set(false);
11061+
11062+
wait_for(30, || {
11063+
Ok(rl2_commits.load(Ordering::SeqCst) > rl2_commits_before)
11064+
})
11065+
.expect("Timed out waiting for Miner 2 to submit its block commit");
11066+
11067+
rl2_skip_commit_op.set(true);
11068+
11069+
info!("------------------------- Pause Miner 2's Block Mining -------------------------");
11070+
TEST_MINE_STALL.lock().unwrap().replace(true);
11071+
11072+
let burn_height_before = get_chain_info(&signer_test.running_nodes.conf).burn_block_height;
11073+
11074+
info!("------------------------- Mine Tenure -------------------------");
11075+
signer_test
11076+
.running_nodes
11077+
.btc_regtest_controller
11078+
.build_next_block(1);
11079+
11080+
wait_for(60, || {
11081+
let info = get_chain_info(&signer_test.running_nodes.conf);
11082+
Ok(info.burn_block_height > burn_height_before)
11083+
})
11084+
.expect("Failed to advance chain tip");
11085+
11086+
info!("------------------------- Miner 1 Submits a Block Commit -------------------------");
11087+
let rl1_commits_before = rl1_commits.load(Ordering::SeqCst);
11088+
signer_test
11089+
.running_nodes
11090+
.nakamoto_test_skip_commit_op
11091+
.set(false);
11092+
11093+
wait_for(30, || {
11094+
Ok(rl1_commits.load(Ordering::SeqCst) > rl1_commits_before)
11095+
})
11096+
.expect("Timed out waiting for Miner 1 to submit its block commit");
11097+
signer_test
11098+
.running_nodes
11099+
.nakamoto_test_skip_commit_op
11100+
.set(true);
11101+
11102+
info!("------------------------- Miner 2 Mines Block N + 1 -------------------------");
11103+
let blocks_processed_before_2 = blocks_mined2.load(Ordering::SeqCst);
11104+
let stacks_height_before = signer_test
11105+
.stacks_client
11106+
.get_peer_info()
11107+
.expect("Failed to get peer info")
11108+
.stacks_tip_height;
11109+
let info_before = get_chain_info(&conf);
11110+
11111+
TEST_MINE_STALL.lock().unwrap().replace(false);
11112+
11113+
wait_for(30, || {
11114+
Ok(signer_test
11115+
.stacks_client
11116+
.get_peer_info()
11117+
.expect("Failed to get peer info")
11118+
.stacks_tip_height
11119+
> stacks_height_before
11120+
&& blocks_mined2.load(Ordering::SeqCst) > blocks_processed_before_2
11121+
&& get_chain_info(&conf).stacks_tip_height > info_before.stacks_tip_height)
11122+
})
11123+
.expect("Timed out waiting for Miner 2 to Mine Block N + 1");
11124+
11125+
// assure we have a successful sortition that miner 2 won
11126+
let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
11127+
assert!(tip.sortition);
11128+
assert_eq!(tip.miner_pk_hash.unwrap(), mining_pkh_2);
11129+
11130+
info!("------------------------- Miner 1 Wins the Next Tenure -------------------------");
11131+
11132+
let blocks_processed_before_1 = blocks_mined1.load(Ordering::SeqCst);
11133+
let mined_before = test_observer::get_mined_nakamoto_blocks().len();
11134+
11135+
next_block_and(
11136+
&mut signer_test.running_nodes.btc_regtest_controller,
11137+
30,
11138+
|| {
11139+
Ok(
11140+
blocks_mined1.load(Ordering::SeqCst) > blocks_processed_before_1
11141+
&& test_observer::get_mined_nakamoto_blocks().len() > mined_before,
11142+
)
11143+
},
11144+
)
11145+
.expect("Timed out waiting for Miner 1 to Mine Block N+1'");
11146+
11147+
info!("------------------------- Miner 1 Mines N+2 -------------------------");
11148+
11149+
let blocks_processed_before_1 = blocks_mined1.load(Ordering::SeqCst);
11150+
let stacks_height_before = signer_test
11151+
.stacks_client
11152+
.get_peer_info()
11153+
.expect("Failed to get peer info")
11154+
.stacks_tip_height;
11155+
let info_before = get_chain_info(&conf);
11156+
let mined_before = test_observer::get_mined_nakamoto_blocks().len();
11157+
11158+
// submit a tx so that the miner will ATTEMPT to mine a stacks block N
11159+
let transfer_tx = make_stacks_transfer(
11160+
&sender_sk,
11161+
0,
11162+
send_fee,
11163+
signer_test.running_nodes.conf.burnchain.chain_id,
11164+
&recipient,
11165+
send_amt,
11166+
);
11167+
let tx = submit_tx(&http_origin, &transfer_tx);
11168+
info!("Submitted tx {tx} in attempt to mine block N+2");
11169+
11170+
wait_for(30, || {
11171+
Ok(signer_test
11172+
.stacks_client
11173+
.get_peer_info()
11174+
.expect("Failed to get peer info")
11175+
.stacks_tip_height
11176+
> stacks_height_before
11177+
&& blocks_mined1.load(Ordering::SeqCst) > blocks_processed_before_1
11178+
&& get_chain_info(&conf).stacks_tip_height > info_before.stacks_tip_height
11179+
&& test_observer::get_mined_nakamoto_blocks().len() > mined_before)
11180+
})
11181+
.expect("Timed out waiting for Miner 1 to Mine Block N+2");
11182+
11183+
info!("------------------------- Shutdown -------------------------");
11184+
rl2_coord_channels
11185+
.lock()
11186+
.expect("Mutex poisoned")
11187+
.stop_chains_coordinator();
11188+
run_loop_stopper_2.store(false, Ordering::SeqCst);
11189+
run_loop_2_thread.join().unwrap();
11190+
signer_test.shutdown();
11191+
}

0 commit comments

Comments
 (0)