@@ -3379,6 +3379,141 @@ fn tx_replay_forking_test() {
3379
3379
signer_test.shutdown();
3380
3380
}
3381
3381
3382
+ #[test]
3383
+ #[ignore]
3384
+ /// Trigger a Bitcoin fork across reward cycle
3385
+ /// and ensure that the signers detect the fork,
3386
+ /// but reject to move into a tx replay state
3387
+ ///
3388
+ /// The test flow is:
3389
+ ///
3390
+ /// - Boot to Epoch 3 (that is in the middle of reward cycle N)
3391
+ /// - Mine until the last tenure of the reward cycle N
3392
+ /// - Include a STX transfer in the last tenure
3393
+ /// - Mine 1 Bitcoin block in the next reward cycle N+1
3394
+ /// - Trigger a Bitcoin fork from reward cycle N (3 blocks)
3395
+ /// - Verify that signers don't move into tx replay state
3396
+ /// - In the end, the STX transfer transaction is not replayed
3397
+ fn tx_replay_rejected_when_forking_across_reward_cycle() {
3398
+ if env::var("BITCOIND_TEST") != Ok("1".into()) {
3399
+ return;
3400
+ }
3401
+
3402
+ let num_signers = 5;
3403
+ let sender_sk = Secp256k1PrivateKey::random();
3404
+ let sender_addr = tests::to_addr(&sender_sk);
3405
+ let send_amt = 100;
3406
+ let send_fee = 180;
3407
+ let mut signer_test: SignerTest<SpawnedSigner> = SignerTest::new_with_config_modifications(
3408
+ num_signers,
3409
+ vec![(sender_addr, (send_amt + send_fee) * 10)],
3410
+ |_| {},
3411
+ |node_config| {
3412
+ node_config.miner.block_commit_delay = Duration::from_secs(1);
3413
+ },
3414
+ None,
3415
+ None,
3416
+ );
3417
+ let conf = signer_test.running_nodes.conf.clone();
3418
+ let http_origin = format!("http://{}", &conf.node.rpc_bind);
3419
+ let burn_chain = signer_test
3420
+ .running_nodes
3421
+ .btc_regtest_controller
3422
+ .get_burnchain();
3423
+
3424
+ signer_test.boot_to_epoch_3();
3425
+ info!("------------------------- Reached Epoch 3.0 -------------------------");
3426
+
3427
+ let burn_block_height = get_chain_info(&conf).burn_block_height;
3428
+ let initial_reward_cycle = signer_test.get_current_reward_cycle();
3429
+ let rc_last_height = burn_chain.nakamoto_last_block_of_cycle(initial_reward_cycle);
3430
+
3431
+ info!("----- Mine to the end of reward cycle {initial_reward_cycle} height {rc_last_height} -----");
3432
+ let pre_fork_tenures = rc_last_height - burn_block_height;
3433
+ for i in 1..=pre_fork_tenures {
3434
+ info!("Mining pre-fork tenure {i} of {pre_fork_tenures}");
3435
+ signer_test.mine_nakamoto_block(Duration::from_secs(30), true);
3436
+ }
3437
+ signer_test.check_signer_states_normal();
3438
+
3439
+ info!("----- Submit Stx transfer in last tenure height {rc_last_height} -----");
3440
+ // Make a transfer tx that will get forked
3441
+ let tip = get_chain_info(&conf);
3442
+ let _ = signer_test
3443
+ .submit_transfer_tx(&sender_sk, send_fee, send_amt)
3444
+ .unwrap();
3445
+ wait_for(30, || {
3446
+ let new_tip = get_chain_info(&conf);
3447
+ Ok(new_tip.stacks_tip_height > tip.stacks_tip_height)
3448
+ })
3449
+ .expect("Timed out waiting for transfer tx to be mined");
3450
+
3451
+ let pre_fork_tx_nonce = get_account(&http_origin, &sender_addr).nonce;
3452
+ assert_eq!(1, pre_fork_tx_nonce);
3453
+
3454
+ info!("----- Mine 1 block in new reward cycle -----");
3455
+ signer_test.mine_nakamoto_block(Duration::from_secs(30), true);
3456
+ signer_test.check_signer_states_normal();
3457
+
3458
+ let next_reward_cycle = initial_reward_cycle + 1;
3459
+ let new_burn_block_height = get_chain_info(&conf).burn_block_height;
3460
+ assert_eq!(next_reward_cycle, signer_test.get_current_reward_cycle());
3461
+ assert_eq!(
3462
+ new_burn_block_height,
3463
+ burn_chain.nakamoto_first_block_of_cycle(next_reward_cycle)
3464
+ );
3465
+
3466
+ info!("----- Trigger Bitcoin fork -----");
3467
+ //Fork on the third-to-last tenure of prev reward cycle
3468
+ let btc_controller = &signer_test.running_nodes.btc_regtest_controller;
3469
+ let burn_block_hash_to_fork = btc_controller.get_block_hash(new_burn_block_height - 2);
3470
+ btc_controller.invalidate_block(&burn_block_hash_to_fork);
3471
+ btc_controller.build_next_block(3);
3472
+
3473
+ // note, we should still have normal signer states!
3474
+ signer_test.check_signer_states_normal();
3475
+
3476
+ //mine throught the fork (just check commits because of naka block mining stalled)
3477
+ TEST_MINE_STALL.set(true);
3478
+ let submitted_commits = signer_test
3479
+ .running_nodes
3480
+ .counters
3481
+ .naka_submitted_commits
3482
+ .clone();
3483
+
3484
+ for i in 0..3 {
3485
+ let current_burn_height = get_chain_info(&signer_test.running_nodes.conf).burn_block_height;
3486
+ info!(
3487
+ "Mining block #{i} to be considered a frequent miner";
3488
+ "current_burn_height" => current_burn_height,
3489
+ );
3490
+ let commits_count = submitted_commits.load(Ordering::SeqCst);
3491
+ next_block_and(
3492
+ &mut signer_test.running_nodes.btc_regtest_controller,
3493
+ 60,
3494
+ || {
3495
+ let commits_submitted = submitted_commits.load(Ordering::SeqCst);
3496
+ Ok(commits_submitted > commits_count)
3497
+ },
3498
+ )
3499
+ .unwrap();
3500
+ }
3501
+
3502
+ let post_fork_tx_nonce = get_account(&http_origin, &sender_addr).nonce;
3503
+ assert_eq!(0, post_fork_tx_nonce);
3504
+
3505
+ info!("----- Check Signers Tx Replay state -----");
3506
+ let (signer_states, _) = signer_test.get_burn_updated_states();
3507
+ for state in signer_states {
3508
+ assert!(
3509
+ state.get_tx_replay_set().is_none(),
3510
+ "Signer state is in tx replay state, when it shouldn't be"
3511
+ );
3512
+ }
3513
+
3514
+ signer_test.shutdown();
3515
+ }
3516
+
3382
3517
#[test]
3383
3518
#[ignore]
3384
3519
fn multiple_miners() {
0 commit comments