Skip to content

Commit eeb5b1e

Browse files
committed
wip: integration test for tx replay state machine
1 parent acb935e commit eeb5b1e

File tree

1 file changed

+221
-0
lines changed
  • testnet/stacks-node/src/tests/signer

1 file changed

+221
-0
lines changed

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

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ use stacks_signer::client::{SignerSlotID, StackerDB};
7777
use stacks_signer::config::{build_signer_config_tomls, GlobalConfig as SignerConfig, Network};
7878
use stacks_signer::signerdb::SignerDb;
7979
use stacks_signer::v0::signer::TEST_REPEAT_PROPOSAL_RESPONSE;
80+
use stacks_signer::v0::signer_state::LocalStateMachine;
8081
use stacks_signer::v0::tests::{
8182
TEST_IGNORE_ALL_BLOCK_PROPOSALS, TEST_PAUSE_BLOCK_BROADCAST, TEST_REJECT_ALL_BLOCK_PROPOSAL,
8283
TEST_SKIP_BLOCK_BROADCAST, TEST_SKIP_SIGNER_CLEANUP, TEST_STALL_BLOCK_VALIDATION_SUBMISSION,
@@ -2649,6 +2650,226 @@ fn bitcoind_forking_test() {
26492650
signer_test.shutdown();
26502651
}
26512652

2653+
#[test]
2654+
#[ignore]
2655+
/// Trigger a Bitcoin fork and ensure that the signer
2656+
/// both detects the fork and moves into a tx replay state
2657+
fn tx_replay_forking_test() {
2658+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
2659+
return;
2660+
}
2661+
2662+
let num_signers = 5;
2663+
let sender_sk = Secp256k1PrivateKey::random();
2664+
let sender_addr = tests::to_addr(&sender_sk);
2665+
let send_amt = 100;
2666+
let send_fee = 180;
2667+
let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
2668+
num_signers,
2669+
vec![(sender_addr, send_amt + send_fee)],
2670+
|_| {},
2671+
|node_config| {
2672+
node_config.miner.block_commit_delay = Duration::from_secs(1);
2673+
},
2674+
None,
2675+
None,
2676+
);
2677+
let conf = signer_test.running_nodes.conf.clone();
2678+
let http_origin = format!("http://{}", &conf.node.rpc_bind);
2679+
let _miner_address = Keychain::default(conf.node.seed.clone())
2680+
.origin_address(conf.is_mainnet())
2681+
.unwrap();
2682+
let miner_pk = signer_test
2683+
.running_nodes
2684+
.btc_regtest_controller
2685+
.get_mining_pubkey()
2686+
.as_deref()
2687+
.map(Secp256k1PublicKey::from_hex)
2688+
.unwrap()
2689+
.unwrap();
2690+
2691+
let get_unconfirmed_commit_data = |btc_controller: &mut BitcoinRegtestController| {
2692+
let unconfirmed_utxo = btc_controller
2693+
.get_all_utxos(&miner_pk)
2694+
.into_iter()
2695+
.find(|utxo| utxo.confirmations == 0)?;
2696+
let unconfirmed_txid = Txid::from_bitcoin_tx_hash(&unconfirmed_utxo.txid);
2697+
let unconfirmed_tx = btc_controller.get_raw_transaction(&unconfirmed_txid);
2698+
let unconfirmed_tx_opreturn_bytes = unconfirmed_tx.output[0].script_pubkey.as_bytes();
2699+
info!(
2700+
"Unconfirmed tx bytes: {}",
2701+
stacks::util::hash::to_hex(unconfirmed_tx_opreturn_bytes)
2702+
);
2703+
let data = LeaderBlockCommitOp::parse_data(
2704+
&unconfirmed_tx_opreturn_bytes[unconfirmed_tx_opreturn_bytes.len() - 77..],
2705+
)
2706+
.unwrap();
2707+
Some(data)
2708+
};
2709+
2710+
signer_test.boot_to_epoch_3();
2711+
info!("------------------------- Reached Epoch 3.0 -------------------------");
2712+
let pre_fork_tenures = 10;
2713+
2714+
for i in 0..pre_fork_tenures {
2715+
info!("Mining pre-fork tenure {} of {pre_fork_tenures}", i + 1);
2716+
signer_test.mine_nakamoto_block(Duration::from_secs(30), true);
2717+
signer_test.check_signer_states_normal();
2718+
}
2719+
2720+
let burn_blocks = test_observer::get_burn_blocks();
2721+
let forked_blocks = burn_blocks.iter().rev().take(2).collect::<Vec<_>>();
2722+
let start_tenure: ConsensusHash = hex_bytes(
2723+
&forked_blocks[0]
2724+
.get("consensus_hash")
2725+
.unwrap()
2726+
.as_str()
2727+
.unwrap()[2..],
2728+
)
2729+
.unwrap()
2730+
.as_slice()
2731+
.into();
2732+
let end_tenure: ConsensusHash = hex_bytes(
2733+
&forked_blocks[1]
2734+
.get("consensus_hash")
2735+
.unwrap()
2736+
.as_str()
2737+
.unwrap()[2..],
2738+
)
2739+
.unwrap()
2740+
.as_slice()
2741+
.into();
2742+
2743+
let tip = get_chain_info(&signer_test.running_nodes.conf);
2744+
// Make a transfer tx (this will get forked)
2745+
signer_test
2746+
.submit_transfer_tx(&sender_sk, send_fee, send_amt)
2747+
.unwrap();
2748+
2749+
wait_for(30, || {
2750+
let new_tip = get_chain_info(&signer_test.running_nodes.conf);
2751+
Ok(new_tip.stacks_tip_height > tip.stacks_tip_height)
2752+
})
2753+
.expect("Timed out waiting for transfer tx to be mined");
2754+
2755+
let pre_fork_1_nonce = get_account(&http_origin, &sender_addr).nonce;
2756+
assert_eq!(pre_fork_1_nonce, 1);
2757+
2758+
info!("------------------------- Triggering Bitcoin Fork -------------------------");
2759+
2760+
let burn_header_hash_to_fork = signer_test
2761+
.running_nodes
2762+
.btc_regtest_controller
2763+
.get_block_hash(tip.burn_block_height - 2);
2764+
signer_test
2765+
.running_nodes
2766+
.btc_regtest_controller
2767+
.invalidate_block(&burn_header_hash_to_fork);
2768+
signer_test
2769+
.running_nodes
2770+
.btc_regtest_controller
2771+
.build_next_block(3);
2772+
2773+
// note, we should still have normal signer states!
2774+
signer_test.check_signer_states_normal();
2775+
2776+
info!("Wait for block off of shallow fork");
2777+
2778+
TEST_MINE_STALL.set(true);
2779+
2780+
let submitted_commits = signer_test
2781+
.running_nodes
2782+
.counters
2783+
.naka_submitted_commits
2784+
.clone();
2785+
2786+
let fork_info = signer_test
2787+
.stacks_client
2788+
// .get_tenure_forking_info(&start_tenure, &end_tenure)
2789+
.get_tenure_forking_info(&end_tenure, &start_tenure)
2790+
.unwrap();
2791+
2792+
info!("---- Fork info: {fork_info:?} ----");
2793+
2794+
for fork in fork_info {
2795+
info!("---- Fork: {} ----", fork.consensus_hash);
2796+
fork.nakamoto_blocks.inspect(|blocks| {
2797+
for block in blocks {
2798+
info!("---- Block: {block:?} ----");
2799+
}
2800+
});
2801+
}
2802+
2803+
// we need to mine some blocks to get back to being considered a frequent miner
2804+
for i in 0..3 {
2805+
let current_burn_height = get_chain_info(&signer_test.running_nodes.conf).burn_block_height;
2806+
info!(
2807+
"Mining block #{i} to be considered a frequent miner";
2808+
"current_burn_height" => current_burn_height,
2809+
);
2810+
let commits_count = submitted_commits.load(Ordering::SeqCst);
2811+
next_block_and_controller(
2812+
&mut signer_test.running_nodes.btc_regtest_controller,
2813+
60,
2814+
|btc_controller| {
2815+
let commits_submitted = submitted_commits
2816+
.load(Ordering::SeqCst);
2817+
if commits_submitted <= commits_count {
2818+
// wait until a commit was submitted
2819+
return Ok(false)
2820+
}
2821+
let Some(payload) = get_unconfirmed_commit_data(btc_controller) else {
2822+
warn!("Commit submitted, but bitcoin doesn't see it in the unconfirmed UTXO set, will try to wait.");
2823+
return Ok(false)
2824+
};
2825+
let burn_parent_modulus = payload.burn_parent_modulus;
2826+
let current_modulus = u8::try_from((current_burn_height + 1) % 5).unwrap();
2827+
info!(
2828+
"Ongoing Commit Operation check";
2829+
"burn_parent_modulus" => burn_parent_modulus,
2830+
"current_modulus" => current_modulus,
2831+
"payload" => ?payload,
2832+
);
2833+
Ok(burn_parent_modulus == current_modulus)
2834+
},
2835+
)
2836+
.unwrap();
2837+
// signer_test.check_signer_states_normal_missed_sortition();
2838+
}
2839+
2840+
let post_fork_1_nonce = get_account(&http_origin, &sender_addr).nonce;
2841+
2842+
let burn_blocks = test_observer::get_burn_blocks().clone();
2843+
2844+
for block in burn_blocks {
2845+
let height = block.get("burn_block_height").unwrap().as_number().unwrap();
2846+
if height.as_u64().unwrap() < 230 {
2847+
continue;
2848+
}
2849+
let consensus_hash = block.get("consensus_hash").unwrap().as_str().unwrap();
2850+
info!("---- Burn Block {height} {consensus_hash} ----");
2851+
}
2852+
2853+
let (signer_states, _) = signer_test.get_burn_updated_states();
2854+
for state in signer_states {
2855+
match state {
2856+
LocalStateMachine::Initialized(signer_state_machine) => {
2857+
assert!(signer_state_machine.tx_replay_state);
2858+
}
2859+
_ => {
2860+
panic!("Signer state is not in the initialized state");
2861+
}
2862+
}
2863+
}
2864+
2865+
// We should have forked 1 tx
2866+
assert_eq!(post_fork_1_nonce, pre_fork_1_nonce - 1);
2867+
2868+
TEST_MINE_STALL.set(false);
2869+
2870+
signer_test.shutdown();
2871+
}
2872+
26522873
#[test]
26532874
#[ignore]
26542875
fn multiple_miners() {

0 commit comments

Comments
 (0)