Skip to content

Commit 2493ed1

Browse files
authored
Merge pull request #6059 from obycode/default-mempool-walk-strategy
chore: update default mempool walk strategy
2 parents 42faabe + 9c59680 commit 2493ed1

File tree

7 files changed

+425
-207
lines changed

7 files changed

+425
-207
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to the versioning scheme outlined in the [README.md](README.md).
77

8-
## Unreleased
8+
# Unreleased
99

1010
### Added
1111

1212
- Added a new RPC endpoint `/v3/health` to query the node's health status. The endpoint returns a 200 status code with relevant synchronization information (including the node's current Stacks tip height, the maximum Stacks tip height among its neighbors, and the difference between these two). A user can use the `difference_from_max_peer` value to decide what is a good threshold for them before considering the node out of sync. The endpoint returns a 500 status code if the query cannot retrieve viable data.
1313

14+
### Changed
15+
16+
- Changed default mempool walk strategy to `NextNonceWithHighestFeeRate`
17+
1418
## [3.1.0.0.12]
1519

1620
### Added

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

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

Comments
 (0)