Skip to main content

anvil/eth/
fees.rs

1use std::{
2    collections::BTreeMap,
3    fmt,
4    pin::Pin,
5    sync::{Arc, LazyLock},
6    task::{Context, Poll},
7};
8
9use alloy_consensus::{BlockHeader, Transaction, TxReceipt};
10use alloy_eips::{calc_next_block_base_fee, eip1559::BaseFeeParams, eip7840::BlobParams};
11use alloy_network::Network;
12use alloy_primitives::B256;
13use futures::StreamExt;
14use parking_lot::{Mutex, RwLock};
15use revm::{context_interface::block::BlobExcessGasAndPrice, primitives::hardfork::SpecId};
16use tempo_hardfork::{TempoHardfork, constants::gas::tempo_t7_next_block_base_fee};
17
18use crate::eth::{
19    backend::{info::StorageInfo, notifications::ChainNotifications},
20    error::BlockchainError,
21};
22
23/// Maximum number of entries in the fee history cache
24pub const MAX_FEE_HISTORY_CACHE_SIZE: u64 = 2048u64;
25
26/// Initial base fee for EIP-1559 blocks.
27pub const INITIAL_BASE_FEE: u64 = 1_000_000_000;
28
29/// Initial default gas price for the first block
30pub const INITIAL_GAS_PRICE: u128 = 1_875_000_000;
31
32/// Bounds the amount the base fee can change between blocks.
33pub const BASE_FEE_CHANGE_DENOMINATOR: u128 = 8;
34
35/// Minimum suggested priority fee
36pub const MIN_SUGGESTED_PRIORITY_FEE: u128 = 1e9 as u128;
37
38/// Stores the fee related information
39#[derive(Clone, Debug)]
40pub struct FeeManager {
41    /// Hardfork identifier
42    spec_id: SpecId,
43    /// The blob params that determine blob fees
44    blob_params: Arc<RwLock<BlobParams>>,
45    /// Tracks the base fee for the next block post London
46    ///
47    /// This value will be updated after a new block was mined
48    base_fee: Arc<RwLock<u64>>,
49    /// Whether the minimum suggested priority fee is enforced
50    is_min_priority_fee_enforced: bool,
51    /// Tracks the excess blob gas, and the base fee, for the next block post Cancun
52    ///
53    /// This value will be updated after a new block was mined
54    blob_excess_gas_and_price: Arc<RwLock<BlobExcessGasAndPrice>>,
55    /// The base price to use Pre London
56    ///
57    /// This will be constant value unless changed manually
58    gas_price: Arc<RwLock<u128>>,
59    elasticity: Arc<RwLock<f64>>,
60    /// Network-specific base fee params for EIP-1559 calculations
61    base_fee_params: BaseFeeParams,
62    /// The active Tempo hardfork, set only when running a Tempo chain.
63    ///
64    /// Tempo replaces EIP-1559: pre-T7 the base fee is fixed, and T7+ uses the TIP-1067 dynamic
65    /// controller. It is updated once after a fork's hardfork is auto-detected.
66    tempo_hardfork: Arc<RwLock<Option<TempoHardfork>>>,
67}
68
69impl FeeManager {
70    #[allow(clippy::too_many_arguments)]
71    pub fn new(
72        spec_id: SpecId,
73        base_fee: u64,
74        is_min_priority_fee_enforced: bool,
75        gas_price: u128,
76        blob_excess_gas_and_price: BlobExcessGasAndPrice,
77        blob_params: BlobParams,
78        base_fee_params: BaseFeeParams,
79        tempo_hardfork: Option<TempoHardfork>,
80    ) -> Self {
81        let elasticity = 1f64 / base_fee_params.elasticity_multiplier as f64;
82        Self {
83            spec_id,
84            blob_params: Arc::new(RwLock::new(blob_params)),
85            base_fee: Arc::new(RwLock::new(base_fee)),
86            is_min_priority_fee_enforced,
87            gas_price: Arc::new(RwLock::new(gas_price)),
88            blob_excess_gas_and_price: Arc::new(RwLock::new(blob_excess_gas_and_price)),
89            elasticity: Arc::new(RwLock::new(elasticity)),
90            base_fee_params,
91            tempo_hardfork: Arc::new(RwLock::new(tempo_hardfork)),
92        }
93    }
94
95    /// Returns the active Tempo hardfork, if running a Tempo chain.
96    pub fn tempo_hardfork(&self) -> Option<TempoHardfork> {
97        *self.tempo_hardfork.read()
98    }
99
100    /// Sets the active Tempo hardfork, used after a fork's hardfork is auto-detected.
101    pub fn set_tempo_hardfork(&self, hardfork: Option<TempoHardfork>) {
102        *self.tempo_hardfork.write() = hardfork;
103    }
104
105    pub fn elasticity(&self) -> f64 {
106        *self.elasticity.read()
107    }
108
109    /// Returns true for post London
110    pub const fn is_eip1559(&self) -> bool {
111        (self.spec_id as u8) >= (SpecId::LONDON as u8)
112    }
113
114    pub const fn is_eip4844(&self) -> bool {
115        (self.spec_id as u8) >= (SpecId::CANCUN as u8)
116    }
117
118    /// Calculates the current blob gas price
119    pub fn blob_gas_price(&self) -> u128 {
120        if self.is_eip4844() { self.base_fee_per_blob_gas() } else { 0 }
121    }
122
123    pub fn base_fee(&self) -> u64 {
124        if self.is_eip1559() { *self.base_fee.read() } else { 0 }
125    }
126
127    pub const fn is_min_priority_fee_enforced(&self) -> bool {
128        self.is_min_priority_fee_enforced
129    }
130
131    /// Raw base gas price
132    pub fn raw_gas_price(&self) -> u128 {
133        *self.gas_price.read()
134    }
135
136    pub fn excess_blob_gas_and_price(&self) -> Option<BlobExcessGasAndPrice> {
137        self.is_eip4844().then(|| *self.blob_excess_gas_and_price.read())
138    }
139
140    pub fn base_fee_per_blob_gas(&self) -> u128 {
141        if self.is_eip4844() { self.blob_excess_gas_and_price.read().blob_gasprice } else { 0 }
142    }
143
144    /// Returns the current gas price
145    pub fn set_gas_price(&self, price: u128) {
146        let mut gas = self.gas_price.write();
147        *gas = price;
148    }
149
150    /// Returns the current base fee
151    pub fn set_base_fee(&self, fee: u64) {
152        trace!(target: "backend::fees", "updated base fee {:?}", fee);
153        let mut base = self.base_fee.write();
154        *base = fee;
155    }
156
157    /// Sets the current blob excess gas and price
158    pub fn set_blob_excess_gas_and_price(&self, blob_excess_gas_and_price: BlobExcessGasAndPrice) {
159        trace!(target: "backend::fees", "updated blob base fee {:?}", blob_excess_gas_and_price);
160        let mut base = self.blob_excess_gas_and_price.write();
161        *base = blob_excess_gas_and_price;
162    }
163
164    /// Calculates the base fee for the next block
165    pub fn get_next_block_base_fee_per_gas(
166        &self,
167        gas_used: u64,
168        gas_limit: u64,
169        last_fee_per_gas: u64,
170    ) -> u64 {
171        // It's naturally impossible for base fee to be 0;
172        // It means it was set by the user deliberately and therefore we treat it as a constant.
173        // Therefore, we skip the base fee calculation altogether and we return 0.
174        if self.base_fee() == 0 {
175            return 0;
176        }
177        self.calculate_next_block_base_fee_per_gas(gas_used, gas_limit, last_fee_per_gas)
178    }
179
180    /// Calculates the next block base fee from the parent block without applying the configured
181    /// zero-fee sentinel.
182    pub(crate) fn calculate_next_block_base_fee_per_gas(
183        &self,
184        gas_used: u64,
185        gas_limit: u64,
186        last_fee_per_gas: u64,
187    ) -> u64 {
188        if !self.is_eip1559() {
189            return 0;
190        }
191        // Tempo replaces EIP-1559 with its own hardfork-specific base fee rules.
192        if let Some(hardfork) = self.tempo_hardfork() {
193            return tempo_next_block_base_fee(hardfork, gas_used, last_fee_per_gas);
194        }
195        calc_next_block_base_fee(gas_used, gas_limit, last_fee_per_gas, self.base_fee_params)
196    }
197
198    /// Calculates the next block blob base fee.
199    pub fn get_next_block_blob_base_fee_per_gas(&self) -> u128 {
200        self.blob_params().calc_blob_fee(self.blob_excess_gas_and_price.read().excess_blob_gas)
201    }
202
203    /// Calculates the next block blob excess gas, using the provided parent blob excess gas and
204    /// parent blob gas used
205    pub fn get_next_block_blob_excess_gas(&self, blob_excess_gas: u64, blob_gas_used: u64) -> u64 {
206        self.blob_params().next_block_excess_blob_gas_osaka(
207            blob_excess_gas,
208            blob_gas_used,
209            self.base_fee(),
210        )
211    }
212
213    /// Configures the blob params
214    pub fn set_blob_params(&self, blob_params: BlobParams) {
215        *self.blob_params.write() = blob_params;
216    }
217
218    /// Returns the active [`BlobParams`]
219    pub fn blob_params(&self) -> BlobParams {
220        *self.blob_params.read()
221    }
222}
223
224/// Computes the next block's base fee for a Tempo chain.
225///
226/// - T7+: the TIP-1067 dynamic controller, an EIP-1559 update against a fixed 10M gas target
227///   clamped to `[floor, cap]`.
228/// - Pre-T7: the fixed hardfork base fee (10 gwei pre-T1, 20 gwei T1+).
229fn tempo_next_block_base_fee(hardfork: TempoHardfork, gas_used: u64, parent_base_fee: u64) -> u64 {
230    if hardfork.is_t7() {
231        return tempo_t7_next_block_base_fee(parent_base_fee, gas_used);
232    }
233    crate::config::tempo_default_base_fee(hardfork)
234}
235
236/// An async service that takes care of the `FeeHistory` cache
237pub struct FeeHistoryService<N: Network>
238where
239    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
240{
241    /// blob parameters for the current spec
242    blob_params: BlobParams,
243    /// incoming notifications about new blocks
244    new_blocks: ChainNotifications,
245    /// contains all fee history related entries
246    cache: FeeHistoryCache,
247    /// number of items to consider
248    fee_history_limit: u64,
249    /// a type that can fetch ethereum-storage data
250    storage_info: StorageInfo<N>,
251}
252
253impl<N: Network> FeeHistoryService<N>
254where
255    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
256{
257    pub const fn new(
258        blob_params: BlobParams,
259        new_blocks: ChainNotifications,
260        cache: FeeHistoryCache,
261        storage_info: StorageInfo<N>,
262    ) -> Self {
263        Self {
264            blob_params,
265            new_blocks,
266            cache,
267            fee_history_limit: MAX_FEE_HISTORY_CACHE_SIZE,
268            storage_info,
269        }
270    }
271
272    /// Returns the configured history limit
273    pub const fn fee_history_limit(&self) -> u64 {
274        self.fee_history_limit
275    }
276
277    /// Inserts a new cache entry for the given block
278    pub(crate) fn insert_cache_entry_for_block(&self, hash: B256, header: &impl BlockHeader) {
279        let (result, block_number) = self.create_cache_entry(hash, header);
280        self.insert_cache_entry(result, block_number);
281    }
282
283    /// Create a new history entry for the block
284    fn create_cache_entry(
285        &self,
286        hash: B256,
287        header: &impl BlockHeader,
288    ) -> (FeeHistoryCacheItem, Option<u64>) {
289        create_fee_history_cache_item(hash, header, &self.storage_info, self.blob_params)
290    }
291
292    fn insert_cache_entry(&self, item: FeeHistoryCacheItem, block_number: Option<u64>) {
293        insert_fee_history_cache_item(&self.cache, item, block_number, self.fee_history_limit);
294    }
295}
296
297/// Inserts an entry into the fee history cache and trims it back to `fee_history_limit`.
298///
299/// Used by the async [`FeeHistoryService`]. The `eth_feeHistory` fallback applies the same bounded
300/// insertion policy to a batch under one lock.
301pub(crate) fn insert_fee_history_cache_item(
302    cache: &FeeHistoryCache,
303    item: FeeHistoryCacheItem,
304    block_number: Option<u64>,
305    fee_history_limit: u64,
306) {
307    if let Some(block_number) = block_number {
308        trace!(target: "fees", "insert new history item={:?} for {}", item, block_number);
309        let mut cache = cache.lock();
310        cache.insert(block_number, item);
311
312        // Trim to the cache limit by dropping the oldest entries (smallest block numbers).
313        // `pop_first` is saturating and correct regardless of insertion order, unlike the
314        // previous index math which could underflow when the `eth_feeHistory` fallback inserts
315        // entries out of order.
316        while cache.len() as u64 > fee_history_limit {
317            cache.pop_first();
318        }
319    }
320}
321
322/// Percentile list from 0.0 to 100.0 with a 0.5 resolution (201 points).
323///
324/// Constant across blocks, so it is computed once instead of being rebuilt on every
325/// `create_fee_history_cache_item` call.
326static REWARD_PERCENTILES: LazyLock<Vec<f64>> = LazyLock::new(|| {
327    let mut percentile: f64 = 0.0;
328    (0..=200)
329        .map(|_| {
330            let val = percentile;
331            percentile += 0.5;
332            val
333        })
334        .collect()
335});
336
337/// Calculates percentile rewards from transactions sorted by effective reward.
338///
339/// [`REWARD_PERCENTILES`] must remain ascending because the transaction cursor never rewinds.
340fn reward_percentiles(transactions: &[(u64, u128)], block_gas_used: f64) -> Vec<u128> {
341    let mut rewards = Vec::with_capacity(REWARD_PERCENTILES.len());
342    let mut transactions = transactions.iter().copied();
343    let Some((mut cumulative_gas, mut current_reward)) = transactions.next() else {
344        return rewards;
345    };
346
347    for &percentile in REWARD_PERCENTILES.iter() {
348        let target_gas = (percentile * block_gas_used / 100f64) as u64;
349        while target_gas > cumulative_gas {
350            let Some((tx_gas_used, effective_reward)) = transactions.next() else { return rewards };
351            cumulative_gas += tx_gas_used;
352            current_reward = effective_reward;
353        }
354        rewards.push(current_reward);
355    }
356
357    rewards
358}
359
360/// Builds the [`FeeHistoryCacheItem`] for a single block.
361///
362/// Shared by the async [`FeeHistoryService`] and by `eth_feeHistory` itself: the service can lag
363/// the chain head (it only runs when the node task is polled), so the RPC handler computes any
364/// missing entry on demand with the same logic instead of returning a short response.
365pub(crate) fn create_fee_history_cache_item<N: Network>(
366    hash: B256,
367    header: &impl BlockHeader,
368    storage_info: &StorageInfo<N>,
369    blob_params: BlobParams,
370) -> (FeeHistoryCacheItem, Option<u64>)
371where
372    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
373{
374    let mut block_number: Option<u64> = None;
375    let base_fee = header.base_fee_per_gas().unwrap_or_default();
376    let excess_blob_gas = header.excess_blob_gas().map(|g| g as u128);
377    let blob_gas_used = header.blob_gas_used().map(|g| g as u128);
378    let base_fee_per_blob_gas = header.blob_fee(blob_params);
379
380    let mut item = FeeHistoryCacheItem {
381        block_hash: hash,
382        base_fee: base_fee as u128,
383        gas_used_ratio: 0f64,
384        blob_gas_used_ratio: 0f64,
385        rewards: Vec::new(),
386        excess_blob_gas,
387        base_fee_per_blob_gas,
388        blob_gas_used,
389    };
390
391    let current_block = storage_info.block(hash);
392    let current_receipts = storage_info.receipts(hash);
393
394    if let (Some(block), Some(receipts)) = (current_block, current_receipts) {
395        block_number = Some(block.header.number());
396
397        let gas_used = block.header.gas_used() as f64;
398        let blob_gas_used = block.header.blob_gas_used().map(|g| g as f64);
399        item.gas_used_ratio = gas_used / block.header.gas_limit() as f64;
400        item.blob_gas_used_ratio = blob_gas_used
401            .map(|g| {
402                let max = blob_params.max_blob_gas_per_block() as f64;
403                if max == 0.0 { 0.0 } else { g / max }
404            })
405            .unwrap_or(0.0);
406
407        // extract useful tx info (gas_used, effective_reward)
408        let mut transactions: Vec<(_, _)> = receipts
409            .iter()
410            .enumerate()
411            .map(|(i, receipt)| {
412                let cumulative = receipt.cumulative_gas_used();
413                let prev_cumulative = if i > 0 { receipts[i - 1].cumulative_gas_used() } else { 0 };
414                let gas_used = cumulative - prev_cumulative;
415                let effective_reward = block
416                    .body
417                    .transactions
418                    .get(i)
419                    .map(|tx| tx.as_ref().effective_tip_per_gas(base_fee).unwrap_or(0))
420                    .unwrap_or(0);
421
422                (gas_used, effective_reward)
423            })
424            .collect();
425
426        // sort by effective reward asc
427        transactions.sort_by_key(|(_, reward)| *reward);
428
429        item.rewards = reward_percentiles(&transactions, gas_used);
430    } else {
431        item.rewards = vec![0; REWARD_PERCENTILES.len()];
432    }
433    (item, block_number)
434}
435
436// An endless future that listens for new blocks and updates the cache
437impl<N: Network> Future for FeeHistoryService<N>
438where
439    N::ReceiptEnvelope: TxReceipt<Log = alloy_primitives::Log>,
440{
441    type Output = ();
442
443    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
444        let pin = self.get_mut();
445
446        while let Poll::Ready(Some(notification)) = pin.new_blocks.poll_next_unpin(cx) {
447            // add the imported block.
448            if let Some(block) = notification.as_new_block() {
449                pin.insert_cache_entry_for_block(block.hash, block.header.as_ref());
450            }
451        }
452
453        Poll::Pending
454    }
455}
456
457pub type FeeHistoryCache = Arc<Mutex<BTreeMap<u64, FeeHistoryCacheItem>>>;
458
459/// A single item in the whole fee history cache
460#[derive(Clone, Debug)]
461pub struct FeeHistoryCacheItem {
462    pub block_hash: B256,
463    pub base_fee: u128,
464    pub gas_used_ratio: f64,
465    pub base_fee_per_blob_gas: Option<u128>,
466    pub blob_gas_used_ratio: f64,
467    pub excess_blob_gas: Option<u128>,
468    pub blob_gas_used: Option<u128>,
469    pub rewards: Vec<u128>,
470}
471
472#[derive(Clone, Default)]
473pub struct FeeDetails {
474    pub gas_price: Option<u128>,
475    pub max_fee_per_gas: Option<u128>,
476    pub max_priority_fee_per_gas: Option<u128>,
477    pub max_fee_per_blob_gas: Option<u128>,
478}
479
480impl FeeDetails {
481    /// All values zero
482    pub const fn zero() -> Self {
483        Self {
484            gas_price: Some(0),
485            max_fee_per_gas: Some(0),
486            max_priority_fee_per_gas: Some(0),
487            max_fee_per_blob_gas: None,
488        }
489    }
490
491    /// If neither `gas_price` nor `max_fee_per_gas` is `Some`, this will set both to `0`
492    pub const fn or_zero_fees(self) -> Self {
493        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
494            self;
495
496        let no_fees = gas_price.is_none() && max_fee_per_gas.is_none();
497        let gas_price = if no_fees { Some(0) } else { gas_price };
498        let max_fee_per_gas = if no_fees { Some(0) } else { max_fee_per_gas };
499        let max_fee_per_blob_gas = if no_fees { None } else { max_fee_per_blob_gas };
500
501        Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas }
502    }
503
504    /// Turns this type into a tuple
505    pub const fn split(self) -> (Option<u128>, Option<u128>, Option<u128>, Option<u128>) {
506        let Self { gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas } =
507            self;
508        (gas_price, max_fee_per_gas, max_priority_fee_per_gas, max_fee_per_blob_gas)
509    }
510
511    /// Creates a new instance from the request's gas related values
512    pub fn new(
513        request_gas_price: Option<u128>,
514        request_max_fee: Option<u128>,
515        request_priority: Option<u128>,
516        max_fee_per_blob_gas: Option<u128>,
517    ) -> Result<Self, BlockchainError> {
518        match (request_gas_price, request_max_fee, request_priority, max_fee_per_blob_gas) {
519            (gas_price, None, None, None) => {
520                // Legacy request, all default to gas price.
521                Ok(Self {
522                    gas_price,
523                    max_fee_per_gas: gas_price,
524                    max_priority_fee_per_gas: gas_price,
525                    max_fee_per_blob_gas: None,
526                })
527            }
528            (_, max_fee, max_priority, max_fee_per_blob_gas) => {
529                // eip-1559
530                // Ensure `max_priority_fee_per_gas` is less or equal to `max_fee_per_gas`.
531                if let Some(max_priority) = max_priority {
532                    let max_fee = max_fee.unwrap_or_default();
533                    if max_priority > max_fee {
534                        return Err(BlockchainError::InvalidFeeInput);
535                    }
536                }
537                Ok(Self {
538                    gas_price: max_fee,
539                    max_fee_per_gas: max_fee,
540                    max_priority_fee_per_gas: max_priority,
541                    max_fee_per_blob_gas,
542                })
543            }
544        }
545    }
546}
547
548impl fmt::Debug for FeeDetails {
549    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
550        write!(fmt, "Fees {{ ")?;
551        write!(fmt, "gas_price: {:?}, ", self.gas_price)?;
552        write!(fmt, "max_fee_per_gas: {:?}, ", self.max_fee_per_gas)?;
553        write!(fmt, "max_priority_fee_per_gas: {:?}, ", self.max_priority_fee_per_gas)?;
554        write!(fmt, "}}")?;
555        Ok(())
556    }
557}
558
559#[cfg(test)]
560mod tests {
561    use super::*;
562
563    fn reward_percentiles_reference(
564        transactions: &[(u64, u128)],
565        block_gas_used: f64,
566    ) -> Vec<u128> {
567        REWARD_PERCENTILES
568            .iter()
569            .filter_map(|&percentile| {
570                let target_gas = (percentile * block_gas_used / 100f64) as u64;
571                let mut cumulative_gas = 0;
572                for (tx_gas_used, effective_reward) in transactions.iter().copied() {
573                    cumulative_gas += tx_gas_used;
574                    if target_gas <= cumulative_gas {
575                        return Some(effective_reward);
576                    }
577                }
578                None
579            })
580            .collect()
581    }
582
583    fn assert_reward_percentiles_match(transactions: &mut [(u64, u128)], gas_used: u64) {
584        transactions.sort_by_key(|(_, reward)| *reward);
585        assert_eq!(
586            reward_percentiles(transactions, gas_used as f64),
587            reward_percentiles_reference(transactions, gas_used as f64)
588        );
589    }
590
591    fn fee_manager(spec_id: SpecId) -> FeeManager {
592        FeeManager::new(
593            spec_id,
594            INITIAL_BASE_FEE,
595            true,
596            INITIAL_GAS_PRICE,
597            BlobExcessGasAndPrice::new_with_spec(0, SpecId::CANCUN),
598            BlobParams::cancun(),
599            BaseFeeParams::ethereum(),
600            None,
601        )
602    }
603
604    #[test]
605    fn raw_next_base_fee_respects_london_activation() {
606        let berlin = fee_manager(SpecId::BERLIN);
607        assert_eq!(
608            berlin.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
609            0
610        );
611
612        let london = fee_manager(SpecId::LONDON);
613        assert_ne!(
614            london.calculate_next_block_base_fee_per_gas(30_000_000, 30_000_000, INITIAL_BASE_FEE),
615            0
616        );
617    }
618
619    #[test]
620    fn reward_percentile_sweep_preserves_boundaries_and_empty_results() {
621        assert_reward_percentiles_match(&mut [], 0);
622
623        let mut transactions = [(5, 1), (0, 2), (5, 3)];
624        assert_reward_percentiles_match(&mut transactions, 1_000);
625        assert_eq!(reward_percentiles(&transactions, 1_000f64), [1, 1, 3]);
626
627        let mut transactions = [(0, 10), (1, 20)];
628        assert_reward_percentiles_match(&mut transactions, 1);
629        let rewards = reward_percentiles(&transactions, 1f64);
630        assert_eq!(&rewards[..200], &[10; 200]);
631        assert_eq!(rewards[200], 20);
632    }
633
634    #[test]
635    fn reward_percentile_sweep_matches_reference_for_randomized_inputs() {
636        let mut state = 0x4d59_5df4_d0f3_3173u64;
637        for _ in 0..2_000 {
638            let len = (next_random(&mut state) % 129) as usize;
639            let mut transactions = (0..len)
640                .map(|_| {
641                    let gas_used = next_random(&mut state) % 100;
642                    let effective_reward = (next_random(&mut state) % 16) as u128;
643                    (gas_used, effective_reward)
644                })
645                .collect::<Vec<_>>();
646            let total_gas = transactions.iter().map(|(gas_used, _)| gas_used).sum::<u64>();
647            let header_gas_used = match next_random(&mut state) % 4 {
648                0 => total_gas,
649                1 => next_random(&mut state) % (total_gas.saturating_add(1)),
650                2 => total_gas.saturating_add(next_random(&mut state) % 1_000),
651                _ => 0,
652            };
653
654            assert_reward_percentiles_match(&mut transactions, header_gas_used);
655        }
656    }
657
658    fn next_random(state: &mut u64) -> u64 {
659        *state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
660        *state
661    }
662}