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