Skip to content

Commit 74cb161

Browse files
authored
feat: estimation (#77)
* feat: estimation * fix: use a max * feat: shortcut for estimate_tx_gas * fix: callee_account * fix: callee_account * nit: gas allowance * lint: clippy * doc: fixlink * nit: remove * chore: doc algorithm * refactor: clean up and add a macro * nit: search * refactor: search range * docs: add one * refactor: more cleaning * nit: add more comments * feat: blanket impl Block, Tx, Cfg for Fn() types * chore: bump version * refactor: use maybeuninit to save a tiny amount of resources * nits: clean up some naming
1 parent a29c7f9 commit 74cb161

File tree

9 files changed

+751
-74
lines changed

9 files changed

+751
-74
lines changed

Cargo.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "trevm"
3-
version = "0.19.2"
3+
version = "0.19.3"
44
rust-version = "1.83.0"
55
edition = "2021"
66
authors = ["init4"]
@@ -56,6 +56,7 @@ tokio = { version = "1.39", features = ["macros", "rt-multi-thread"] }
5656
[features]
5757
default = [
5858
"concurrent-db",
59+
"estimate_gas",
5960
"revm/std",
6061
"revm/c-kzg",
6162
"revm/blst",
@@ -65,6 +66,8 @@ default = [
6566

6667
concurrent-db = ["dep:dashmap"]
6768

69+
estimate_gas = ["optional_eip3607", "optional_no_base_fee"]
70+
6871
test-utils = ["revm/test-utils", "revm/std", "revm/serde-json", "revm/alloydb"]
6972

7073
secp256k1 = ["revm/secp256k1"]

src/est.rs

+264
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
use crate::MIN_TRANSACTION_GAS;
2+
use revm::primitives::{Bytes, ExecutionResult, HaltReason, Output};
3+
use std::ops::Range;
4+
5+
/// Simple wrapper around a range of u64 values, with convenience methods for
6+
/// binary searching.
7+
pub(crate) struct SearchRange(Range<u64>);
8+
9+
impl From<Range<u64>> for SearchRange {
10+
fn from(value: Range<u64>) -> Self {
11+
Self(value)
12+
}
13+
}
14+
15+
impl From<SearchRange> for Range<u64> {
16+
fn from(value: SearchRange) -> Self {
17+
value.0
18+
}
19+
}
20+
21+
impl SearchRange {
22+
/// Create a new search range.
23+
pub(crate) const fn new(start: u64, end: u64) -> Self {
24+
Self(start..end)
25+
}
26+
27+
/// Calculate the midpoint of the search range.
28+
pub(crate) const fn midpoint(&self) -> u64 {
29+
(self.0.end - self.0.start) / 2
30+
}
31+
32+
/// Get the start of the search range.
33+
pub(crate) const fn min(&self) -> u64 {
34+
self.0.start
35+
}
36+
37+
/// Set the start of the search range.
38+
pub(crate) const fn set_min(&mut self, min: u64) {
39+
self.0.start = min;
40+
}
41+
42+
pub(crate) const fn maybe_raise_min(&mut self, candidate: u64) {
43+
if candidate > self.min() {
44+
self.set_min(candidate);
45+
}
46+
}
47+
48+
/// Get the end of the search range.
49+
pub(crate) const fn max(&self) -> u64 {
50+
self.0.end
51+
}
52+
53+
/// Set the end of the search range.
54+
pub(crate) const fn set_max(&mut self, max: u64) {
55+
self.0.end = max;
56+
}
57+
58+
/// Lower the maximum of the search range, if the candidate is lower.
59+
pub(crate) const fn maybe_lower_max(&mut self, candidate: u64) {
60+
if candidate < self.max() {
61+
self.set_max(candidate);
62+
}
63+
}
64+
65+
/// Calculate the search ratio.
66+
pub(crate) const fn ratio(&self) -> f64 {
67+
(self.max() - self.min()) as f64 / self.max() as f64
68+
}
69+
70+
/// True if the search range contains the given value.
71+
pub(crate) fn contains(&self, value: u64) -> bool {
72+
self.0.contains(&value)
73+
}
74+
75+
/// Return the size of the range.
76+
pub(crate) const fn size(&self) -> u64 {
77+
self.0.end - self.0.start
78+
}
79+
}
80+
81+
/// The result of gas estimation.
82+
///
83+
/// This is a trimmed version of [`ExecutionResult`], that contains only
84+
/// information relevant to gas estimation.
85+
#[derive(Debug, Clone, PartialEq, Eq)]
86+
pub enum EstimationResult {
87+
/// The estimation was successful, the result is the gas estimation.
88+
Success {
89+
/// The gas estimation.
90+
estimation: u64,
91+
/// The amount of gas that was refunded to the caller as unused.
92+
refund: u64,
93+
/// The amount of gas used in the execution.
94+
gas_used: u64,
95+
/// The output of execution.
96+
output: Output,
97+
},
98+
/// Estimation failed due to contract revert.
99+
Revert {
100+
/// The revert reason.
101+
reason: Bytes,
102+
/// The amount of gas used in the execution.
103+
gas_used: u64,
104+
},
105+
/// The estimation failed due to EVM halt.
106+
Halt {
107+
/// The halt reason.
108+
reason: HaltReason,
109+
/// The amount of gas used in the execution
110+
gas_used: u64,
111+
},
112+
}
113+
114+
impl From<&ExecutionResult> for EstimationResult {
115+
fn from(value: &ExecutionResult) -> Self {
116+
match value {
117+
ExecutionResult::Success { gas_used, output, gas_refunded, .. } => Self::Success {
118+
estimation: *gas_used,
119+
refund: *gas_refunded,
120+
gas_used: *gas_used,
121+
output: output.clone(),
122+
},
123+
ExecutionResult::Revert { output, gas_used } => {
124+
Self::Revert { reason: output.clone(), gas_used: *gas_used }
125+
}
126+
ExecutionResult::Halt { reason, gas_used } => {
127+
Self::Halt { reason: *reason, gas_used: *gas_used }
128+
}
129+
}
130+
}
131+
}
132+
133+
impl EstimationResult {
134+
/// Create a successful estimation result with a gas estimation of 21000.
135+
pub const fn basic_transfer_success() -> Self {
136+
Self::Success {
137+
estimation: MIN_TRANSACTION_GAS,
138+
refund: 0,
139+
gas_used: MIN_TRANSACTION_GAS,
140+
output: Output::Call(Bytes::new()),
141+
}
142+
}
143+
144+
/// Return true if the execution was successful.
145+
pub const fn is_success(&self) -> bool {
146+
matches!(self, Self::Success { .. })
147+
}
148+
149+
/// Return true if the execution was not successful.
150+
pub const fn is_failure(&self) -> bool {
151+
!self.is_success()
152+
}
153+
154+
/// Get the gas estimation, if the execution was successful.
155+
pub const fn gas_estimation(&self) -> Option<u64> {
156+
match self {
157+
Self::Success { estimation, .. } => Some(*estimation),
158+
_ => None,
159+
}
160+
}
161+
162+
/// Get the gas refunded, if the execution was successful.
163+
pub const fn gas_refunded(&self) -> Option<u64> {
164+
match self {
165+
Self::Success { refund, .. } => Some(*refund),
166+
_ => None,
167+
}
168+
}
169+
170+
/// Get the output, if the execution was successful.
171+
pub const fn output(&self) -> Option<&Output> {
172+
match self {
173+
Self::Success { output, .. } => Some(output),
174+
_ => None,
175+
}
176+
}
177+
178+
/// Get the gas used in execution, regardless of the outcome.
179+
pub const fn gas_used(&self) -> u64 {
180+
match self {
181+
Self::Success { gas_used, .. } => *gas_used,
182+
Self::Revert { gas_used, .. } => *gas_used,
183+
Self::Halt { gas_used, .. } => *gas_used,
184+
}
185+
}
186+
187+
/// Return true if the execution failed due to revert.
188+
pub const fn is_revert(&self) -> bool {
189+
matches!(self, Self::Revert { .. })
190+
}
191+
192+
/// Get the revert reason if the execution failed due to revert.
193+
pub const fn revert_reason(&self) -> Option<&Bytes> {
194+
match self {
195+
Self::Revert { reason, .. } => Some(reason),
196+
_ => None,
197+
}
198+
}
199+
200+
/// Return true if the execution failed due to EVM halt.
201+
pub const fn is_halt(&self) -> bool {
202+
matches!(self, Self::Halt { .. })
203+
}
204+
205+
/// Get the halt reason if the execution failed due to EVM halt.
206+
pub const fn halt_reason(&self) -> Option<&HaltReason> {
207+
match self {
208+
Self::Halt { reason, .. } => Some(reason),
209+
_ => None,
210+
}
211+
}
212+
213+
/// Adjust the binary search range based on the estimation outcome.
214+
pub(crate) fn adjust_binary_search_range(
215+
&self,
216+
limit: u64,
217+
range: &mut SearchRange,
218+
) -> Result<(), Self> {
219+
match self {
220+
Self::Success { .. } => range.set_max(limit),
221+
Self::Revert { .. } => range.set_min(limit),
222+
Self::Halt { reason, gas_used } => {
223+
// Both `OutOfGas` and `InvalidEFOpcode` can occur dynamically
224+
// if the gas left is too low. Treat this as an out of gas
225+
// condition, knowing that the call succeeds with a
226+
// higher gas limit.
227+
//
228+
// Common usage of invalid opcode in OpenZeppelin:
229+
// <https://github.com/OpenZeppelin/openzeppelin-contracts/blob/94697be8a3f0dfcd95dfb13ffbd39b5973f5c65d/contracts/metatx/ERC2771Forwarder.sol#L360-L367>
230+
if matches!(reason, HaltReason::OutOfGas(_) | HaltReason::InvalidFEOpcode) {
231+
range.set_min(limit);
232+
} else {
233+
return Err(Self::Halt { reason: *reason, gas_used: *gas_used });
234+
}
235+
}
236+
}
237+
Ok(())
238+
}
239+
}
240+
241+
// Some code above is reproduced from `reth`. It is reused here under the MIT
242+
// license.
243+
//
244+
// The MIT License (MIT)
245+
//
246+
// Copyright (c) 2022-2024 Reth Contributors
247+
//
248+
// Permission is hereby granted, free of charge, to any person obtaining a copy
249+
// of this software and associated documentation files (the "Software"), to deal
250+
// in the Software without restriction, including without limitation the rights
251+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
252+
// copies of the Software, and to permit persons to whom the Software is
253+
// furnished to do so, subject to the following conditions:
254+
//
255+
// The above copyright notice and this permission notice shall be included in
256+
// all copies or substantial portions of the Software.
257+
//
258+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
259+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
260+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
261+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
262+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
263+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
264+
// THE SOFTWARE.

0 commit comments

Comments
 (0)