Skip to content

Commit 8543ea2

Browse files
committed
Fix code review issues and improve API clarity
- Fix O(n²) index rebuild in cache eviction by moving rebuild outside loop - Add --metering-state-root-time-us CLI arg for forward compatibility - Reduce cloning in estimator by using reference slices for aggregation - Fix supporting_transactions to return 0 when no txs fit (was incorrectly 1) - Export ResourceFeeEstimateResponse from public API - Rename supporting_transactions to threshold_tx_count for clarity
1 parent e5f367d commit 8543ea2

File tree

5 files changed

+83
-57
lines changed

5 files changed

+83
-57
lines changed

crates/metering/src/cache.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,12 +273,15 @@ impl MeteringCache {
273273
}
274274

275275
fn evict_if_needed(&mut self) {
276+
let mut evicted = false;
276277
while self.blocks.len() > self.max_blocks {
277278
if let Some(oldest) = self.blocks.pop_front() {
278279
self.block_index.remove(&oldest.block_number);
280+
evicted = true;
279281
}
280-
281-
// Rebuild index to maintain correctness.
282+
}
283+
// Rebuild index once after all evictions to maintain correctness.
284+
if evicted {
282285
self.rebuild_index();
283286
}
284287
}

crates/metering/src/estimator.rs

Lines changed: 64 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,11 @@ pub struct ResourceEstimate {
137137
/// Recommended fee based on a percentile of transactions above the threshold.
138138
/// Provides a safety margin over the bare minimum.
139139
pub recommended_priority_fee: U256,
140-
/// Total resource usage of transactions at or below the threshold.
140+
/// Total resource usage of transactions at or above the threshold fee.
141141
pub cumulative_usage: u128,
142-
/// Number of transactions that would be displaced at the threshold fee.
143-
pub supporting_transactions: usize,
142+
/// Number of transactions at or above `threshold_priority_fee`. These higher-paying
143+
/// transactions remain included alongside the bundle; lower-paying ones are displaced.
144+
pub threshold_tx_count: usize,
144145
/// Total transactions considered in the estimate.
145146
pub total_transactions: usize,
146147
}
@@ -282,6 +283,7 @@ impl PriorityFeeEstimator {
282283
// Materialise sorted transactions per flashblock so we can drop the lock before
283284
// running the estimation logic.
284285
let mut flashblock_transactions = Vec::new();
286+
let mut total_tx_count = 0usize;
285287
for flashblock in block_metrics.flashblocks() {
286288
let sorted: Vec<MeteredTransaction> = flashblock
287289
.transactions_sorted_by_priority_fee()
@@ -291,6 +293,7 @@ impl PriorityFeeEstimator {
291293
if sorted.is_empty() {
292294
continue;
293295
}
296+
total_tx_count += sorted.len();
294297
flashblock_transactions.push((flashblock.flashblock_index, sorted));
295298
}
296299
drop(cache_guard);
@@ -299,16 +302,20 @@ impl PriorityFeeEstimator {
299302
return Ok(None);
300303
}
301304

302-
// Flatten into a single list for use-it-or-lose-it resources.
303-
let mut aggregate_transactions: Vec<MeteredTransaction> = flashblock_transactions
304-
.iter()
305-
.flat_map(|(_, txs)| txs.iter().cloned())
306-
.collect();
307-
aggregate_transactions.sort_by(|a, b| a.priority_fee_per_gas.cmp(&b.priority_fee_per_gas));
305+
// Build the aggregate list for use-it-or-lose-it resources by collecting references
306+
// to avoid cloning transactions twice.
307+
let mut aggregate_refs: Vec<&MeteredTransaction> = Vec::with_capacity(total_tx_count);
308+
for (_, txs) in &flashblock_transactions {
309+
aggregate_refs.extend(txs.iter());
310+
}
311+
aggregate_refs.sort_by(|a, b| a.priority_fee_per_gas.cmp(&b.priority_fee_per_gas));
308312

309313
let mut flashblock_estimates = Vec::new();
310314

311-
for (flashblock_index, txs) in flashblock_transactions {
315+
for (flashblock_index, txs) in &flashblock_transactions {
316+
// Build a reference slice for this flashblock's transactions.
317+
let txs_refs: Vec<&MeteredTransaction> = txs.iter().collect();
318+
312319
let mut estimates = ResourceEstimates::default();
313320
for resource in ResourceKind::all() {
314321
let Some(demand_value) = demand.demand_for(resource) else {
@@ -318,10 +325,10 @@ impl PriorityFeeEstimator {
318325
continue;
319326
};
320327

321-
let transactions = if resource.use_it_or_lose_it() {
322-
&aggregate_transactions
328+
let transactions: &[&MeteredTransaction] = if resource.use_it_or_lose_it() {
329+
&aggregate_refs
323330
} else {
324-
&txs
331+
&txs_refs
325332
};
326333
let estimate = compute_estimate(
327334
resource,
@@ -337,7 +344,7 @@ impl PriorityFeeEstimator {
337344
}
338345

339346
flashblock_estimates.push(FlashblockResourceEstimates {
340-
flashblock_index,
347+
flashblock_index: *flashblock_index,
341348
estimates,
342349
});
343350
}
@@ -410,7 +417,7 @@ impl PriorityFeeEstimator {
410417
threshold_priority_fee: median_fee,
411418
recommended_priority_fee: median_fee,
412419
cumulative_usage: 0,
413-
supporting_transactions: 0,
420+
threshold_tx_count: 0,
414421
total_transactions: 0,
415422
},
416423
);
@@ -464,7 +471,7 @@ impl PriorityFeeEstimator {
464471
/// Returns `Err` if the bundle's demand exceeds the resource limit.
465472
fn compute_estimate(
466473
resource: ResourceKind,
467-
transactions: &[MeteredTransaction],
474+
transactions: &[&MeteredTransaction],
468475
demand: u128,
469476
limit: u128,
470477
usage_fn: fn(&MeteredTransaction) -> u128,
@@ -486,13 +493,13 @@ fn compute_estimate(
486493
threshold_priority_fee: default_fee,
487494
recommended_priority_fee: default_fee,
488495
cumulative_usage: 0,
489-
supporting_transactions: 0,
496+
threshold_tx_count: 0,
490497
total_transactions: 0,
491498
});
492499
}
493500

494501
// Sort transactions by priority fee descending (highest first).
495-
let mut sorted: Vec<_> = transactions.iter().collect();
502+
let mut sorted: Vec<_> = transactions.to_vec();
496503
sorted.sort_by(|a, b| b.priority_fee_per_gas.cmp(&a.priority_fee_per_gas));
497504

498505
// Walk from highest-paying transactions, subtracting usage from remaining capacity.
@@ -523,41 +530,45 @@ fn compute_estimate(
523530
threshold_priority_fee: default_fee,
524531
recommended_priority_fee: default_fee,
525532
cumulative_usage: included_usage,
526-
supporting_transactions: sorted.len(),
533+
threshold_tx_count: sorted.len(),
527534
total_transactions: sorted.len(),
528535
});
529536
}
530537

531-
let (threshold_idx, threshold_fee) = match last_included_idx {
538+
let (supporting_count, threshold_fee, recommended_fee) = match last_included_idx {
532539
Some(idx) => {
533540
// At least one transaction fits alongside the bundle.
534541
// The threshold is the fee of the last included transaction.
535-
(idx, sorted[idx].priority_fee_per_gas)
542+
let threshold_fee = sorted[idx].priority_fee_per_gas;
543+
544+
// For recommended fee, look at included transactions (those above threshold)
545+
// and pick one at the specified percentile for a safety margin.
546+
let included = &sorted[..=idx];
547+
let percentile = percentile.clamp(0.0, 1.0);
548+
let recommended_fee = if included.len() <= 1 {
549+
threshold_fee
550+
} else {
551+
// Pick from the higher end of included transactions for safety.
552+
let pos = ((included.len() - 1) as f64 * (1.0 - percentile)).round() as usize;
553+
included[pos.min(included.len() - 1)].priority_fee_per_gas
554+
};
555+
556+
(idx + 1, threshold_fee, recommended_fee)
536557
}
537558
None => {
538559
// No transactions fit - even the first transaction would crowd out
539560
// the bundle. The bundle must beat the highest fee to be included.
540-
(0, sorted[0].priority_fee_per_gas)
561+
// Report 0 supporting transactions since none were actually included.
562+
let threshold_fee = sorted[0].priority_fee_per_gas;
563+
(0, threshold_fee, threshold_fee)
541564
}
542565
};
543566

544-
// For recommended fee, look at included transactions (those above threshold)
545-
// and pick one at the specified percentile for a safety margin.
546-
let included = &sorted[..=threshold_idx];
547-
let percentile = percentile.clamp(0.0, 1.0);
548-
let recommended_fee = if included.len() <= 1 {
549-
threshold_fee
550-
} else {
551-
// Pick from the higher end of included transactions for safety.
552-
let pos = ((included.len() - 1) as f64 * (1.0 - percentile)).round() as usize;
553-
included[pos.min(included.len() - 1)].priority_fee_per_gas
554-
};
555-
556567
Ok(ResourceEstimate {
557568
threshold_priority_fee: threshold_fee,
558569
recommended_priority_fee: recommended_fee,
559570
cumulative_usage: included_usage,
560-
supporting_transactions: threshold_idx + 1,
571+
threshold_tx_count: supporting_count,
561572
total_transactions: sorted.len(),
562573
})
563574
}
@@ -645,9 +656,10 @@ mod tests {
645656
// - Include tx priority=5: remaining = 20-10 = 10 < 15 ✗ (stop)
646657
// Threshold = 10 (the last included tx's fee)
647658
let txs = vec![tx(10, 10), tx(5, 10), tx(2, 10)];
659+
let txs_refs: Vec<&MeteredTransaction> = txs.iter().collect();
648660
let quote = compute_estimate(
649661
ResourceKind::GasUsed,
650-
&txs,
662+
&txs_refs,
651663
15,
652664
30, // limit
653665
usage_extractor(ResourceKind::GasUsed),
@@ -657,7 +669,7 @@ mod tests {
657669
.expect("no error");
658670
assert_eq!(quote.threshold_priority_fee, U256::from(10));
659671
assert_eq!(quote.cumulative_usage, 10); // Only the first tx was included
660-
assert_eq!(quote.supporting_transactions, 1);
672+
assert_eq!(quote.threshold_tx_count, 1);
661673
assert_eq!(quote.total_transactions, 3);
662674
}
663675

@@ -666,9 +678,10 @@ mod tests {
666678
// Limit: 100, Demand: 15
667679
// All transactions fit with room to spare → return default fee
668680
let txs = vec![tx(10, 10), tx(5, 10), tx(2, 10)];
681+
let txs_refs: Vec<&MeteredTransaction> = txs.iter().collect();
669682
let quote = compute_estimate(
670683
ResourceKind::GasUsed,
671-
&txs,
684+
&txs_refs,
672685
15,
673686
100, // limit is much larger than total usage
674687
usage_extractor(ResourceKind::GasUsed),
@@ -679,16 +692,17 @@ mod tests {
679692
assert_eq!(quote.threshold_priority_fee, DEFAULT_FEE);
680693
assert_eq!(quote.recommended_priority_fee, DEFAULT_FEE);
681694
assert_eq!(quote.cumulative_usage, 30); // All txs included
682-
assert_eq!(quote.supporting_transactions, 3);
695+
assert_eq!(quote.threshold_tx_count, 3);
683696
}
684697

685698
#[test]
686699
fn compute_estimate_demand_exceeds_limit() {
687700
// Demand > Limit → Error
688701
let txs = vec![tx(10, 10), tx(5, 10)];
702+
let txs_refs: Vec<&MeteredTransaction> = txs.iter().collect();
689703
let result = compute_estimate(
690704
ResourceKind::GasUsed,
691-
&txs,
705+
&txs_refs,
692706
50, // demand
693707
30, // limit
694708
usage_extractor(ResourceKind::GasUsed),
@@ -712,9 +726,10 @@ mod tests {
712726
// After including tx priority=10: remaining = 20 >= 20 ✓
713727
// After including tx priority=5: remaining = 10 < 20 ✗
714728
let txs = vec![tx(10, 10), tx(5, 10)];
729+
let txs_refs: Vec<&MeteredTransaction> = txs.iter().collect();
715730
let quote = compute_estimate(
716731
ResourceKind::GasUsed,
717-
&txs,
732+
&txs_refs,
718733
20,
719734
30,
720735
usage_extractor(ResourceKind::GasUsed),
@@ -724,16 +739,17 @@ mod tests {
724739
.expect("no error");
725740
assert_eq!(quote.threshold_priority_fee, U256::from(10));
726741
assert_eq!(quote.cumulative_usage, 10);
727-
assert_eq!(quote.supporting_transactions, 1);
742+
assert_eq!(quote.threshold_tx_count, 1);
728743
}
729744

730745
#[test]
731746
fn compute_estimate_single_transaction() {
732747
// Single tx that fits
733748
let txs = vec![tx(10, 10)];
749+
let txs_refs: Vec<&MeteredTransaction> = txs.iter().collect();
734750
let quote = compute_estimate(
735751
ResourceKind::GasUsed,
736-
&txs,
752+
&txs_refs,
737753
15,
738754
30,
739755
usage_extractor(ResourceKind::GasUsed),
@@ -752,9 +768,10 @@ mod tests {
752768
// Limit: 25, Demand: 20
753769
// First tx uses 10, remaining = 15 < 20 → can't even include first tx
754770
let txs = vec![tx(10, 10), tx(5, 10)];
771+
let txs_refs: Vec<&MeteredTransaction> = txs.iter().collect();
755772
let quote = compute_estimate(
756773
ResourceKind::GasUsed,
757-
&txs,
774+
&txs_refs,
758775
20,
759776
25,
760777
usage_extractor(ResourceKind::GasUsed),
@@ -765,16 +782,16 @@ mod tests {
765782
// Need to beat the highest fee since we couldn't include any tx
766783
assert_eq!(quote.threshold_priority_fee, U256::from(10));
767784
assert_eq!(quote.cumulative_usage, 0);
768-
assert_eq!(quote.supporting_transactions, 1); // Reports first tx as threshold
785+
assert_eq!(quote.threshold_tx_count, 0); // No transactions can coexist
769786
}
770787

771788
#[test]
772789
fn compute_estimate_empty_transactions() {
773790
// No transactions = uncongested, return default fee
774-
let txs: Vec<MeteredTransaction> = vec![];
791+
let txs_refs: Vec<&MeteredTransaction> = vec![];
775792
let quote = compute_estimate(
776793
ResourceKind::GasUsed,
777-
&txs,
794+
&txs_refs,
778795
15,
779796
30,
780797
usage_extractor(ResourceKind::GasUsed),

crates/metering/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,7 @@ pub use estimator::{
1818
};
1919
pub use kafka::{KafkaBundleConsumer, KafkaBundleConsumerConfig};
2020
pub use meter::meter_bundle;
21-
pub use rpc::{MeteredPriorityFeeResponse, MeteringApiImpl, MeteringApiServer};
21+
pub use rpc::{
22+
MeteredPriorityFeeResponse, MeteringApiImpl, MeteringApiServer, ResourceFeeEstimateResponse,
23+
};
2224
pub use tips_core::types::{Bundle, MeterBundleResponse, TransactionResult};

crates/metering/src/rpc.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ pub struct ResourceFeeEstimateResponse {
2727
pub threshold_priority_fee: String,
2828
pub recommended_priority_fee: String,
2929
pub cumulative_usage: String,
30-
pub supporting_transactions: u64,
30+
pub threshold_tx_count: u64,
3131
pub total_transactions: u64,
3232
}
3333

@@ -256,10 +256,7 @@ fn to_response(resource: ResourceKind, estimate: &ResourceEstimate) -> ResourceF
256256
threshold_priority_fee: estimate.threshold_priority_fee.to_string(),
257257
recommended_priority_fee: estimate.recommended_priority_fee.to_string(),
258258
cumulative_usage: estimate.cumulative_usage.to_string(),
259-
supporting_transactions: estimate
260-
.supporting_transactions
261-
.try_into()
262-
.unwrap_or(u64::MAX),
259+
threshold_tx_count: estimate.threshold_tx_count.try_into().unwrap_or(u64::MAX),
263260
total_transactions: estimate.total_transactions.try_into().unwrap_or(u64::MAX),
264261
}
265262
}

crates/node/src/main.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ const DEFAULT_EXECUTION_TIME_US: u128 = 50_000;
2626
/// Default data availability limit per flashblock in bytes (120KB).
2727
const DEFAULT_DA_BYTES: u64 = 120_000;
2828

29+
/// Default state root time budget per block in microseconds (disabled by default).
30+
const DEFAULT_STATE_ROOT_TIME_US: Option<u128> = None;
31+
2932
/// Default priority fee returned when a resource is not congested (1 wei).
3033
const DEFAULT_UNCONGESTED_PRIORITY_FEE: u64 = 1;
3134

@@ -112,6 +115,11 @@ struct Args {
112115
#[arg(long = "metering-execution-time-us", default_value_t = DEFAULT_EXECUTION_TIME_US)]
113116
pub metering_execution_time_us: u128,
114117

118+
/// State root time budget per block in microseconds for priority fee estimation.
119+
/// Not used until per-transaction state-root timing is available in the TIPS Kafka schema.
120+
#[arg(long = "metering-state-root-time-us")]
121+
pub metering_state_root_time_us: Option<u128>,
122+
115123
/// Data availability limit per flashblock in bytes for priority fee estimation.
116124
#[arg(long = "metering-da-bytes", default_value_t = DEFAULT_DA_BYTES)]
117125
pub metering_da_bytes: u64,
@@ -166,7 +174,6 @@ struct MeteringKafkaSettings {
166174
}
167175

168176
#[derive(Clone)]
169-
#[allow(dead_code)]
170177
struct MeteringRuntime {
171178
cache: Arc<RwLock<MeteringCache>>,
172179
estimator: Arc<PriorityFeeEstimator>,
@@ -286,7 +293,7 @@ fn main() {
286293
let limits = ResourceLimits {
287294
gas_used: Some(args.metering_gas_limit),
288295
execution_time_us: Some(args.metering_execution_time_us),
289-
state_root_time_us: None,
296+
state_root_time_us: args.metering_state_root_time_us,
290297
data_availability_bytes: Some(args.metering_da_bytes),
291298
};
292299
let estimator = Arc::new(PriorityFeeEstimator::new(

0 commit comments

Comments
 (0)