From 16f5969b7c6b339daa3d9168b93a780643482a15 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 21 Feb 2025 10:37:30 -0500 Subject: [PATCH 1/8] feat: more allowance features --- Cargo.toml | 3 + src/evm.rs | 130 +++++++++++++++++++++++++++++++++++++++----- src/fill/fillers.rs | 35 +++++++++++- 3 files changed, 151 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0c07447..8150f86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ tokio = { version = "1.39", features = ["macros", "rt-multi-thread"] } [features] default = [ + "call", "concurrent-db", "estimate_gas", "revm/std", @@ -64,6 +65,8 @@ default = [ "revm/secp256k1", ] +call = ["optional_eip3607", "optional_no_base_fee"] + concurrent-db = ["dep:dashmap"] estimate_gas = ["optional_eip3607", "optional_no_base_fee"] diff --git a/src/evm.rs b/src/evm.rs index cb57d36..eb77918 100644 --- a/src/evm.rs +++ b/src/evm.rs @@ -1,7 +1,6 @@ use crate::{ driver::DriveBlockResult, est::{EstimationResult, SearchRange}, - fillers::GasEstimationFiller, unwrap_or_trevm_err, Block, BlockDriver, BundleDriver, Cfg, ChainDriver, DriveBundleResult, DriveChainResult, ErroredState, EvmErrored, EvmExtUnchecked, EvmNeedsBlock, EvmNeedsCfg, EvmNeedsTx, EvmReady, EvmTransacted, HasBlock, HasCfg, HasTx, NeedsCfg, NeedsTx, Ready, @@ -217,6 +216,20 @@ impl<'a, Ext, Db: Database + DatabaseCommit, TrevmState> Trevm<'a, Ext, Db, Trev Ok(self) } } + + /// Get the gas allowance for a specific caller and gas price. + pub fn try_gas_allowance( + &mut self, + caller: Address, + gas_price: U256, + ) -> Result { + if gas_price.is_zero() { + return Ok(u64::MAX); + } + let gas_price = U256::from(gas_price); + let balance = self.try_read_balance(caller)?; + Ok((balance / gas_price).saturating_to()) + } } impl Trevm<'_, Ext, Db, TrevmState> { @@ -265,6 +278,20 @@ impl Trevm<'_, Ext None => Ok(None), } } + + /// Get the gas allowance for a specific caller and gas price. + pub fn try_gas_allowance_ref( + &self, + caller: Address, + gas_price: U256, + ) -> Result::Error> { + if gas_price.is_zero() { + return Ok(u64::MAX); + } + let gas_price = U256::from(gas_price); + let balance = self.try_read_balance_ref(caller)?; + Ok((balance / gas_price).saturating_to()) + } } impl + DatabaseCommit, TrevmState> @@ -1032,7 +1059,7 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmNeedsTx<'a, Ext, Db> { unsafe { core::mem::transmute(self) } } - /// Execute a transaction. Shortcut for `fill_tx(tx).run_tx()`. + /// Execute a transaction. Shortcut for `fill_tx(tx).run()`. pub fn run_tx( self, filler: &T, @@ -1040,9 +1067,25 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmNeedsTx<'a, Ext, Db> { self.fill_tx(filler).run() } + /// Simulate the transaction, and return the [`ExecutionResult`]. The + /// following modifications are made to the environment while simulating. + /// + /// - [EIP-3607] is disabled. + /// - Base fee checks are disabled. + /// - Nonce checks are disabled. + /// + /// [EIP-3607]: https://eips.ethereum.org/EIPS/eip-3607 + pub fn call_tx( + self, + filler: &T, + ) -> Result<(ExecutionResult, Self), EvmErrored<'a, Ext, Db>> { + self.fill_tx(filler).call() + } + /// Estimate the gas cost of a transaction. Shortcut for `fill_tx(tx). /// estimate()`. Returns an [`EstimationResult`] and the EVM populated with /// the transaction. + #[cfg(feature = "estimate_gas")] pub fn estimate_tx_gas( self, filler: &T, @@ -1054,11 +1097,38 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmNeedsTx<'a, Ext, Db> { // --- HAS TX impl<'a, Ext, Db: Database + DatabaseCommit, TrevmState: HasTx> Trevm<'a, Ext, Db, TrevmState> { + #[cfg(feature = "call")] + fn try_with_call_filler( + self, + filler: &crate::fillers::CallFiller, + f: impl FnOnce(Self) -> Result, EvmErrored<'a, Ext, Db>>, + ) -> Result, EvmErrored<'a, Ext, Db>> { + // override all relevant env bits + self.try_with_cfg(filler, |this| { + this.try_with_block(filler, |mut this| { + // reproducing code from `try_with_tx` to avoid trait bounds + let previous = this.inner.tx_mut().clone(); + filler.fill_tx_env(this.inner.tx_mut()); + match f(this) { + Ok(mut evm) => { + *evm.inner.tx_mut() = previous; + Ok(evm) + } + Err(mut evm) => { + *evm.inner.tx_mut() = previous; + Err(evm) + } + } + }) + }) + } + /// Convenience function to use the estimator to fill both Cfg and Tx, and /// run a fallible function. - fn try_with_gas_estimation_filler( + #[cfg(feature = "estimate_gas")] + fn try_with_estimate_gas_filler( self, - filler: &GasEstimationFiller, + filler: &crate::fillers::GasEstimationFiller, f: impl FnOnce(Self) -> Result>, ) -> Result> { self.try_with_cfg(filler, |this| this.try_with_tx(filler, f)) @@ -1188,15 +1258,10 @@ impl<'a, Ext, Db: Database + DatabaseCommit, TrevmState: HasTx> Trevm<'a, Ext, D /// Return the maximum gas that the caller can purchase. This is the balance /// of the caller divided by the gas price. - pub fn gas_allowance(&mut self) -> Result> { + pub fn caller_gas_allowance(&mut self) -> Result> { // Avoid DB read if gas price is zero let gas_price = self.gas_price(); - if gas_price.is_zero() { - return Ok(u64::MAX); - } - - let balance = self.try_read_balance(self.caller()).map_err(EVMError::Database)?; - Ok((balance / gas_price).saturating_to()) + self.try_gas_allowance(self.caller(), gas_price).map_err(EVMError::Database) } } @@ -1260,6 +1325,38 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { } } + /// Simulate the transaction, and return the [`ExecutionResult`]. The + /// following modifications are made to the environment while simulating. + /// + /// - [EIP-3607] is disabled. + /// - Base fee checks are disabled. + /// - Nonce checks are disabled. + /// + /// [EIP-3607]: https://eips.ethereum.org/EIPS/eip-3607 + #[cfg(feature = "call")] + pub fn call( + self, + ) -> Result<(ExecutionResult, EvmNeedsTx<'a, Ext, Db>), EvmErrored<'a, Ext, Db>> { + let mut output = MaybeUninit::uninit(); + + let gas_limit = self.tx().gas_limit; + + let this = self.try_with_call_filler( + &crate::fillers::CallFiller { gas_limit }, + |this| match this.run() { + Ok(t) => { + let (o, t) = t.take_result(); + + output.write(o); + + Ok(t) + } + Err(err) => return Err(err), + }, + )?; + Ok((unsafe { output.assume_init() }, this)) + } + /// Calculate the minimum gas required to start EVM execution. /// /// This uses [`calculate_initial_tx_gas`] to calculate the initial gas. @@ -1272,7 +1369,7 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { /// /// [EIP-2930]: https://eips.ethereum.org/EIPS/eip-2930 /// [EIP-7702]: https://eips.ethereum.org/EIPS/eip-7702 - fn calculate_initial_gas(&self) -> u64 { + pub fn calculate_initial_gas(&self) -> u64 { calculate_initial_tx_gas( self.spec_id(), &[], @@ -1289,6 +1386,7 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { /// - Check that the target is not a `create`. /// - Check that the target is not a contract. /// - Return the minimum gas required for the transfer. + #[cfg(feature = "estimate_gas")] fn estimate_gas_simple_transfer(&mut self) -> Result, EVMError> { if !self.is_transfer() { return Ok(None); @@ -1308,13 +1406,14 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { } /// Convenience function to simplify nesting of [`Self::estimate_gas`]. + #[cfg(feature = "estimate_gas")] fn run_estimate( self, - filler: &GasEstimationFiller, + filler: &crate::fillers::GasEstimationFiller, ) -> Result<(EstimationResult, Self), EvmErrored<'a, Ext, Db>> { let mut estimation = MaybeUninit::uninit(); - let this = self.try_with_gas_estimation_filler(filler, |this| match this.run() { + let this = self.try_with_estimate_gas_filler(filler, |this| match this.run() { Ok(trevm) => { let (e, t) = trevm.take_estimation(); @@ -1378,6 +1477,7 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { /// - Loop. /// /// [here]: https://github.com/paradigmxyz/reth/blob/ad503a08fa242b28ad3c1fea9caa83df2dfcf72d/crates/rpc/rpc-eth-api/src/helpers/estimate.rs#L35-L42 + #[cfg(feature = "estimate_gas")] pub fn estimate_gas(mut self) -> Result<(EstimationResult, Self), EvmErrored<'a, Ext, Db>> { if let Some(est) = unwrap_or_trevm_err!(self.estimate_gas_simple_transfer(), self) { return Ok((EstimationResult::basic_transfer_success(est), self)); @@ -1395,7 +1495,7 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { // Check that the account has enough ETH to cover the gas, and lower if // necessary. - let allowance = unwrap_or_trevm_err!(self.gas_allowance(), self); + let allowance = unwrap_or_trevm_err!(self.caller_gas_allowance(), self); search_range.maybe_lower_max(allowance); // Raise the floor to the amount of gas required to initialize the EVM. diff --git a/src/fill/fillers.rs b/src/fill/fillers.rs index 7be6c39..971fa85 100644 --- a/src/fill/fillers.rs +++ b/src/fill/fillers.rs @@ -1,5 +1,8 @@ -use crate::fill::traits::{Cfg, Tx}; -use revm::primitives::{CfgEnv, TxEnv}; +use crate::{ + fill::traits::{Cfg, Tx}, + Block, +}; +use revm::primitives::{BlockEnv, CfgEnv, TxEnv}; /// A [`Cfg`] that disables gas-related checks and payment of the /// beneficiary reward, while leaving other cfg options unchanged. @@ -62,6 +65,7 @@ pub(crate) struct GasEstimationFiller { pub(crate) gas_limit: u64, } +#[cfg(feature = "estimate_gas")] impl From for GasEstimationFiller { fn from(gas_limit: u64) -> Self { Self { gas_limit } @@ -83,3 +87,30 @@ impl Tx for GasEstimationFiller { tx_env.gas_limit = self.gas_limit; } } + +#[cfg(feature = "call")] +pub(crate) struct CallFiller { + pub gas_limit: u64, +} + +#[cfg(feature = "call")] +impl Cfg for CallFiller { + fn fill_cfg_env(&self, cfg_env: &mut CfgEnv) { + cfg_env.disable_base_fee = true; + cfg_env.disable_eip3607 = true; + } +} + +#[cfg(feature = "call")] +impl Block for CallFiller { + fn fill_block_env(&self, block_env: &mut BlockEnv) { + block_env.gas_limit = alloy::primitives::U256::from(self.gas_limit); + } +} + +#[cfg(feature = "call")] +impl Tx for CallFiller { + fn fill_tx_env(&self, tx_env: &mut TxEnv) { + DisableNonceCheck.fill_tx_env(tx_env); + } +} From 2081dbd3e5e7a8083959e75efc050c6034ab55fb Mon Sep 17 00:00:00 2001 From: James Date: Fri, 21 Feb 2025 10:40:57 -0500 Subject: [PATCH 2/8] chore: version and readme --- Cargo.toml | 2 +- README.md | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8150f86..e51c726 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "trevm" -version = "0.19.6" +version = "0.19.7" rust-version = "1.83.0" edition = "2021" authors = ["init4"] diff --git a/README.md b/README.md index c5eb09d..5038227 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,19 @@ Trevm is useful for: ## Note on Trevm Versioning -Trevm generally uses [semantic versioning](https://semver.org/). However, we -also strive to indicate the MAJOR version of revm in the MINOR version of +Trevm generally uses [semantic versioning](https://semver.org/). While pre-1.0, +we also strive to indicate the MAJOR version of revm in the MINOR version of trevm. For example, trevm `0.19.x` SHOULD BE compatible with revm `19.x.x`. In general, we will try to maintain compatibility with the latest revm version, and will not backport trevm fixes to older trevm or revm versions. It is generally not advised to use old revm versions, as the EVM is a living spec. +In order to maintain this relationship (that trevm MINOR == revm MAJOR) we will +sometimes make breaking changes in patch versions. This is technically semver +compliant pre-1.0, but will cause build breakage downstream for users of those +features. We will take care to document breaking changes in patch releases +via github release notes. + ## Limitations Trevm is a work in progress and is not feature complete. In particular, trevm From 35f3c9d14318705777e9364de0d4a0306efa14b7 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 21 Feb 2025 10:51:23 -0500 Subject: [PATCH 3/8] fix: clean up no-default and all-features --- src/evm.rs | 73 +++++++++++++++++++++++++-------------------- src/fill/fillers.rs | 11 +++---- src/lib.rs | 2 ++ src/macros.rs | 1 + 4 files changed, 48 insertions(+), 39 deletions(-) diff --git a/src/evm.rs b/src/evm.rs index eb77918..9e5f7ce 100644 --- a/src/evm.rs +++ b/src/evm.rs @@ -1,10 +1,8 @@ use crate::{ - driver::DriveBlockResult, - est::{EstimationResult, SearchRange}, - unwrap_or_trevm_err, Block, BlockDriver, BundleDriver, Cfg, ChainDriver, DriveBundleResult, - DriveChainResult, ErroredState, EvmErrored, EvmExtUnchecked, EvmNeedsBlock, EvmNeedsCfg, - EvmNeedsTx, EvmReady, EvmTransacted, HasBlock, HasCfg, HasTx, NeedsCfg, NeedsTx, Ready, - TransactedState, Tx, MIN_TRANSACTION_GAS, + driver::DriveBlockResult, Block, BlockDriver, BundleDriver, Cfg, ChainDriver, + DriveBundleResult, DriveChainResult, ErroredState, EvmErrored, EvmExtUnchecked, EvmNeedsBlock, + EvmNeedsCfg, EvmNeedsTx, EvmReady, EvmTransacted, HasBlock, HasCfg, HasTx, NeedsCfg, NeedsTx, + TransactedState, Tx, }; use alloy::{ primitives::{Address, Bytes, U256}, @@ -13,14 +11,14 @@ use alloy::{ use core::convert::Infallible; use revm::{ db::{states::bundle_state::BundleRetention, BundleState, State}, - interpreter::gas::{calculate_initial_tx_gas, CALL_STIPEND}, + interpreter::gas::calculate_initial_tx_gas, primitives::{ AccountInfo, AuthorizationList, BlockEnv, Bytecode, EVMError, Env, EvmState, - ExecutionResult, InvalidTransaction, ResultAndState, SpecId, TxEnv, TxKind, KECCAK_EMPTY, + ExecutionResult, InvalidTransaction, ResultAndState, SpecId, TxEnv, TxKind, }, Database, DatabaseCommit, DatabaseRef, Evm, }; -use std::{fmt, mem::MaybeUninit}; +use std::fmt; /// Trevm provides a type-safe interface to the EVM, using the typestate pattern. /// @@ -1075,6 +1073,7 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmNeedsTx<'a, Ext, Db> { /// - Nonce checks are disabled. /// /// [EIP-3607]: https://eips.ethereum.org/EIPS/eip-3607 + #[cfg(feature = "call")] pub fn call_tx( self, filler: &T, @@ -1085,11 +1084,13 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmNeedsTx<'a, Ext, Db> { /// Estimate the gas cost of a transaction. Shortcut for `fill_tx(tx). /// estimate()`. Returns an [`EstimationResult`] and the EVM populated with /// the transaction. + /// + /// [`EstimationResult`]: crate::EstimationResult #[cfg(feature = "estimate_gas")] pub fn estimate_tx_gas( self, filler: &T, - ) -> Result<(EstimationResult, EvmReady<'a, Ext, Db>), EvmErrored<'a, Ext, Db>> { + ) -> Result<(crate::EstimationResult, EvmReady<'a, Ext, Db>), EvmErrored<'a, Ext, Db>> { self.fill_tx(filler).estimate_gas() } } @@ -1337,23 +1338,20 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { pub fn call( self, ) -> Result<(ExecutionResult, EvmNeedsTx<'a, Ext, Db>), EvmErrored<'a, Ext, Db>> { - let mut output = MaybeUninit::uninit(); + let mut output = std::mem::MaybeUninit::uninit(); let gas_limit = self.tx().gas_limit; - let this = self.try_with_call_filler( - &crate::fillers::CallFiller { gas_limit }, - |this| match this.run() { - Ok(t) => { - let (o, t) = t.take_result(); + let this = + self.try_with_call_filler(&crate::fillers::CallFiller { gas_limit }, |this| { + let t = this.run()?; - output.write(o); + let (o, t) = t.take_result(); - Ok(t) - } - Err(err) => return Err(err), - }, - )?; + output.write(o); + + Ok(t) + })?; Ok((unsafe { output.assume_init() }, this)) } @@ -1388,6 +1386,8 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { /// - Return the minimum gas required for the transfer. #[cfg(feature = "estimate_gas")] fn estimate_gas_simple_transfer(&mut self) -> Result, EVMError> { + use alloy::consensus::constants::KECCAK_EMPTY; + if !self.is_transfer() { return Ok(None); } @@ -1410,8 +1410,8 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { fn run_estimate( self, filler: &crate::fillers::GasEstimationFiller, - ) -> Result<(EstimationResult, Self), EvmErrored<'a, Ext, Db>> { - let mut estimation = MaybeUninit::uninit(); + ) -> Result<(crate::EstimationResult, Self), EvmErrored<'a, Ext, Db>> { + let mut estimation = std::mem::MaybeUninit::uninit(); let this = self.try_with_estimate_gas_filler(filler, |this| match this.run() { Ok(trevm) => { @@ -1478,9 +1478,11 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { /// /// [here]: https://github.com/paradigmxyz/reth/blob/ad503a08fa242b28ad3c1fea9caa83df2dfcf72d/crates/rpc/rpc-eth-api/src/helpers/estimate.rs#L35-L42 #[cfg(feature = "estimate_gas")] - pub fn estimate_gas(mut self) -> Result<(EstimationResult, Self), EvmErrored<'a, Ext, Db>> { - if let Some(est) = unwrap_or_trevm_err!(self.estimate_gas_simple_transfer(), self) { - return Ok((EstimationResult::basic_transfer_success(est), self)); + pub fn estimate_gas( + mut self, + ) -> Result<(crate::EstimationResult, Self), EvmErrored<'a, Ext, Db>> { + if let Some(est) = crate::unwrap_or_trevm_err!(self.estimate_gas_simple_transfer(), self) { + return Ok((crate::EstimationResult::basic_transfer_success(est), self)); } // We shrink the gas limit to 64 bits, as using more than 18 quintillion @@ -1488,7 +1490,8 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { let initial_limit = self.gas_limit(); // Start the search range at 21_000 gas. - let mut search_range = SearchRange::new(MIN_TRANSACTION_GAS, initial_limit); + let mut search_range = + crate::est::SearchRange::new(crate::MIN_TRANSACTION_GAS, initial_limit); // Block it to the gas cap. search_range.maybe_lower_max(self.block_gas_limit().saturating_to::()); @@ -1529,7 +1532,7 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { // NB: 64 / 63 is due to Ethereum's gas-forwarding rules. Each call // frame can forward only 63/64 of the gas it has when it makes a new // frame. - let mut needle = gas_used + gas_refunded + CALL_STIPEND * 64 / 63; + let mut needle = gas_used + gas_refunded + revm::interpreter::gas::CALL_STIPEND * 64 / 63; // If the first search is outside the range, we don't need to try it. if search_range.contains(needle) { estimate_and_adjust!(estimate, trevm, needle, search_range); @@ -1753,16 +1756,22 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmTransacted<'a, Ext, Db> { } /// Create an [`EstimationResult`] from the transaction [`ExecutionResult`]. - pub fn estimation(&self) -> EstimationResult { + /// + /// [`EstimationResult`]: crate::EstimationResult + #[cfg(feature = "estimate_gas")] + pub fn estimation(&self) -> crate::EstimationResult { self.result().into() } /// Take the [`EstimationResult`] and return it and the EVM. This discards /// pending state changes, but leaves the EVM ready to execute the same /// transaction again. - pub fn take_estimation(self) -> (EstimationResult, EvmReady<'a, Ext, Db>) { + /// + /// [`EstimationResult`]: crate::EstimationResult + #[cfg(feature = "estimate_gas")] + pub fn take_estimation(self) -> (crate::EstimationResult, EvmReady<'a, Ext, Db>) { let estimation = self.estimation(); - (estimation, Trevm { inner: self.inner, state: Ready::new() }) + (estimation, Trevm { inner: self.inner, state: crate::Ready::new() }) } } diff --git a/src/fill/fillers.rs b/src/fill/fillers.rs index 971fa85..b733b98 100644 --- a/src/fill/fillers.rs +++ b/src/fill/fillers.rs @@ -1,8 +1,5 @@ -use crate::{ - fill::traits::{Cfg, Tx}, - Block, -}; -use revm::primitives::{BlockEnv, CfgEnv, TxEnv}; +use crate::fill::traits::{Cfg, Tx}; +use revm::primitives::{CfgEnv, TxEnv}; /// A [`Cfg`] that disables gas-related checks and payment of the /// beneficiary reward, while leaving other cfg options unchanged. @@ -102,8 +99,8 @@ impl Cfg for CallFiller { } #[cfg(feature = "call")] -impl Block for CallFiller { - fn fill_block_env(&self, block_env: &mut BlockEnv) { +impl crate::Block for CallFiller { + fn fill_block_env(&self, block_env: &mut revm::primitives::BlockEnv) { block_env.gas_limit = alloy::primitives::U256::from(self.gas_limit); } } diff --git a/src/lib.rs b/src/lib.rs index e06d4f4..0670dbe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -380,7 +380,9 @@ pub use driver::{ mod evm; pub use evm::Trevm; +#[cfg(feature = "estimate_gas")] mod est; +#[cfg(feature = "estimate_gas")] pub use est::EstimationResult; mod ext; diff --git a/src/macros.rs b/src/macros.rs index 606bdfa..0289bd7 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -28,6 +28,7 @@ macro_rules! trevm_bail { } /// Macro for gas estimation binary search loop. +#[cfg(feature = "estimate_gas")] macro_rules! estimate_and_adjust { ($est:ident, $trevm:ident, $gas_limit:ident, $range:ident) => { ($est, $trevm) = $trevm.run_estimate(&$gas_limit.into())?; From 747c855b1418a4cf20c891ef09393efec5466c72 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 21 Feb 2025 10:57:03 -0500 Subject: [PATCH 4/8] fix: docs --- src/evm.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/evm.rs b/src/evm.rs index 9e5f7ce..d8a139d 100644 --- a/src/evm.rs +++ b/src/evm.rs @@ -1477,6 +1477,8 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { /// - Loop. /// /// [here]: https://github.com/paradigmxyz/reth/blob/ad503a08fa242b28ad3c1fea9caa83df2dfcf72d/crates/rpc/rpc-eth-api/src/helpers/estimate.rs#L35-L42 + /// [`EstimationREsult`]: crate::EstimationResult + /// [`MIN_TRANSACTION_GAS`]: crate::MIN_TRANSACTION_GAS #[cfg(feature = "estimate_gas")] pub fn estimate_gas( mut self, From 4ab0fe594bea6983f9f0db5f24ac3c50c7228aef Mon Sep 17 00:00:00 2001 From: James Date: Mon, 24 Feb 2025 12:14:06 -0500 Subject: [PATCH 5/8] feat: cap gas on tx --- src/evm.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ src/fill/alloy.rs | 5 ++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/evm.rs b/src/evm.rs index d8a139d..dcfc7ae 100644 --- a/src/evm.rs +++ b/src/evm.rs @@ -1264,6 +1264,53 @@ impl<'a, Ext, Db: Database + DatabaseCommit, TrevmState: HasTx> Trevm<'a, Ext, D let gas_price = self.gas_price(); self.try_gas_allowance(self.caller(), gas_price).map_err(EVMError::Database) } + + /// This function caps the gas limit of the transaction to the allowance of + /// the caller. + /// + /// This is useful for e.g. call simulation, where the exact amount of gas + /// used is less important than ensuring that the call succeeds and returns + /// a meaningful result. + /// + /// # Returns + /// + /// The gas limit after the operation. + pub fn cap_tx_gas_to_allowance(&mut self) -> Result> { + let allowance = self.caller_gas_allowance()?; + let tx = self.inner_mut_unchecked().tx_mut(); + tx.gas_limit = tx.gas_limit.min(allowance); + Ok(tx.gas_limit) + } + + /// Cap the gas limit of the transaction to the minimum of the block gas + /// limit and the transaction's gas limit. + /// + /// This is useful for ensuring that the transaction does not exceed the + /// block gas limit, e.g. during call simulation. + /// + /// # Returns + /// + /// The gas limit after the operation. + pub fn cap_tx_gas_to_block_limit(&mut self) -> u64 { + let block_gas_limit = self.block_gas_limit().saturating_to(); + let tx = self.inner_mut_unchecked().tx_mut(); + tx.gas_limit = tx.gas_limit.min(block_gas_limit); + tx.gas_limit + } + + /// This function caps the gas limit of the transaction to the minimum of + /// the block limit and the caller's gas allowance. + /// + /// This is equivalent to calling [`Self::cap_tx_gas_to_block_limit`] and + /// [`Self::cap_tx_gas_to_allowance`] in sequence. + /// + /// # Returns + /// + /// The gas limit after the operation. + pub fn cap_tx_gas(&mut self) -> Result> { + self.cap_tx_gas_to_block_limit(); + self.cap_tx_gas_to_allowance() + } } // -- NEEDS TX with State diff --git a/src/fill/alloy.rs b/src/fill/alloy.rs index 7efe558..81a769d 100644 --- a/src/fill/alloy.rs +++ b/src/fill/alloy.rs @@ -303,7 +303,10 @@ impl Tx for alloy::rpc::types::TransactionRequest { } = tx_env; *caller = self.from.unwrap_or_default(); - *gas_limit = self.gas.unwrap_or_default(); + + // NB: this is set to max if not provided, as users will typically + // intend that to mean "as much as possible" + *gas_limit = self.gas.unwrap_or(u64::MAX); *gas_price = U256::from(self.gas_price.unwrap_or_default()); *transact_to = self.to.unwrap_or_default(); *value = self.value.unwrap_or_default(); From b1d0d864225c4bb20f500c77f88c7f774112a3ae Mon Sep 17 00:00:00 2001 From: James Date: Mon, 24 Feb 2025 12:59:05 -0500 Subject: [PATCH 6/8] fix: estimation midpoint --- Cargo.toml | 1 + src/est.rs | 9 ++++++++- src/evm.rs | 17 ++++++++++------- src/macros.rs | 6 ++++++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e51c726..9c305c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ revm = { version = "19.5.0", default-features = false, features = ["std"] } zenith-types = { version = "0.14" } dashmap = { version = "6.1.0", optional = true } +tracing = "0.1.41" [dev-dependencies] alloy-rlp = { version = "0.3", default-features = false } diff --git a/src/est.rs b/src/est.rs index 86db0f9..f7baa0e 100644 --- a/src/est.rs +++ b/src/est.rs @@ -5,6 +5,12 @@ use std::ops::Range; /// binary searching. pub(crate) struct SearchRange(Range); +impl core::fmt::Display for SearchRange { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}..{}", self.0.start, self.0.end) + } +} + impl From> for SearchRange { fn from(value: Range) -> Self { Self(value) @@ -25,7 +31,7 @@ impl SearchRange { /// Calculate the midpoint of the search range. pub(crate) const fn midpoint(&self) -> u64 { - (self.0.end - self.0.start) / 2 + (self.max() - self.min()) / 2 + self.min() } /// Get the start of the search range. @@ -38,6 +44,7 @@ impl SearchRange { self.0.start = min; } + /// Raise the minimum of the search range, if the candidate is higher. pub(crate) const fn maybe_raise_min(&mut self, candidate: u64) { if candidate > self.min() { self.set_min(candidate); diff --git a/src/evm.rs b/src/evm.rs index dcfc7ae..5a426ef 100644 --- a/src/evm.rs +++ b/src/evm.rs @@ -1542,13 +1542,16 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { let mut search_range = crate::est::SearchRange::new(crate::MIN_TRANSACTION_GAS, initial_limit); - // Block it to the gas cap. - search_range.maybe_lower_max(self.block_gas_limit().saturating_to::()); - - // Check that the account has enough ETH to cover the gas, and lower if - // necessary. - let allowance = unwrap_or_trevm_err!(self.caller_gas_allowance(), self); - search_range.maybe_lower_max(allowance); + let span = tracing::debug_span!( + "estimate_gas", + start_min = search_range.min(), + start_max = search_range.max() + ); + let _e = span.enter(); + + // Cap the gas limit to the caller's allowance and block limit + unwrap_or_trevm_err!(self.cap_tx_gas(), self); + search_range.maybe_lower_max(self.gas_limit()); // Raise the floor to the amount of gas required to initialize the EVM. search_range.maybe_raise_min(self.calculate_initial_gas()); diff --git a/src/macros.rs b/src/macros.rs index 0289bd7..c3860fa 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -31,6 +31,12 @@ macro_rules! trevm_bail { #[cfg(feature = "estimate_gas")] macro_rules! estimate_and_adjust { ($est:ident, $trevm:ident, $gas_limit:ident, $range:ident) => { + ::tracing::trace!( + gas_limit = $gas_limit, + range = %$range, + "running gas estimate call" + ); + ($est, $trevm) = $trevm.run_estimate(&$gas_limit.into())?; if let Err(e) = $est.adjust_binary_search_range($gas_limit, &mut $range) { return Ok((e, $trevm)); From 2afa67283a7e53d2221baf45cd48aeab7885bb8b Mon Sep 17 00:00:00 2001 From: James Date: Mon, 24 Feb 2025 13:50:14 -0500 Subject: [PATCH 7/8] fix: estimation result --- src/est.rs | 32 ++++++++++++++++++++++++++------ src/evm.rs | 6 ++++-- src/macros.rs | 1 + 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/est.rs b/src/est.rs index f7baa0e..1da67e4 100644 --- a/src/est.rs +++ b/src/est.rs @@ -31,7 +31,7 @@ impl SearchRange { /// Calculate the midpoint of the search range. pub(crate) const fn midpoint(&self) -> u64 { - (self.max() - self.min()) / 2 + self.min() + (self.max() + self.min()) / 2 } /// Get the start of the search range. @@ -117,11 +117,33 @@ pub enum EstimationResult { }, } -impl From<&ExecutionResult> for EstimationResult { - fn from(value: &ExecutionResult) -> Self { +impl core::fmt::Display for EstimationResult { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Success { estimation, refund, gas_used, .. } => { + write!( + f, + "Success {{ estimation: {}, refund: {}, gas_used: {}, .. }}", + estimation, refund, gas_used + ) + } + Self::Revert { gas_used, .. } => { + write!(f, "Revert {{ gas_used: {}, .. }}", gas_used) + } + Self::Halt { reason, gas_used } => { + write!(f, "Halt {{ reason: {:?}, gas_used: {} }}", reason, gas_used) + } + } + } +} + +impl EstimationResult { + /// Initialize the estimation result from an execution result and the gas + /// limit of the transaction that produced the estimation. + pub fn from_limit_and_execution_result(limit: u64, value: &ExecutionResult) -> Self { match value { ExecutionResult::Success { gas_used, output, gas_refunded, .. } => Self::Success { - estimation: *gas_used, + estimation: limit, refund: *gas_refunded, gas_used: *gas_used, output: output.clone(), @@ -134,9 +156,7 @@ impl From<&ExecutionResult> for EstimationResult { } } } -} -impl EstimationResult { /// Create a successful estimation result with a gas estimation of 21000. pub const fn basic_transfer_success(estimation: u64) -> Self { Self::Success { diff --git a/src/evm.rs b/src/evm.rs index 5a426ef..4b8d16a 100644 --- a/src/evm.rs +++ b/src/evm.rs @@ -1598,7 +1598,7 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { needle = std::cmp::min(gas_used * 3, search_range.midpoint()); // Binary search loop. - // This is a heuristic adopted from reth + // The second conditional is a heuristic adopted from geth and reth. // An estimation error is allowed once the current gas limit range // used in the binary search is small enough (less than 1.5% of the // highest gas limit) @@ -1812,7 +1812,9 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmTransacted<'a, Ext, Db> { /// [`EstimationResult`]: crate::EstimationResult #[cfg(feature = "estimate_gas")] pub fn estimation(&self) -> crate::EstimationResult { - self.result().into() + use crate::EstimationResult; + + EstimationResult::from_limit_and_execution_result(self.gas_limit(), self.result()) } /// Take the [`EstimationResult`] and return it and the EVM. This discards diff --git a/src/macros.rs b/src/macros.rs index c3860fa..7135e38 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -32,6 +32,7 @@ macro_rules! trevm_bail { macro_rules! estimate_and_adjust { ($est:ident, $trevm:ident, $gas_limit:ident, $range:ident) => { ::tracing::trace!( + estimate = %$est, gas_limit = $gas_limit, range = %$range, "running gas estimate call" From 1c0eb5bba0ee88ed2a359c360c44622506c059ca Mon Sep 17 00:00:00 2001 From: James Date: Mon, 24 Feb 2025 14:30:25 -0500 Subject: [PATCH 8/8] feat: misc tracing --- src/evm.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/evm.rs b/src/evm.rs index 4b8d16a..ade44ce 100644 --- a/src/evm.rs +++ b/src/evm.rs @@ -1434,6 +1434,7 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { #[cfg(feature = "estimate_gas")] fn estimate_gas_simple_transfer(&mut self) -> Result, EVMError> { use alloy::consensus::constants::KECCAK_EMPTY; + use tracing::trace; if !self.is_transfer() { return Ok(None); @@ -1449,7 +1450,9 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { // delegate calculation to revm. This ensures that things like bogus // 2930 access lists don't mess up our estimates - Ok(Some(self.calculate_initial_gas())) + let initial = self.calculate_initial_gas(); + trace!(initial, "using initial gas for simple transfer"); + Ok(Some(initial)) } /// Convenience function to simplify nesting of [`Self::estimate_gas`]. @@ -1530,6 +1533,8 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { pub fn estimate_gas( mut self, ) -> Result<(crate::EstimationResult, Self), EvmErrored<'a, Ext, Db>> { + use tracing::{debug, enabled}; + if let Some(est) = crate::unwrap_or_trevm_err!(self.estimate_gas_simple_transfer(), self) { return Ok((crate::EstimationResult::basic_transfer_success(est), self)); } @@ -1545,8 +1550,12 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { let span = tracing::debug_span!( "estimate_gas", start_min = search_range.min(), - start_max = search_range.max() + start_max = search_range.max(), + tx = "omitted", ); + if enabled!(tracing::Level::TRACE) { + span.record("tx", format!("{:?}", &self.tx())); + } let _e = span.enter(); // Cap the gas limit to the caller's allowance and block limit @@ -1559,11 +1568,13 @@ impl<'a, Ext, Db: Database + DatabaseCommit> EvmReady<'a, Ext, Db> { // Run an estimate with the max gas limit. // NB: we declare these mut as we re-use the binding throughout the // function. + debug!(gas_limit = search_range.max(), "running optimistic estimate"); let (mut estimate, mut trevm) = self.run_estimate(&search_range.max().into())?; // If it failed, no amount of gas is likely to work, so we shortcut // return. if estimate.is_failure() { + debug!(%estimate, "optimistic estimate failed"); return Ok((estimate, trevm)); }