@@ -1116,155 +1116,6 @@ fn test_build_anchored_blocks_connected_by_microblocks_across_epoch_invalid() {
11161116 assert_eq ! ( last_block. header. total_work. work, 10 ) ; // mined a chain successfully across the epoch boundary
11171117}
11181118
1119- #[ test]
1120- /// This test covers two different behaviors added to the block assembly logic:
1121- /// (1) Ordering by estimated fee rate: the test peer uses the "unit" estimator
1122- /// for costs, but this estimator still uses the fee of the transaction to order
1123- /// the mempool. This leads to the behavior in this test where txs are included
1124- /// like 0 -> 1 -> 2 ... -> 25 -> next origin 0 -> 1 ...
1125- /// because the fee goes up with the nonce.
1126- /// (2) Discovery of nonce in the mempool iteration: this behavior allows the miner
1127- /// to consider an origin's "next" transaction immediately. Prior behavior would
1128- /// only do so after processing any other origin's transactions.
1129- fn test_build_anchored_blocks_incrementing_nonces ( ) {
1130- let private_keys: Vec < _ > = ( 0 ..10 ) . map ( |_| StacksPrivateKey :: random ( ) ) . collect ( ) ;
1131- let addresses: Vec < _ > = private_keys
1132- . iter ( )
1133- . map ( |sk| {
1134- StacksAddress :: from_public_keys (
1135- C32_ADDRESS_VERSION_TESTNET_SINGLESIG ,
1136- & AddressHashMode :: SerializeP2PKH ,
1137- 1 ,
1138- & vec ! [ StacksPublicKey :: from_private( sk) ] ,
1139- )
1140- . unwrap ( )
1141- } )
1142- . collect ( ) ;
1143-
1144- let initial_balances: Vec < _ > = addresses
1145- . iter ( )
1146- . map ( |addr| ( addr. to_account_principal ( ) , 100000000000 ) )
1147- . collect ( ) ;
1148-
1149- let mut peer_config = TestPeerConfig :: new ( function_name ! ( ) , 2030 , 2031 ) ;
1150- peer_config. initial_balances = initial_balances;
1151- let burnchain = peer_config. burnchain . clone ( ) ;
1152-
1153- let mut peer = TestPeer :: new ( peer_config) ;
1154-
1155- let chainstate_path = peer. chainstate_path . clone ( ) ;
1156-
1157- let mut mempool = MemPoolDB :: open_test ( false , 0x80000000 , & chainstate_path) . unwrap ( ) ;
1158-
1159- // during the tenure, let's push transactions to the mempool
1160- let tip =
1161- SortitionDB :: get_canonical_burn_chain_tip ( peer. sortdb . as_ref ( ) . unwrap ( ) . conn ( ) ) . unwrap ( ) ;
1162-
1163- let ( burn_ops, stacks_block, microblocks) = peer. make_tenure (
1164- |ref mut miner,
1165- ref mut sortdb,
1166- ref mut chainstate,
1167- vrf_proof,
1168- ref parent_opt,
1169- ref parent_microblock_header_opt| {
1170- let parent_tip = match parent_opt {
1171- None => StacksChainState :: get_genesis_header_info ( chainstate. db ( ) ) . unwrap ( ) ,
1172- Some ( block) => {
1173- let ic = sortdb. index_conn ( ) ;
1174- let snapshot = SortitionDB :: get_block_snapshot_for_winning_stacks_block (
1175- & ic,
1176- & tip. sortition_id ,
1177- & block. block_hash ( ) ,
1178- )
1179- . unwrap ( )
1180- . unwrap ( ) ; // succeeds because we don't fork
1181- StacksChainState :: get_anchored_block_header_info (
1182- chainstate. db ( ) ,
1183- & snapshot. consensus_hash ,
1184- & snapshot. winning_stacks_block_hash ,
1185- )
1186- . unwrap ( )
1187- . unwrap ( )
1188- }
1189- } ;
1190-
1191- let parent_header_hash = parent_tip. anchored_header . block_hash ( ) ;
1192- let parent_consensus_hash = parent_tip. consensus_hash . clone ( ) ;
1193- let coinbase_tx = make_coinbase ( miner, 0 ) ;
1194-
1195- let txs: Vec < _ > = private_keys
1196- . iter ( )
1197- . flat_map ( |privk| {
1198- let privk = privk. clone ( ) ;
1199- ( 0 ..25 ) . map ( move |tx_nonce| {
1200- let contract = "(define-data-var bar int 0)" ;
1201- make_user_contract_publish (
1202- & privk,
1203- tx_nonce,
1204- 200 * ( tx_nonce + 1 ) ,
1205- & format ! ( "contract-{}" , tx_nonce) ,
1206- contract,
1207- )
1208- } )
1209- } )
1210- . collect ( ) ;
1211-
1212- for tx in txs {
1213- mempool
1214- . submit (
1215- chainstate,
1216- sortdb,
1217- & parent_consensus_hash,
1218- & parent_header_hash,
1219- & tx,
1220- None ,
1221- & ExecutionCost :: max_value ( ) ,
1222- & StacksEpochId :: Epoch20 ,
1223- )
1224- . unwrap ( ) ;
1225- }
1226-
1227- let anchored_block = StacksBlockBuilder :: build_anchored_block (
1228- chainstate,
1229- & sortdb. index_handle_at_tip ( ) ,
1230- & mut mempool,
1231- & parent_tip,
1232- tip. total_burn ,
1233- vrf_proof,
1234- Hash160 ( [ 0 ; 20 ] ) ,
1235- & coinbase_tx,
1236- BlockBuilderSettings :: limited ( ) ,
1237- None ,
1238- & burnchain,
1239- )
1240- . unwrap ( ) ;
1241- ( anchored_block. 0 , vec ! [ ] )
1242- } ,
1243- ) ;
1244-
1245- peer. next_burnchain_block ( burn_ops) ;
1246- peer. process_stacks_epoch_at_tip ( & stacks_block, & microblocks) ;
1247-
1248- // expensive transaction was not mined, but the two stx-transfers were
1249- assert_eq ! ( stacks_block. txs. len( ) , 251 ) ;
1250-
1251- // block should be ordered like coinbase, nonce 0, nonce 1, .. nonce 25, nonce 0, ..
1252- // because the tx fee for each transaction increases with the nonce
1253- for ( i, tx) in stacks_block. txs . iter ( ) . enumerate ( ) {
1254- if i == 0 {
1255- let okay = matches ! ( tx. payload, TransactionPayload :: Coinbase ( ..) ) ;
1256- assert ! ( okay, "Coinbase should be first tx" ) ;
1257- } else {
1258- let expected_nonce = ( i - 1 ) % 25 ;
1259- assert_eq ! (
1260- tx. get_origin_nonce( ) ,
1261- expected_nonce as u64 ,
1262- "{i}th transaction should have nonce = {expected_nonce}" ,
1263- ) ;
1264- }
1265- }
1266- }
1267-
12681119#[ test]
12691120fn test_build_anchored_blocks_skip_too_expensive ( ) {
12701121 let privk = StacksPrivateKey :: from_hex (
@@ -5257,3 +5108,206 @@ fn mempool_walk_test_next_nonce_with_highest_fee_rate_strategy() {
52575108 } ,
52585109 ) ;
52595110}
5111+
5112+ /// Shared helper function to test different mempool walk strategies.
5113+ ///
5114+ /// This function creates a test scenario with multiple addresses (10), each sending
5115+ /// transactions with incrementing nonces (0-24) and fees (fee = 200 * (nonce + 1)).
5116+ /// It then builds a block using the specified mempool walk strategy and validates
5117+ /// the transaction ordering using the provided expectation function.
5118+ ///
5119+ /// The expectation function receives the transaction index (excluding coinbase) and
5120+ /// the complete block, and should return the expected nonce for the transaction at
5121+ /// that position according to the specific mempool walk strategy being tested.
5122+ fn run_mempool_walk_strategy_nonce_order_test < F > (
5123+ test_name : & str ,
5124+ strategy : MemPoolWalkStrategy ,
5125+ expected_nonce_fn : F ,
5126+ ) where
5127+ F : Fn ( usize , & StacksBlock ) -> u64 ,
5128+ {
5129+ let private_keys: Vec < _ > = ( 0 ..10 ) . map ( |_| StacksPrivateKey :: random ( ) ) . collect ( ) ;
5130+ let addresses: Vec < _ > = private_keys
5131+ . iter ( )
5132+ . map ( |sk| {
5133+ StacksAddress :: from_public_keys (
5134+ C32_ADDRESS_VERSION_TESTNET_SINGLESIG ,
5135+ & AddressHashMode :: SerializeP2PKH ,
5136+ 1 ,
5137+ & vec ! [ StacksPublicKey :: from_private( sk) ] ,
5138+ )
5139+ . unwrap ( )
5140+ } )
5141+ . collect ( ) ;
5142+
5143+ let initial_balances: Vec < _ > = addresses
5144+ . iter ( )
5145+ . map ( |addr| ( addr. to_account_principal ( ) , 100000000000 ) )
5146+ . collect ( ) ;
5147+
5148+ let mut peer_config = TestPeerConfig :: new ( test_name, 2030 , 2031 ) ;
5149+ peer_config. initial_balances = initial_balances;
5150+ let burnchain = peer_config. burnchain . clone ( ) ;
5151+
5152+ let mut peer = TestPeer :: new ( peer_config) ;
5153+ let chainstate_path = peer. chainstate_path . clone ( ) ;
5154+ let mut mempool = MemPoolDB :: open_test ( false , 0x80000000 , & chainstate_path) . unwrap ( ) ;
5155+
5156+ let tip =
5157+ SortitionDB :: get_canonical_burn_chain_tip ( peer. sortdb . as_ref ( ) . unwrap ( ) . conn ( ) ) . unwrap ( ) ;
5158+
5159+ let ( burn_ops, stacks_block, microblocks) = peer. make_tenure (
5160+ |ref mut miner,
5161+ ref mut sortdb,
5162+ ref mut chainstate,
5163+ vrf_proof,
5164+ ref parent_opt,
5165+ ref parent_microblock_header_opt| {
5166+ let parent_tip = match parent_opt {
5167+ None => StacksChainState :: get_genesis_header_info ( chainstate. db ( ) ) . unwrap ( ) ,
5168+ Some ( block) => {
5169+ let ic = sortdb. index_conn ( ) ;
5170+ let snapshot = SortitionDB :: get_block_snapshot_for_winning_stacks_block (
5171+ & ic,
5172+ & tip. sortition_id ,
5173+ & block. block_hash ( ) ,
5174+ )
5175+ . unwrap ( )
5176+ . unwrap ( ) ;
5177+ StacksChainState :: get_anchored_block_header_info (
5178+ chainstate. db ( ) ,
5179+ & snapshot. consensus_hash ,
5180+ & snapshot. winning_stacks_block_hash ,
5181+ )
5182+ . unwrap ( )
5183+ . unwrap ( )
5184+ }
5185+ } ;
5186+
5187+ let parent_header_hash = parent_tip. anchored_header . block_hash ( ) ;
5188+ let parent_consensus_hash = parent_tip. consensus_hash . clone ( ) ;
5189+ let coinbase_tx = make_coinbase ( miner, 0 ) ;
5190+
5191+ // Create 25 transactions per address with incrementing fees
5192+ let txs: Vec < _ > = private_keys
5193+ . iter ( )
5194+ . flat_map ( |privk| {
5195+ let privk = privk. clone ( ) ;
5196+ ( 0 ..25 ) . map ( move |tx_nonce| {
5197+ let contract = "(define-data-var bar int 0)" ;
5198+ make_user_contract_publish (
5199+ & privk,
5200+ tx_nonce,
5201+ 200 * ( tx_nonce + 1 ) , // Higher nonce = higher fee
5202+ & format ! ( "contract-{}" , tx_nonce) ,
5203+ contract,
5204+ )
5205+ } )
5206+ } )
5207+ . collect ( ) ;
5208+
5209+ for tx in txs {
5210+ mempool
5211+ . submit (
5212+ chainstate,
5213+ sortdb,
5214+ & parent_consensus_hash,
5215+ & parent_header_hash,
5216+ & tx,
5217+ None ,
5218+ & ExecutionCost :: max_value ( ) ,
5219+ & StacksEpochId :: Epoch20 ,
5220+ )
5221+ . unwrap ( ) ;
5222+ }
5223+
5224+ // Build block with specified strategy
5225+ let mut settings = BlockBuilderSettings :: limited ( ) ;
5226+ settings. mempool_settings . strategy = strategy;
5227+
5228+ let anchored_block = StacksBlockBuilder :: build_anchored_block (
5229+ chainstate,
5230+ & sortdb. index_handle_at_tip ( ) ,
5231+ & mut mempool,
5232+ & parent_tip,
5233+ tip. total_burn ,
5234+ vrf_proof,
5235+ Hash160 ( [ 0 ; 20 ] ) ,
5236+ & coinbase_tx,
5237+ settings,
5238+ None ,
5239+ & burnchain,
5240+ )
5241+ . unwrap ( ) ;
5242+ ( anchored_block. 0 , vec ! [ ] )
5243+ } ,
5244+ ) ;
5245+
5246+ peer. next_burnchain_block ( burn_ops) ;
5247+ peer. process_stacks_epoch_at_tip ( & stacks_block, & microblocks) ;
5248+
5249+ // Verify we got the expected number of transactions (250 + 1 coinbase)
5250+ assert_eq ! ( stacks_block. txs. len( ) , 251 ) ;
5251+
5252+ // Verify transaction ordering matches the expected strategy behavior
5253+ for ( i, tx) in stacks_block. txs . iter ( ) . enumerate ( ) {
5254+ if i == 0 {
5255+ let okay = matches ! ( tx. payload, TransactionPayload :: Coinbase ( ..) ) ;
5256+ assert ! ( okay, "Coinbase should be first tx" ) ;
5257+ } else {
5258+ // i is 1-indexed, so we need to subtract 1 for the coinbase
5259+ let expected_nonce = expected_nonce_fn ( i - 1 , & stacks_block) ;
5260+ assert_eq ! (
5261+ tx. get_origin_nonce( ) ,
5262+ expected_nonce,
5263+ "{i}th transaction should have nonce = {expected_nonce} with strategy {:?}" ,
5264+ strategy
5265+ ) ;
5266+ }
5267+ }
5268+ }
5269+
5270+ #[ test]
5271+ /// Tests block assembly with the `GlobalFeeRate` mempool walk strategy.
5272+ ///
5273+ /// Scenario: 10 accounts, 25 transactions each (nonces 0-24), fees increase with nonce.
5274+ ///
5275+ /// Expected Behavior:
5276+ /// This strategy selects the highest-fee *ready* transaction globally.
5277+ /// Since transaction fees are `200 * (nonce + 1)`, an account's nonce `N+1`
5278+ /// transaction has a higher fee than its nonce `N` transaction.
5279+ /// Consequently, after Account A's nonce 0 transaction is processed, its now-ready
5280+ /// nonce 1 transaction (fee `200*2=400`) will be preferred over Account B's
5281+ /// pending nonce 0 transaction (fee `200*1=200`).
5282+ /// This results in one account's transactions being processed sequentially
5283+ /// (e.g., A0, A1, ..., A24) before moving to the next account (B0, B1, ..., B24).
5284+ fn test_build_anchored_blocks_nonce_order_global_fee_rate_strategy ( ) {
5285+ run_mempool_walk_strategy_nonce_order_test (
5286+ function_name ! ( ) ,
5287+ MemPoolWalkStrategy :: GlobalFeeRate ,
5288+ // Expected: 0,1,..,24 (for acc1), then 0,1,..,24 (for acc2), ...
5289+ |tx_index, _| ( tx_index % 25 ) as u64 ,
5290+ ) ;
5291+ }
5292+
5293+ #[ test]
5294+ /// Tests block assembly with the `NextNonceWithHighestFeeRate` mempool walk strategy.
5295+ ///
5296+ /// Scenario: 10 accounts, 25 transactions each (nonces 0-24), fees increase with nonce.
5297+ ///
5298+ /// Expected Behavior:
5299+ /// This strategy prioritizes transactions that match the next expected nonce for each
5300+ /// account, then (secondarily) by fee rate within that group of "next nonce" transactions.
5301+ /// This directly results in transactions being ordered by "nonce rounds" in the block:
5302+ /// all nonce 0 transactions from all accounts first, then all nonce 1s, and so on.
5303+ fn test_build_anchored_blocks_nonce_order_next_nonce_with_highest_fee_rate_strategy ( ) {
5304+ run_mempool_walk_strategy_nonce_order_test (
5305+ function_name ! ( ) ,
5306+ MemPoolWalkStrategy :: NextNonceWithHighestFeeRate ,
5307+ |tx_index, _| {
5308+ // Expected nonce sequence: 0,0,...,0 (10 times), then 1,1,...,1 (10 times), ...
5309+ // Each group of 10 transactions corresponds to one nonce value, across all 10 accounts.
5310+ ( tx_index / 10 ) as u64
5311+ } ,
5312+ ) ;
5313+ }
0 commit comments