1use 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
15pub 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
33pub 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
47pub 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
58pub 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
94pub 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#[derive(Debug)]
113pub struct CallManyBudget {
114 gas_cap: u64,
116 remaining_gas: Option<u64>,
118 deadline: Option<Instant>,
121 capped: bool,
123}
124
125impl CallManyBudget {
126 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 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 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 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 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 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 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 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 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 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 assert_eq!(checked_blob_gas_used_ratio(0, 0), 0.0);
313 assert_eq!(checked_blob_gas_used_ratio(50, 0), 0.0);
315 assert_eq!(checked_blob_gas_used_ratio(0, 100), 0.0);
317 assert_eq!(checked_blob_gas_used_ratio(50, 100), 0.5);
319 assert_eq!(checked_blob_gas_used_ratio(100, 100), 1.0);
321 }
322}