@@ -1129,155 +1129,6 @@ fn test_build_anchored_blocks_connected_by_microblocks_across_epoch_invalid() {
1129
1129
assert_eq ! ( last_block. header. total_work. work, 10 ) ; // mined a chain successfully across the epoch boundary
1130
1130
}
1131
1131
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
-
1281
1132
#[ test]
1282
1133
fn test_build_anchored_blocks_skip_too_expensive ( ) {
1283
1134
let privk = StacksPrivateKey :: from_hex (
@@ -5270,3 +5121,206 @@ fn mempool_walk_test_next_nonce_with_highest_fee_rate_strategy() {
5270
5121
} ,
5271
5122
) ;
5272
5123
}
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