@@ -77,6 +77,7 @@ use stacks_signer::client::{SignerSlotID, StackerDB};
77
77
use stacks_signer::config::{build_signer_config_tomls, GlobalConfig as SignerConfig, Network};
78
78
use stacks_signer::signerdb::SignerDb;
79
79
use stacks_signer::v0::signer::TEST_REPEAT_PROPOSAL_RESPONSE;
80
+ use stacks_signer::v0::signer_state::LocalStateMachine;
80
81
use stacks_signer::v0::tests::{
81
82
TEST_IGNORE_ALL_BLOCK_PROPOSALS, TEST_PAUSE_BLOCK_BROADCAST, TEST_REJECT_ALL_BLOCK_PROPOSAL,
82
83
TEST_SKIP_BLOCK_BROADCAST, TEST_SKIP_SIGNER_CLEANUP, TEST_STALL_BLOCK_VALIDATION_SUBMISSION,
@@ -2649,6 +2650,226 @@ fn bitcoind_forking_test() {
2649
2650
signer_test.shutdown();
2650
2651
}
2651
2652
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
+
2652
2873
#[test]
2653
2874
#[ignore]
2654
2875
fn multiple_miners() {
0 commit comments