Skip to content

Commit aaea5f4

Browse files
authored
Merge pull request #5415 from stacks-network/fix/tenure-extend-no-sortition
test: additional scenarios for empty sortition tenure extends
2 parents ebe6adb + a782284 commit aaea5f4

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,
@@ -2562,6 +2564,338 @@ fn empty_sortition() {
25622564
signer_test.shutdown();
25632565
}
25642566

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

0 commit comments

Comments
 (0)