Skip to main content

reth_rpc_eth_types/
utils.rs

1//! Commonly used code snippets
2
3use super::{
4    error::{AsEthApiError, FromEthApiError},
5    EthApiError, EthResult,
6};
7use alloy_consensus::TxReceipt;
8use alloy_rpc_types_eth::Bundle;
9use evm2::{interpreter::InstrStop, TxResultExt};
10use reth_evm::TransactionEnvMut;
11use reth_primitives_traits::{Recovered, SignedTransaction};
12use reth_rpc_server_types::constants::{DEFAULT_CALL_MANY_TIMEOUT, DEFAULT_MAX_CALL_MANY_CALLS};
13use std::{future::Future, time::Instant};
14
15/// Calculates the gas used and next log index for a transaction at the given index
16pub fn calculate_gas_used_and_next_log_index(
17    tx_index: u64,
18    all_receipts: &[impl TxReceipt],
19) -> (u64, usize) {
20    let mut gas_used = 0;
21    let mut next_log_index = 0;
22
23    if tx_index > 0 {
24        for receipt in all_receipts.iter().take(tx_index as usize) {
25            gas_used = receipt.cumulative_gas_used();
26            next_log_index += receipt.logs().len();
27        }
28    }
29
30    (gas_used, next_log_index)
31}
32
33/// Recovers a [`SignedTransaction`] from an enveloped encoded byte stream.
34///
35/// This is a helper function that returns the appropriate RPC-specific error if the input data is
36/// malformed.
37///
38/// This function uses [`alloy_eips::eip2718::Decodable2718::decode_2718_exact`] to ensure
39/// that the entire input buffer is consumed and no trailing bytes are allowed.
40///
41/// See [`alloy_eips::eip2718::Decodable2718::decode_2718_exact`]
42pub fn recover_raw_transaction<T: SignedTransaction>(data: &[u8]) -> EthResult<Recovered<T>> {
43    SignedTransaction::try_into_recovered(decode_raw_transaction::<T>(data)?)
44        .or(Err(EthApiError::InvalidTransactionSignature))
45}
46
47/// Decodes an EIP-2718 transaction without recovering its sender.
48///
49/// Rejects empty input, malformed transactions, and trailing bytes with RPC-specific errors.
50pub fn decode_raw_transaction<T: SignedTransaction>(data: &[u8]) -> EthResult<T> {
51    if data.is_empty() {
52        return Err(EthApiError::EmptyRawTransactionData)
53    }
54
55    T::decode_2718_exact(data).map_err(|_| EthApiError::FailedToDecodeSignedTransaction)
56}
57
58/// Performs a binary search within a given block range to find the desired block number.
59///
60/// The binary search is performed by calling the provided asynchronous `check` closure on the
61/// blocks of the range. The closure should return a future representing the result of performing
62/// the desired logic at a given block. The future resolves to an `bool` where:
63/// - `true` indicates that the condition has been matched, but we can try to find a lower block to
64///   make the condition more matchable.
65/// - `false` indicates that the condition not matched, so the target is not present in the current
66///   block and should continue searching in a higher range.
67///
68/// Args:
69/// - `low`: The lower bound of the block range (inclusive).
70/// - `high`: The upper bound of the block range (inclusive).
71/// - `check`: A closure that performs the desired logic at a given block.
72pub async fn binary_search<F, Fut, E>(low: u64, high: u64, check: F) -> Result<u64, E>
73where
74    F: Fn(u64) -> Fut,
75    Fut: Future<Output = Result<bool, E>>,
76{
77    let mut low = low;
78    let mut high = high;
79    let mut num = high;
80
81    while low <= high {
82        let mid = (low + high) / 2;
83        if check(mid).await? {
84            high = mid - 1;
85            num = mid;
86        } else {
87            low = mid + 1
88        }
89    }
90
91    Ok(num)
92}
93
94/// Calculates the blob gas used ratio for a block, accounting for the case where
95/// `max_blob_gas_per_block` is zero.
96///
97/// Returns `0.0` if `max_blob_gas_per_block` is `0`, otherwise returns the ratio
98/// `blob_gas_used/max_blob_gas_per_block`.
99pub fn checked_blob_gas_used_ratio(blob_gas_used: u64, max_blob_gas_per_block: u64) -> f64 {
100    if max_blob_gas_per_block == 0 {
101        0.0
102    } else {
103        blob_gas_used as f64 / max_blob_gas_per_block as f64
104    }
105}
106
107/// Bounds the work of one `eth_callMany`, `trace_callMany` or `debug_traceCallMany` request.
108///
109/// The request may carry at most [`DEFAULT_MAX_CALL_MANY_CALLS`] calls, all of its calls share
110/// one gas budget of the RPC gas cap, and no call starts more than [`DEFAULT_CALL_MANY_TIMEOUT`]
111/// after the first one.
112#[derive(Debug)]
113pub struct CallManyBudget {
114    /// The RPC gas cap the calls share, reported once they exceed it.
115    gas_cap: u64,
116    /// Gas left for the remaining calls, `None` if the RPC gas cap is disabled.
117    remaining_gas: Option<u64>,
118    /// No call starts after this instant. Set when the first call is admitted, so waiting for a
119    /// permit or replaying the block's transactions does not count.
120    deadline: Option<Instant>,
121    /// Whether [`Self::admit`] lowered the gas limit of the last admitted call.
122    capped: bool,
123}
124
125impl CallManyBudget {
126    /// Creates the budget for a request of `calls` calls that share `gas_cap` gas.
127    ///
128    /// Returns an error if `calls` exceeds [`DEFAULT_MAX_CALL_MANY_CALLS`]. A `gas_cap` of `0`
129    /// disables the gas budget, as it disables the RPC gas cap.
130    pub fn new(calls: usize, gas_cap: u64) -> Result<Self, EthApiError> {
131        if calls > DEFAULT_MAX_CALL_MANY_CALLS {
132            return Err(EthApiError::InvalidParams(format!(
133                "call count {calls} exceeds limit {DEFAULT_MAX_CALL_MANY_CALLS}"
134            )))
135        }
136        Ok(Self {
137            gas_cap,
138            remaining_gas: (gas_cap != 0).then_some(gas_cap),
139            deadline: None,
140            capped: false,
141        })
142    }
143
144    /// Creates the budget for a request of `bundles`, see [`Self::new`].
145    ///
146    /// An empty bundle counts as one call, so requests of empty bundles are bounded too.
147    pub fn for_bundles<T>(bundles: &[Bundle<T>], gas_cap: u64) -> Result<Self, EthApiError> {
148        Self::new(bundles.iter().map(|bundle| bundle.transactions.len().max(1)).sum(), gas_cap)
149    }
150
151    /// Admits the request's next call, lowering the gas limit of `tx_env` to the gas left for the
152    /// request's remaining calls.
153    ///
154    /// The first admitted call starts the request's timeout. Returns
155    /// [`EthApiError::ExecutionTimedOut`] once it has expired.
156    pub fn admit<T: TransactionEnvMut>(
157        &mut self,
158        tx_env: &mut Recovered<T>,
159    ) -> Result<(), EthApiError> {
160        let now = Instant::now();
161        if now >= *self.deadline.get_or_insert(now + DEFAULT_CALL_MANY_TIMEOUT) {
162            return Err(EthApiError::ExecutionTimedOut(DEFAULT_CALL_MANY_TIMEOUT))
163        }
164        self.capped = false;
165        if let Some(remaining) = self.remaining_gas &&
166            tx_env.gas_limit() > remaining
167        {
168            tx_env.inner_mut().set_gas_limit(remaining);
169            self.capped = true;
170        }
171        Ok(())
172    }
173
174    /// Returns [`EthApiError::CallManyGasCapExceeded`] in place of `err` if the last admitted call
175    /// failed its intrinsic gas check because [`Self::admit`] lowered its gas limit.
176    pub fn map_call_err<E: AsEthApiError + FromEthApiError>(&self, err: E) -> E {
177        if self.capped && err.is_gas_too_low() {
178            return E::from_eth_err(EthApiError::CallManyGasCapExceeded(self.gas_cap))
179        }
180        err
181    }
182
183    /// Charges the gas the last admitted call used against the budget.
184    ///
185    /// Returns [`EthApiError::CallManyGasCapExceeded`] if the call ran out of gas after
186    /// [`Self::admit`] lowered its gas limit, and an internal error if it used more gas than is
187    /// left, which an admitted call cannot do.
188    pub fn charge<E>(&mut self, result: &TxResultExt<E>) -> Result<(), EthApiError> {
189        if self.capped && result.stop.is_out_of_gas() && result.stop != InstrStop::MemoryLimitOOG {
190            return Err(EthApiError::CallManyGasCapExceeded(self.gas_cap))
191        }
192        if let Some(remaining) = &mut self.remaining_gas {
193            *remaining =
194                remaining.checked_sub(result.tx_gas_used()).ok_or(EthApiError::InternalEthError)?;
195        }
196        Ok(())
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use alloy_consensus::{Transaction, TxLegacy};
204    use alloy_primitives::Address;
205    use evm2::ethereum::TxEnvelope;
206
207    #[test]
208    fn call_many_budget_limits_calls() {
209        assert!(CallManyBudget::new(DEFAULT_MAX_CALL_MANY_CALLS, 0).is_ok());
210
211        let empty = Bundle::<()> { transactions: Vec::new(), block_override: None };
212        let full =
213            Bundle { transactions: vec![(); DEFAULT_MAX_CALL_MANY_CALLS], block_override: None };
214        assert!(CallManyBudget::for_bundles(&vec![empty.clone(); DEFAULT_MAX_CALL_MANY_CALLS], 0)
215            .is_ok());
216        assert_eq!(
217            CallManyBudget::for_bundles(&vec![empty.clone(); DEFAULT_MAX_CALL_MANY_CALLS + 1], 0)
218                .unwrap_err()
219                .to_string(),
220            "call count 1025 exceeds limit 1024"
221        );
222        assert_eq!(
223            CallManyBudget::for_bundles(&[full, empty], 0).unwrap_err().to_string(),
224            "call count 1025 exceeds limit 1024"
225        );
226    }
227
228    fn tx_env(gas_limit: u64) -> Recovered<TxEnvelope> {
229        Recovered::new_unchecked(
230            TxEnvelope::Legacy(TxLegacy { gas_limit, ..Default::default() }),
231            Address::ZERO,
232        )
233    }
234
235    fn tx_result(gas_used: u64, stop: InstrStop) -> TxResultExt {
236        TxResultExt {
237            status: stop.is_success(),
238            total_gas_spent: gas_used,
239            stop,
240            ..Default::default()
241        }
242    }
243
244    #[test]
245    fn call_many_budget_shares_gas_cap() {
246        let admitted = |budget: &mut CallManyBudget, gas_limit| {
247            let mut tx_env = tx_env(gas_limit);
248            budget.admit(&mut tx_env).unwrap();
249            tx_env.gas_limit()
250        };
251
252        let mut budget = CallManyBudget::new(3, 50_000).unwrap();
253        assert_eq!(admitted(&mut budget, 30_000), 30_000);
254        // A call that was not capped keeps its own out-of-gas result.
255        budget.charge(&tx_result(30_000, InstrStop::OutOfGas)).unwrap();
256        assert_eq!(admitted(&mut budget, 30_000_000), 20_000);
257        assert!(matches!(
258            budget.charge(&tx_result(20_000, InstrStop::OutOfGas)),
259            Err(EthApiError::CallManyGasCapExceeded(50_000))
260        ));
261        assert_eq!(admitted(&mut budget, 10_000), 10_000);
262        assert!(matches!(
263            budget.charge(&tx_result(20_001, InstrStop::Stop)),
264            Err(EthApiError::InternalEthError)
265        ));
266
267        let mut unlimited = CallManyBudget::new(3, 0).unwrap();
268        unlimited.charge(&tx_result(u64::MAX, InstrStop::Stop)).unwrap();
269        assert_eq!(admitted(&mut unlimited, u64::MAX), u64::MAX);
270    }
271
272    #[test]
273    fn call_many_budget_times_out() {
274        let mut budget = CallManyBudget::new(2, 0).unwrap();
275        assert_eq!(budget.deadline, None);
276        budget.admit(&mut tx_env(21_000)).unwrap();
277        assert!(budget.deadline.is_some());
278
279        budget.deadline = Some(Instant::now());
280        assert!(matches!(
281            budget.admit(&mut tx_env(21_000)),
282            Err(EthApiError::ExecutionTimedOut(timeout)) if timeout == DEFAULT_CALL_MANY_TIMEOUT
283        ));
284    }
285
286    #[tokio::test]
287    async fn test_binary_search() {
288        // in the middle
289        let num: Result<_, ()> =
290            binary_search(1, 10, |mid| Box::pin(async move { Ok(mid >= 5) })).await;
291        assert_eq!(num, Ok(5));
292
293        // in the upper
294        let num: Result<_, ()> =
295            binary_search(1, 10, |mid| Box::pin(async move { Ok(mid >= 7) })).await;
296        assert_eq!(num, Ok(7));
297
298        // in the lower
299        let num: Result<_, ()> =
300            binary_search(1, 10, |mid| Box::pin(async move { Ok(mid >= 1) })).await;
301        assert_eq!(num, Ok(1));
302
303        // higher than the upper
304        let num: Result<_, ()> =
305            binary_search(1, 10, |mid| Box::pin(async move { Ok(mid >= 11) })).await;
306        assert_eq!(num, Ok(10));
307    }
308
309    #[test]
310    fn test_checked_blob_gas_used_ratio() {
311        // No blob gas used, max blob gas per block is 0
312        assert_eq!(checked_blob_gas_used_ratio(0, 0), 0.0);
313        // Blob gas used is non-zero, max blob gas per block is 0 (division by zero protection)
314        assert_eq!(checked_blob_gas_used_ratio(50, 0), 0.0);
315        // Blob gas used is zero, max blob gas per block is non-zero
316        assert_eq!(checked_blob_gas_used_ratio(0, 100), 0.0);
317        // Blob gas used is non-zero, max blob gas per block is non-zero
318        assert_eq!(checked_blob_gas_used_ratio(50, 100), 0.5);
319        // Blob gas used is non-zero and equal to max blob gas per block
320        assert_eq!(checked_blob_gas_used_ratio(100, 100), 1.0);
321    }
322}