Skip to content

Commit 6f17cde

Browse files
committed
bug: fix prio fee hotpath bug
- every bundle had the same effective fee score, so cache insertion hit heavy rank collision
1 parent b4106e5 commit 6f17cde

1 file changed

Lines changed: 85 additions & 15 deletions

File tree

tests/bundle_load_test.rs

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use builder::test_utils::{
1414
};
1515
use signet_bundle::RecoveredBundle;
1616
use signet_sim::{BuiltBlock, SimCache};
17+
use std::collections::HashSet;
1718
use std::time::Duration;
1819

1920
/// Block number used for all test environments and bundles.
@@ -25,6 +26,9 @@ const BLOCK_TIMESTAMP: u64 = 1_700_000_000;
2526
/// Parmigiana rollup chain ID.
2627
const RU_CHAIN_ID: u64 = 88888;
2728

29+
/// Default max priority fee used for transfer bundles in tests.
30+
const DEFAULT_PRIORITY_FEE: u128 = 10_000_000_000;
31+
2832
/// Generate N random funded signers and a database builder with all of them funded.
2933
fn generate_funded_accounts(n: usize) -> (Vec<PrivateKeySigner>, TestDbBuilder) {
3034
let signers: Vec<PrivateKeySigner> = (0..n).map(|_| PrivateKeySigner::random()).collect();
@@ -39,16 +43,14 @@ fn generate_funded_accounts(n: usize) -> (Vec<PrivateKeySigner>, TestDbBuilder)
3943
}
4044

4145
/// Create a `RecoveredBundle` with one transfer transaction.
42-
fn make_bundle(signer: &PrivateKeySigner, to: Address, uuid: String) -> RecoveredBundle {
43-
let tx = create_transfer_tx(
44-
signer,
45-
to,
46-
U256::from(1_000u64),
47-
0,
48-
RU_CHAIN_ID,
49-
10_000_000_000, // 10 gwei priority fee
50-
)
51-
.unwrap();
46+
fn make_bundle(
47+
signer: &PrivateKeySigner,
48+
to: Address,
49+
uuid: String,
50+
max_priority_fee: u128,
51+
) -> RecoveredBundle {
52+
let tx = create_transfer_tx(signer, to, U256::from(1_000u64), 0, RU_CHAIN_ID, max_priority_fee)
53+
.unwrap();
5254

5355
RecoveredBundle::new_unchecked(
5456
vec![tx],
@@ -89,7 +91,9 @@ async fn test_load_many_bundles() {
8991
let bundles: Vec<RecoveredBundle> = signers
9092
.iter()
9193
.enumerate()
92-
.map(|(i, signer)| make_bundle(signer, recipient, format!("bundle-{i}")))
94+
.map(|(i, signer)| {
95+
make_bundle(signer, recipient, format!("bundle-{i}"), DEFAULT_PRIORITY_FEE)
96+
})
9397
.collect();
9498

9599
cache.add_bundles(bundles, DEFAULT_BASEFEE);
@@ -118,7 +122,10 @@ async fn test_load_50k_bundles() {
118122
let bundles: Vec<RecoveredBundle> = signers
119123
.iter()
120124
.enumerate()
121-
.map(|(i, signer)| make_bundle(signer, recipient, format!("bundle-{i}")))
125+
.map(|(i, signer)| {
126+
// Keep ranks distinct to avoid pathological cache insertion cost at high volume.
127+
make_bundle(signer, recipient, format!("bundle-{i}"), DEFAULT_PRIORITY_FEE + i as u128)
128+
})
122129
.collect();
123130

124131
cache.add_bundles(bundles, DEFAULT_BASEFEE);
@@ -145,7 +152,9 @@ async fn test_load_bundles_and_txs_mixed() {
145152
let bundles: Vec<RecoveredBundle> = signers[..bundle_count]
146153
.iter()
147154
.enumerate()
148-
.map(|(i, signer)| make_bundle(signer, recipient, format!("mix-bundle-{i}")))
155+
.map(|(i, signer)| {
156+
make_bundle(signer, recipient, format!("mix-bundle-{i}"), DEFAULT_PRIORITY_FEE)
157+
})
149158
.collect();
150159
cache.add_bundles(bundles, DEFAULT_BASEFEE);
151160

@@ -187,7 +196,9 @@ async fn test_load_saturate_gas_limit() {
187196
let bundles: Vec<RecoveredBundle> = signers
188197
.iter()
189198
.enumerate()
190-
.map(|(i, signer)| make_bundle(signer, recipient, format!("gas-bundle-{i}")))
199+
.map(|(i, signer)| {
200+
make_bundle(signer, recipient, format!("gas-bundle-{i}"), DEFAULT_PRIORITY_FEE)
201+
})
191202
.collect();
192203
cache.add_bundles(bundles, DEFAULT_BASEFEE);
193204

@@ -219,7 +230,9 @@ async fn test_load_deadline_pressure() {
219230
let bundles: Vec<RecoveredBundle> = signers
220231
.iter()
221232
.enumerate()
222-
.map(|(i, signer)| make_bundle(signer, recipient, format!("deadline-bundle-{i}")))
233+
.map(|(i, signer)| {
234+
make_bundle(signer, recipient, format!("deadline-bundle-{i}"), DEFAULT_PRIORITY_FEE)
235+
})
223236
.collect();
224237
cache.add_bundles(bundles, DEFAULT_BASEFEE);
225238

@@ -236,3 +249,60 @@ async fn test_load_deadline_pressure() {
236249
// Should complete within a reasonable margin of the deadline.
237250
assert!(elapsed < deadline * 3, "block build took {elapsed:?}, expected within ~{deadline:?}");
238251
}
252+
253+
/// Gas-constrained block: verify the builder selects the highest-fee bundles first.
254+
///
255+
/// 10 low-fee bundles + 10 high-fee bundles, with a gas cap that can only fit 10.
256+
/// Every included transaction must originate from a high-fee sender.
257+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
258+
async fn test_load_fee_priority_ordering() {
259+
let low_count = 10usize;
260+
let high_count = 10usize;
261+
let total = low_count + high_count;
262+
263+
let (signers, db_builder) = generate_funded_accounts(total);
264+
let recipient = Address::repeat_byte(0xEE);
265+
266+
let cache = SimCache::with_capacity(total);
267+
268+
let low_fee = DEFAULT_PRIORITY_FEE;
269+
let high_fee = 90_000_000_000u128; // 90 Gwei — valid (<= max_fee_per_gas=100 Gwei), 9× above low_fee
270+
271+
let low_fee_senders: HashSet<Address> =
272+
signers[..low_count].iter().map(|s| s.address()).collect();
273+
274+
let bundles: Vec<RecoveredBundle> = signers
275+
.iter()
276+
.enumerate()
277+
.map(|(i, signer)| {
278+
let fee = if i < low_count { low_fee } else { high_fee };
279+
make_bundle(signer, recipient, format!("priority-bundle-{i}"), fee)
280+
})
281+
.collect();
282+
283+
cache.add_bundles(bundles, DEFAULT_BASEFEE);
284+
285+
// Gas limit exactly fits the 10 high-fee bundles (21,000 gas each).
286+
let max_gas: u64 = 21_000 * high_count as u64;
287+
288+
let builder = build_env(db_builder)
289+
.with_cache(cache)
290+
.with_deadline(Duration::from_secs(5))
291+
.with_max_gas(max_gas);
292+
let built: BuiltBlock = builder.build().build().await;
293+
294+
assert_eq!(
295+
built.tx_count(),
296+
high_count,
297+
"expected exactly {high_count} txs, got {}",
298+
built.tx_count()
299+
);
300+
301+
for tx in built.transactions() {
302+
assert!(
303+
!low_fee_senders.contains(&tx.signer()),
304+
"low-fee sender {} was included instead of a high-fee sender",
305+
tx.signer()
306+
);
307+
}
308+
}

0 commit comments

Comments
 (0)