Skip to content

Commit c3d8cc8

Browse files
committed
extend test_build_anchored_blocks_incrementing_nonces for NextNonceWithHighestFeeRate
1 parent ce66233 commit c3d8cc8

File tree

1 file changed

+203
-149
lines changed

1 file changed

+203
-149
lines changed

stackslib/src/chainstate/stacks/tests/block_construction.rs

Lines changed: 203 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -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]
12821133
fn 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

Comments
 (0)