@@ -1116,155 +1116,6 @@ fn test_build_anchored_blocks_connected_by_microblocks_across_epoch_invalid() {
1116
1116
assert_eq ! ( last_block. header. total_work. work, 10 ) ; // mined a chain successfully across the epoch boundary
1117
1117
}
1118
1118
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
-
1268
1119
#[ test]
1269
1120
fn test_build_anchored_blocks_skip_too_expensive ( ) {
1270
1121
let privk = StacksPrivateKey :: from_hex (
@@ -5257,3 +5108,206 @@ fn mempool_walk_test_next_nonce_with_highest_fee_rate_strategy() {
5257
5108
} ,
5258
5109
) ;
5259
5110
}
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