Skip to content

Commit eae2ce2

Browse files
committed
test: additional scenarios for empty sortition tenure extends
1 parent a335dcd commit eae2ce2

File tree

2 files changed

+339
-3
lines changed

2 files changed

+339
-3
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ jobs:
9999
- tests::signer::v0::forked_tenure_okay
100100
- tests::signer::v0::forked_tenure_invalid
101101
- tests::signer::v0::empty_sortition
102+
- tests::signer::v0::empty_sortition_before_approval
103+
- tests::signer::v0::empty_sortition_before_proposal
102104
- tests::signer::v0::bitcoind_forking_test
103105
- tests::signer::v0::multiple_miners
104106
- tests::signer::v0::mock_sign_epoch_25

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

Lines changed: 337 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,16 @@ use tracing_subscriber::{fmt, EnvFilter};
6666
use super::SignerTest;
6767
use crate::config::{EventKeyType, EventObserverConfig};
6868
use crate::event_dispatcher::MinedNakamotoBlockEvent;
69-
use crate::nakamoto_node::miner::{TEST_BLOCK_ANNOUNCE_STALL, TEST_BROADCAST_STALL};
69+
use crate::nakamoto_node::miner::{
70+
TEST_BLOCK_ANNOUNCE_STALL, TEST_BROADCAST_STALL, TEST_MINE_STALL,
71+
};
7072
use crate::nakamoto_node::sign_coordinator::TEST_IGNORE_SIGNERS;
7173
use crate::neon::Counters;
7274
use crate::run_loop::boot_nakamoto;
7375
use crate::tests::nakamoto_integrations::{
7476
boot_to_epoch_25, boot_to_epoch_3_reward_set, next_block_and, next_block_and_controller,
75-
setup_epoch_3_reward_set, wait_for, POX_4_DEFAULT_STACKER_BALANCE,
76-
POX_4_DEFAULT_STACKER_STX_AMT,
77+
next_block_and_process_new_stacks_block, setup_epoch_3_reward_set, wait_for,
78+
POX_4_DEFAULT_STACKER_BALANCE, POX_4_DEFAULT_STACKER_STX_AMT,
7779
};
7880
use crate::tests::neon_integrations::{
7981
get_account, get_chain_info, get_chain_info_opt, next_block_and_wait,
@@ -2591,6 +2593,338 @@ fn empty_sortition() {
25912593
signer_test.shutdown();
25922594
}
25932595

2596+
#[test]
2597+
#[ignore]
2598+
/// This test checks the behavior of signers when an empty sortition arrives
2599+
/// before the first block of the previous tenure has been approved.
2600+
/// Specifically:
2601+
/// - The empty sortition will trigger the miner to attempt a tenure extend.
2602+
/// - Signers will accept the tenure extend and sign subsequent blocks built
2603+
/// off the old sortition
2604+
fn empty_sortition_before_approval() {
2605+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
2606+
return;
2607+
}
2608+
2609+
tracing_subscriber::registry()
2610+
.with(fmt::layer())
2611+
.with(EnvFilter::from_default_env())
2612+
.init();
2613+
2614+
info!("------------------------- Test Setup -------------------------");
2615+
let num_signers = 5;
2616+
let sender_sk = Secp256k1PrivateKey::new();
2617+
let sender_addr = tests::to_addr(&sender_sk);
2618+
let send_amt = 100;
2619+
let send_fee = 180;
2620+
let recipient = PrincipalData::from(StacksAddress::burn_address(false));
2621+
let block_proposal_timeout = Duration::from_secs(20);
2622+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
2623+
num_signers,
2624+
vec![(sender_addr.clone(), send_amt + send_fee)],
2625+
|config| {
2626+
// make the duration long enough that the miner will be marked as malicious
2627+
config.block_proposal_timeout = block_proposal_timeout;
2628+
},
2629+
|_| {},
2630+
None,
2631+
None,
2632+
);
2633+
let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
2634+
2635+
signer_test.boot_to_epoch_3();
2636+
2637+
next_block_and_process_new_stacks_block(
2638+
&mut signer_test.running_nodes.btc_regtest_controller,
2639+
60,
2640+
&signer_test.running_nodes.coord_channel,
2641+
)
2642+
.unwrap();
2643+
2644+
let info = get_chain_info(&signer_test.running_nodes.conf);
2645+
let burn_height_before = info.burn_block_height;
2646+
let stacks_height_before = info.stacks_tip_height;
2647+
2648+
info!("Forcing miner to ignore signatures for next block");
2649+
TEST_IGNORE_SIGNERS.lock().unwrap().replace(true);
2650+
2651+
info!("Pausing block commits to trigger an empty sortition.");
2652+
signer_test
2653+
.running_nodes
2654+
.nakamoto_test_skip_commit_op
2655+
.0
2656+
.lock()
2657+
.unwrap()
2658+
.replace(true);
2659+
2660+
info!("------------------------- Test Mine Tenure A -------------------------");
2661+
let proposed_before = signer_test
2662+
.running_nodes
2663+
.nakamoto_blocks_proposed
2664+
.load(Ordering::SeqCst);
2665+
// Mine a regular tenure and wait for a block proposal
2666+
next_block_and(
2667+
&mut signer_test.running_nodes.btc_regtest_controller,
2668+
60,
2669+
|| {
2670+
let proposed_count = signer_test
2671+
.running_nodes
2672+
.nakamoto_blocks_proposed
2673+
.load(Ordering::SeqCst);
2674+
Ok(proposed_count > proposed_before)
2675+
},
2676+
)
2677+
.expect("Failed to mine tenure A and propose a block");
2678+
2679+
info!("------------------------- Test Mine Empty Tenure B -------------------------");
2680+
2681+
// Trigger an empty tenure
2682+
next_block_and(
2683+
&mut signer_test.running_nodes.btc_regtest_controller,
2684+
60,
2685+
|| {
2686+
let burn_height = get_chain_info(&signer_test.running_nodes.conf).burn_block_height;
2687+
Ok(burn_height == burn_height_before + 2)
2688+
},
2689+
)
2690+
.expect("Failed to mine empty tenure");
2691+
2692+
info!("Unpause block commits");
2693+
signer_test
2694+
.running_nodes
2695+
.nakamoto_test_skip_commit_op
2696+
.0
2697+
.lock()
2698+
.unwrap()
2699+
.replace(false);
2700+
2701+
info!("Stop ignoring signers and wait for the tip to advance");
2702+
TEST_IGNORE_SIGNERS.lock().unwrap().replace(false);
2703+
2704+
wait_for(60, || {
2705+
let info = get_chain_info(&signer_test.running_nodes.conf);
2706+
Ok(info.stacks_tip_height > stacks_height_before)
2707+
})
2708+
.expect("Failed to advance chain tip");
2709+
2710+
let info = get_chain_info(&signer_test.running_nodes.conf);
2711+
info!("Current state: {:?}", info);
2712+
2713+
// Wait for a block with a tenure extend to be mined
2714+
wait_for(60, || {
2715+
let blocks = test_observer::get_blocks();
2716+
let last_block = blocks.last().unwrap();
2717+
info!("Last block mined: {:?}", last_block);
2718+
for tx in last_block["transactions"].as_array().unwrap() {
2719+
let raw_tx = tx["raw_tx"].as_str().unwrap();
2720+
if raw_tx == "0x00" {
2721+
continue;
2722+
}
2723+
let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap();
2724+
let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap();
2725+
match &parsed.payload {
2726+
TransactionPayload::TenureChange(payload) => match payload.cause {
2727+
TenureChangeCause::Extended => {
2728+
info!("Found tenure extend block");
2729+
return Ok(true);
2730+
}
2731+
TenureChangeCause::BlockFound => {
2732+
info!("Found block with tenure change");
2733+
}
2734+
},
2735+
payload => {
2736+
info!("Found tx with payload: {:?}", payload);
2737+
}
2738+
};
2739+
}
2740+
Ok(false)
2741+
})
2742+
.expect("Timed out waiting for tenure extend");
2743+
2744+
let stacks_height_before = get_chain_info(&signer_test.running_nodes.conf).stacks_tip_height;
2745+
2746+
// submit a tx so that the miner will mine an extra block
2747+
let sender_nonce = 0;
2748+
let transfer_tx = make_stacks_transfer(
2749+
&sender_sk,
2750+
sender_nonce,
2751+
send_fee,
2752+
signer_test.running_nodes.conf.burnchain.chain_id,
2753+
&recipient,
2754+
send_amt,
2755+
);
2756+
submit_tx(&http_origin, &transfer_tx);
2757+
2758+
wait_for(60, || {
2759+
let info = get_chain_info(&signer_test.running_nodes.conf);
2760+
Ok(info.stacks_tip_height > stacks_height_before)
2761+
})
2762+
.expect("Failed to advance chain tip with STX transfer");
2763+
2764+
next_block_and_process_new_stacks_block(
2765+
&mut signer_test.running_nodes.btc_regtest_controller,
2766+
60,
2767+
&signer_test.running_nodes.coord_channel,
2768+
)
2769+
.expect("Failed to mine a normal tenure after the tenure extend");
2770+
2771+
signer_test.shutdown();
2772+
}
2773+
2774+
#[test]
2775+
#[ignore]
2776+
/// This test checks the behavior of signers when an empty sortition arrives
2777+
/// before the first block of the previous tenure has been proposed.
2778+
/// Specifically:
2779+
/// - The empty sortition will trigger the miner to attempt a tenure extend.
2780+
/// - Signers will accept the tenure extend and sign subsequent blocks built
2781+
/// off the old sortition
2782+
fn empty_sortition_before_proposal() {
2783+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
2784+
return;
2785+
}
2786+
2787+
tracing_subscriber::registry()
2788+
.with(fmt::layer())
2789+
.with(EnvFilter::from_default_env())
2790+
.init();
2791+
2792+
info!("------------------------- Test Setup -------------------------");
2793+
let num_signers = 5;
2794+
let sender_sk = Secp256k1PrivateKey::new();
2795+
let sender_addr = tests::to_addr(&sender_sk);
2796+
let send_amt = 100;
2797+
let send_fee = 180;
2798+
let recipient = PrincipalData::from(StacksAddress::burn_address(false));
2799+
let block_proposal_timeout = Duration::from_secs(20);
2800+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
2801+
num_signers,
2802+
vec![(sender_addr.clone(), send_amt + send_fee)],
2803+
|config| {
2804+
// make the duration long enough that the miner will be marked as malicious
2805+
config.block_proposal_timeout = block_proposal_timeout;
2806+
},
2807+
|_| {},
2808+
None,
2809+
None,
2810+
);
2811+
let http_origin = format!("http://{}", &signer_test.running_nodes.conf.node.rpc_bind);
2812+
2813+
signer_test.boot_to_epoch_3();
2814+
2815+
next_block_and_process_new_stacks_block(
2816+
&mut signer_test.running_nodes.btc_regtest_controller,
2817+
60,
2818+
&signer_test.running_nodes.coord_channel,
2819+
)
2820+
.unwrap();
2821+
2822+
let info = get_chain_info(&signer_test.running_nodes.conf);
2823+
let stacks_height_before = info.stacks_tip_height;
2824+
2825+
info!("Pause block commits to ensure we get an empty sortition");
2826+
signer_test
2827+
.running_nodes
2828+
.nakamoto_test_skip_commit_op
2829+
.0
2830+
.lock()
2831+
.unwrap()
2832+
.replace(true);
2833+
2834+
info!("Pause miner so it doesn't propose a block before the next tenure arrives");
2835+
TEST_MINE_STALL.lock().unwrap().replace(true);
2836+
2837+
info!("------------------------- Test Mine Tenure A and B -------------------------");
2838+
signer_test
2839+
.running_nodes
2840+
.btc_regtest_controller
2841+
.build_next_block(2);
2842+
2843+
// Sleep to ensure the signers see both burn blocks
2844+
sleep_ms(5_000);
2845+
2846+
info!("Unpause miner");
2847+
TEST_MINE_STALL.lock().unwrap().replace(false);
2848+
2849+
info!("Unpause block commits");
2850+
signer_test
2851+
.running_nodes
2852+
.nakamoto_test_skip_commit_op
2853+
.0
2854+
.lock()
2855+
.unwrap()
2856+
.replace(false);
2857+
2858+
wait_for(60, || {
2859+
let info = get_chain_info(&signer_test.running_nodes.conf);
2860+
Ok(info.stacks_tip_height > stacks_height_before)
2861+
})
2862+
.expect("Failed to advance chain tip");
2863+
2864+
let info = get_chain_info(&signer_test.running_nodes.conf);
2865+
info!("Current state: {:?}", info);
2866+
2867+
// Wait for a block with a tenure extend to be mined
2868+
wait_for(60, || {
2869+
let blocks = test_observer::get_blocks();
2870+
let last_block = blocks.last().unwrap();
2871+
info!("Last block mined: {:?}", last_block);
2872+
for tx in last_block["transactions"].as_array().unwrap() {
2873+
let raw_tx = tx["raw_tx"].as_str().unwrap();
2874+
if raw_tx == "0x00" {
2875+
continue;
2876+
}
2877+
let tx_bytes = hex_bytes(&raw_tx[2..]).unwrap();
2878+
let parsed = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap();
2879+
match &parsed.payload {
2880+
TransactionPayload::TenureChange(payload) => match payload.cause {
2881+
TenureChangeCause::Extended => {
2882+
info!("Found tenure extend block");
2883+
return Ok(true);
2884+
}
2885+
TenureChangeCause::BlockFound => {
2886+
info!("Found block with tenure change");
2887+
}
2888+
},
2889+
payload => {
2890+
info!("Found tx with payload: {:?}", payload);
2891+
}
2892+
};
2893+
}
2894+
Ok(false)
2895+
})
2896+
.expect("Timed out waiting for tenure extend");
2897+
2898+
let stacks_height_before = get_chain_info(&signer_test.running_nodes.conf).stacks_tip_height;
2899+
2900+
// submit a tx so that the miner will mine an extra block
2901+
let sender_nonce = 0;
2902+
let transfer_tx = make_stacks_transfer(
2903+
&sender_sk,
2904+
sender_nonce,
2905+
send_fee,
2906+
signer_test.running_nodes.conf.burnchain.chain_id,
2907+
&recipient,
2908+
send_amt,
2909+
);
2910+
submit_tx(&http_origin, &transfer_tx);
2911+
2912+
wait_for(60, || {
2913+
let info = get_chain_info(&signer_test.running_nodes.conf);
2914+
Ok(info.stacks_tip_height > stacks_height_before)
2915+
})
2916+
.expect("Failed to advance chain tip with STX transfer");
2917+
2918+
next_block_and_process_new_stacks_block(
2919+
&mut signer_test.running_nodes.btc_regtest_controller,
2920+
60,
2921+
&signer_test.running_nodes.coord_channel,
2922+
)
2923+
.expect("Failed to mine a normal tenure after the tenure extend");
2924+
2925+
signer_test.shutdown();
2926+
}
2927+
25942928
#[test]
25952929
#[ignore]
25962930
/// This test checks that Epoch 2.5 signers will issue a mock signature per burn block they receive.

0 commit comments

Comments
 (0)