@@ -39,7 +39,7 @@ use stacks::clarity_cli::vm_execute as execute;
39
39
use stacks:: cli;
40
40
use stacks:: codec:: StacksMessageCodec ;
41
41
use stacks:: config:: { EventKeyType , EventObserverConfig , FeeEstimatorName , InitialBalance } ;
42
- use stacks:: core:: mempool:: MemPoolWalkTxTypes ;
42
+ use stacks:: core:: mempool:: { MemPoolWalkStrategy , MemPoolWalkTxTypes } ;
43
43
use stacks:: core:: test_util:: {
44
44
make_contract_call, make_contract_publish, make_contract_publish_microblock_only,
45
45
make_microblock, make_stacks_transfer_mblock_only, make_stacks_transfer_serialized, to_addr,
@@ -4527,19 +4527,26 @@ fn mining_events_integration_test() {
4527
4527
channel. stop_chains_coordinator ( ) ;
4528
4528
}
4529
4529
4530
- /// This test checks that the limit behavior in the miner works as expected for anchored block
4531
- /// building. When we first hit the block limit, the limit behavior switches to
4532
- /// `CONTRACT_LIMIT_HIT`, during which stx transfers are still allowed, and contract related
4533
- /// transactions are skipped.
4534
- /// Note: the test is sensitive to the order in which transactions are mined; it is written
4535
- /// expecting that transactions are traversed in the order tx_1, tx_2, tx_3, and tx_4.
4536
- #[ test]
4537
- #[ ignore]
4538
- fn block_limit_hit_integration_test ( ) {
4539
- if env:: var ( "BITCOIND_TEST" ) != Ok ( "1" . into ( ) ) {
4540
- return ;
4541
- }
4542
-
4530
+ /// Sets up and runs a block limit integration test with the specified mempool walk strategy.
4531
+ ///
4532
+ /// This function creates a controlled test environment to verify how different mempool walking
4533
+ /// strategies affect transaction ordering when block limits are hit. It simulates a scenario
4534
+ /// where:
4535
+ ///
4536
+ /// - tx1: High-fee (555k) oversize contract from addr1 (nonce 0) - (oversize - causes block limit)
4537
+ /// - tx2: High-fee (555k) oversize contract from addr1 (nonce 1) - (oversize - depends on tx1)
4538
+ /// - tx3: Lower-fee (150k) medium contract from addr2 (nonce 0) - (medium size)
4539
+ /// - tx4: Low-fee (180) STX transfer from addr3 (nonce 0) - (tiny)
4540
+ ///
4541
+ /// The key difference between strategies:
4542
+ /// - GlobalFeeRate: Prioritizes by fee rate globally, uses candidate_cache for nonce-invalid transactions
4543
+ /// - NextNonceWithHighestFeeRate: Pre-filters to only valid-nonce transactions, then prioritizes by fee within account groups
4544
+ ///
4545
+ /// # Test Environment Setup
4546
+ /// - 3 accounts with 10M STX each
4547
+ /// - Bitcoin regtest with 201 blocks bootstrapped
4548
+ /// - Microblocks enabled with 30s wait time
4549
+ fn setup_block_limit_test ( strategy : MemPoolWalkStrategy ) -> ( Vec < serde_json:: Value > , [ String ; 4 ] ) {
4543
4550
// 700 invocations
4544
4551
let max_contract_src = format ! (
4545
4552
"(define-private (work) (begin {} 1))
@@ -4588,22 +4595,23 @@ fn block_limit_hit_integration_test() {
4588
4595
let spender_sk = StacksPrivateKey :: random ( ) ;
4589
4596
let addr = to_addr ( & spender_sk) ;
4590
4597
let second_spender_sk = StacksPrivateKey :: random ( ) ;
4591
- let second_spender_addr: PrincipalData = to_addr ( & second_spender_sk) . into ( ) ;
4598
+ let second_spender_addr = to_addr ( & second_spender_sk) ;
4592
4599
let third_spender_sk = StacksPrivateKey :: random ( ) ;
4593
- let third_spender_addr: PrincipalData = to_addr ( & third_spender_sk) . into ( ) ;
4600
+ let third_spender_addr = to_addr ( & third_spender_sk) ;
4594
4601
4595
4602
let ( mut conf, _miner_account) = neon_integration_test_conf ( ) ;
4603
+ conf. miner . mempool_walk_strategy = strategy;
4596
4604
4597
4605
conf. initial_balances . push ( InitialBalance {
4598
4606
address : addr. into ( ) ,
4599
4607
amount : 10_000_000 ,
4600
4608
} ) ;
4601
4609
conf. initial_balances . push ( InitialBalance {
4602
- address : second_spender_addr. clone ( ) ,
4610
+ address : second_spender_addr. into ( ) ,
4603
4611
amount : 10_000_000 ,
4604
4612
} ) ;
4605
4613
conf. initial_balances . push ( InitialBalance {
4606
- address : third_spender_addr. clone ( ) ,
4614
+ address : third_spender_addr. into ( ) ,
4607
4615
amount : 10_000_000 ,
4608
4616
} ) ;
4609
4617
@@ -4702,6 +4710,7 @@ fn block_limit_hit_integration_test() {
4702
4710
next_block_and_wait ( & mut btc_regtest_controller, & blocks_processed) ;
4703
4711
sleep_ms ( 20_000 ) ;
4704
4712
4713
+ // Verify nonces
4705
4714
let res = get_account ( & http_origin, & addr) ;
4706
4715
assert_eq ! ( res. nonce, 2 ) ;
4707
4716
@@ -4714,30 +4723,152 @@ fn block_limit_hit_integration_test() {
4714
4723
let mined_block_events = test_observer:: get_blocks ( ) ;
4715
4724
assert_eq ! ( mined_block_events. len( ) , 5 ) ;
4716
4725
4717
- let tx_third_block = mined_block_events[ 3 ]
4718
- . get ( "transactions" )
4719
- . unwrap ( )
4720
- . as_array ( )
4721
- . unwrap ( ) ;
4722
- assert_eq ! ( tx_third_block. len( ) , 3 ) ;
4723
- let txid_1_exp = tx_third_block[ 1 ] . get ( "txid" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ;
4724
- let txid_4_exp = tx_third_block[ 2 ] . get ( "txid" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ;
4725
- assert_eq ! ( format!( "0x{txid_1}" ) , txid_1_exp) ;
4726
- assert_eq ! ( format!( "0x{txid_4}" ) , txid_4_exp) ;
4727
-
4728
- let tx_fourth_block = mined_block_events[ 4 ]
4729
- . get ( "transactions" )
4730
- . unwrap ( )
4731
- . as_array ( )
4732
- . unwrap ( ) ;
4733
- assert_eq ! ( tx_fourth_block. len( ) , 3 ) ;
4734
- let txid_2_exp = tx_fourth_block[ 1 ] . get ( "txid" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ;
4735
- let txid_3_exp = tx_fourth_block[ 2 ] . get ( "txid" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ;
4736
- assert_eq ! ( format!( "0x{txid_2}" ) , txid_2_exp) ;
4737
- assert_eq ! ( format!( "0x{txid_3}" ) , txid_3_exp) ;
4738
-
4739
4726
test_observer:: clear ( ) ;
4740
4727
channel. stop_chains_coordinator ( ) ;
4728
+ ( mined_block_events, [ txid_1, txid_2, txid_3, txid_4] )
4729
+ }
4730
+
4731
+ /// Tests block limit behavior with GlobalFeeRate mempool walk strategy.
4732
+ ///
4733
+ /// This test verifies that when using GlobalFeeRate strategy, transactions are selected
4734
+ /// purely based on fee rate without considering nonce dependencies within accounts.
4735
+ ///
4736
+ /// # Expected Behavior with GlobalFeeRate
4737
+ /// The miner processes transactions in pure fee-rate order using candidate caching:
4738
+ ///
4739
+ /// **Initial Processing Order:** tx1/tx2 (555k fee) → tx3 (150k fee) → tx4 (180 fee)
4740
+ /// **Block 3 Results:**
4741
+ /// - tx1 gets mined (highest fee, valid nonce 0)
4742
+ /// - tx2 has invalid nonce (1), cached for later retry
4743
+ /// - tx3 blocked by contract limit, deferred to next block
4744
+ /// - tx4 gets mined (STX transfers allowed after contract limit)
4745
+ ///
4746
+ /// **Block 4 Results:**
4747
+ /// - tx2 now valid (addr1 nonce advanced to 1), gets mined
4748
+ /// - tx3 gets mined (no contract limit in new block)
4749
+ ///
4750
+ /// **Final Distribution:**
4751
+ /// - Block 3: 3 transactions (coinbase + tx1 + tx4)
4752
+ /// - Block 4: 3 transactions (coinbase + tx2 + tx3)
4753
+ #[ test]
4754
+ #[ ignore]
4755
+ fn block_limit_hit_integration_test_global_fee_rate ( ) {
4756
+ if env:: var ( "BITCOIND_TEST" ) != Ok ( "1" . into ( ) ) {
4757
+ return ;
4758
+ }
4759
+
4760
+ let ( events, [ txid_1, txid_2, txid_3, txid_4] ) =
4761
+ setup_block_limit_test ( MemPoolWalkStrategy :: GlobalFeeRate ) ;
4762
+
4763
+ // Block 3: should contain tx1 and tx4 (GlobalFeeRate strategy)
4764
+ let block3_txs = events[ 3 ] [ "transactions" ] . as_array ( ) . unwrap ( ) ;
4765
+ assert_eq ! (
4766
+ block3_txs. len( ) ,
4767
+ 3 ,
4768
+ "Block 3 should have 3 transactions (coinbase + 2 user txs)"
4769
+ ) ;
4770
+
4771
+ let txid_1_exp = block3_txs[ 1 ] . get ( "txid" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ;
4772
+ let txid_4_exp = block3_txs[ 2 ] . get ( "txid" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ;
4773
+ assert_eq ! (
4774
+ format!( "0x{}" , txid_1) ,
4775
+ txid_1_exp,
4776
+ "tx1 should be in block 3"
4777
+ ) ;
4778
+ assert_eq ! (
4779
+ format!( "0x{}" , txid_4) ,
4780
+ txid_4_exp,
4781
+ "tx4 should be in block 3"
4782
+ ) ;
4783
+
4784
+ // Block 4: should contain tx2 and tx3
4785
+ let block4_txs = events[ 4 ] [ "transactions" ] . as_array ( ) . unwrap ( ) ;
4786
+ assert_eq ! (
4787
+ block4_txs. len( ) ,
4788
+ 3 ,
4789
+ "Block 4 should have 3 transactions (coinbase + 2 user txs)"
4790
+ ) ;
4791
+
4792
+ let txid_2_exp = block4_txs[ 1 ] . get ( "txid" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ;
4793
+ let txid_3_exp = block4_txs[ 2 ] . get ( "txid" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ;
4794
+ assert_eq ! (
4795
+ format!( "0x{}" , txid_2) ,
4796
+ txid_2_exp,
4797
+ "tx2 should be in block 4"
4798
+ ) ;
4799
+ assert_eq ! (
4800
+ format!( "0x{}" , txid_3) ,
4801
+ txid_3_exp,
4802
+ "tx3 should be in block 4"
4803
+ ) ;
4804
+ }
4805
+
4806
+ /// Tests block limit behavior with NextNonceWithHighestFeeRate mempool walk strategy.
4807
+ ///
4808
+ /// This test verifies that when using NextNonceWithHighestFeeRate strategy, transactions
4809
+ /// are grouped by account and selected based on the next valid nonce, with the strategy
4810
+ /// re-querying the mempool after nonce state changes.
4811
+ ///
4812
+ /// # Expected Behavior with NextNonceWithHighestFeeRate
4813
+ /// The miner uses a multi-pass approach with nonce-aware querying:
4814
+ ///
4815
+ /// **Pass 1 - Initial Query:** tx1, tx3, tx4 (tx2 filtered out: nonce 1 > expected 0)
4816
+ /// **Pass 1 - Processing Order:** tx4 → tx3 → tx1 (account ranking + fee prioritization)
4817
+ /// **Pass 1 - Results:**
4818
+ /// - tx4 gets mined (STX transfer, low cost)
4819
+ /// - tx3 gets mined (medium contract fits remaining budget)
4820
+ /// - tx1 gets mined (oversize contract, nearly exhausts block budget)
4821
+ /// - Nonce cache updated: addr1 nonce advances to 1
4822
+ ///
4823
+ /// **Pass 2 - Re-query:** tx2 now becomes valid (addr1 nonce = 1)
4824
+ /// **Pass 2 - Processing:** tx2 exceeds remaining block budget, skipped to next block
4825
+ ///
4826
+ /// **Block 3 Final:** 4 transactions (coinbase + tx4 + tx3 + tx1)
4827
+ /// **Block 4:** 2 transactions (coinbase + tx2)
4828
+ #[ test]
4829
+ #[ ignore]
4830
+ fn block_limit_hit_integration_test_next_nonce_highest_fee ( ) {
4831
+ if env:: var ( "BITCOIND_TEST" ) != Ok ( "1" . into ( ) ) {
4832
+ return ;
4833
+ }
4834
+
4835
+ let ( events, [ txid_1, txid_2, txid_3, txid_4] ) =
4836
+ setup_block_limit_test ( MemPoolWalkStrategy :: NextNonceWithHighestFeeRate ) ;
4837
+
4838
+ // Block 3: should contain tx1, tx3, and tx4 (NextNonceWithHighestFeeRate strategy)
4839
+ let block3_txs = events[ 3 ] [ "transactions" ] . as_array ( ) . unwrap ( ) ;
4840
+ assert_eq ! ( block3_txs. len( ) , 4 , "Block 3 should have 4 transactions" ) ;
4841
+
4842
+ // Extract transaction IDs from block 3 (skip coinbase at index 0)
4843
+ let block3_txids: Vec < String > = block3_txs[ 1 ..]
4844
+ . iter ( )
4845
+ . map ( |tx| tx. get ( "txid" ) . unwrap ( ) . as_str ( ) . unwrap ( ) . to_string ( ) )
4846
+ . collect ( ) ;
4847
+
4848
+ // Check that tx1, tx3, and tx4 are in block 3
4849
+ assert ! (
4850
+ block3_txids. contains( & format!( "0x{}" , txid_1) ) ,
4851
+ "tx1 should be in block 3"
4852
+ ) ;
4853
+ assert ! (
4854
+ block3_txids. contains( & format!( "0x{}" , txid_3) ) ,
4855
+ "tx3 should be in block 3"
4856
+ ) ;
4857
+ assert ! (
4858
+ block3_txids. contains( & format!( "0x{}" , txid_4) ) ,
4859
+ "tx4 should be in block 3"
4860
+ ) ;
4861
+
4862
+ // Block 4: should contain tx2 (NextNonceWithHighestFeeRate strategy)
4863
+ let block4_txs = events[ 4 ] [ "transactions" ] . as_array ( ) . unwrap ( ) ;
4864
+ assert_eq ! ( block4_txs. len( ) , 2 , "Block 4 should have 2 transactions" ) ;
4865
+
4866
+ let txid_2_exp = block4_txs[ 1 ] . get ( "txid" ) . unwrap ( ) . as_str ( ) . unwrap ( ) ;
4867
+ assert_eq ! (
4868
+ format!( "0x{}" , txid_2) ,
4869
+ txid_2_exp,
4870
+ "tx2 should be in block 4"
4871
+ ) ;
4741
4872
}
4742
4873
4743
4874
#[ test]
0 commit comments