Skip to content

Commit a266eb2

Browse files
zsluedemVid201
authored andcommitted
feat: new estimate method
1 parent 7c5dacf commit a266eb2

File tree

10 files changed

+653
-136
lines changed

10 files changed

+653
-136
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/contracts/src/entry_point.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,19 @@ use super::{
1515
},
1616
tracer::JS_TRACER,
1717
};
18-
use crate::{error::decode_revert_error, gen::ExecutionResult};
18+
use crate::{error::decode_revert_error, executor_tracer::EXECUTOR_TRACER, gen::ExecutionResult};
1919
use ethers::{
2020
prelude::{ContractError, Event},
2121
providers::Middleware,
2222
types::{
23-
Address, Bytes, GethDebugTracerType, GethDebugTracingCallOptions, GethDebugTracingOptions,
24-
GethTrace, TransactionRequest, U256,
23+
spoof, transaction::eip2718::TypedTransaction, Address, Bytes, GethDebugTracerType,
24+
GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, TransactionRequest, U256,
2525
},
2626
};
2727
use std::sync::Arc;
2828

29+
const UINT96_MAX: u128 = 5192296858534827628530496329220095;
30+
2931
#[derive(Clone, Debug, PartialEq, Eq)]
3032
pub enum SimulateValidationResult {
3133
ValidationResult(ValidationResult),
@@ -133,6 +135,43 @@ impl<M: Middleware + 'static> EntryPoint<M> {
133135
Ok(res)
134136
}
135137

138+
pub async fn simulate_handle_op_trace<U: Into<UserOperation>>(
139+
&self,
140+
uo: U,
141+
) -> Result<GethTrace, EntryPointError> {
142+
let uo = uo.into();
143+
let max_fee_per_gas = uo.max_fee_per_gas;
144+
let call = self.entry_point_api.simulate_handle_op(uo, Address::zero(), Bytes::default());
145+
let mut tx: TypedTransaction = call.tx;
146+
tx.set_from(Address::zero());
147+
tx.set_gas_price(max_fee_per_gas);
148+
tx.set_gas(u64::MAX);
149+
let res = self
150+
.eth_client
151+
.debug_trace_call(
152+
tx,
153+
None,
154+
GethDebugTracingCallOptions {
155+
tracing_options: GethDebugTracingOptions {
156+
disable_storage: None,
157+
disable_stack: None,
158+
enable_memory: None,
159+
enable_return_data: None,
160+
tracer: Some(GethDebugTracerType::JsTracer(EXECUTOR_TRACER.into())),
161+
tracer_config: None,
162+
timeout: None,
163+
},
164+
state_overrides: Some(spoof::balance(Address::zero(), UINT96_MAX.into())),
165+
},
166+
)
167+
.await
168+
.map_err(|e| {
169+
EntryPointError::from_middleware_error::<M>(e).expect_err("trace err is expected")
170+
})?;
171+
172+
Ok(res)
173+
}
174+
136175
pub async fn handle_ops<U: Into<UserOperation>>(
137176
&self,
138177
uos: Vec<U>,

crates/contracts/src/error.rs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ pub enum EntryPointError {
1515
#[error("{0}")]
1616
FailedOp(FailedOp),
1717

18+
/// execution reverted
19+
#[error("execution reverted: {0}")]
20+
ExecutionReverted(String),
21+
1822
/// There is no revert when there should be
1923
#[error("{function} should revert")]
2024
NoRevert {
@@ -129,23 +133,25 @@ impl EntryPointError {
129133
Err(Self::Provider { inner: format!("middleware error: {err:?}") })
130134
}
131135
}
132-
136+
// ethers-rs could not handle `require (true, "reason")` or `revert("test failed")` well in this
137+
// case revert with `require` error would ends up with error event signature `0x08c379a0`
138+
// we need to handle it manually
139+
pub fn decode_revert_string(data: Bytes) -> Option<String> {
140+
let (error_sig, reason) = data.split_at(4);
141+
if error_sig == [0x08, 0xc3, 0x79, 0xa0] {
142+
<String as AbiDecode>::decode(reason).ok()
143+
} else {
144+
None
145+
}
146+
}
133147
pub fn decode_revert_error(data: Bytes) -> Result<EntryPointAPIErrors, EntryPointError> {
134148
let decoded = EntryPointAPIErrors::decode(data.as_ref());
135149
match decoded {
136150
Ok(res) => Ok(res),
137151
Err(e) => {
138-
// ethers-rs could not handle `require (true, "reason")` well in this case
139-
// revert with `require` error would ends up with error event signature `0x08c379a0`
140-
// we need to handle it manually
141-
let (error_sig, reason) = data.split_at(4);
142-
if error_sig == [0x08, 0xc3, 0x79, 0xa0] {
143-
return <String as AbiDecode>::decode(reason)
144-
.map(EntryPointAPIErrors::RevertString)
145-
.map_err(|e| EntryPointError::Decode {
146-
inner: format!("data field can't be deserialized to revert error: {e:?}",),
147-
});
148-
}
152+
if let Some(error_str) = decode_revert_string(data) {
153+
return Ok(EntryPointAPIErrors::RevertString(error_str));
154+
};
149155

150156
Err(EntryPointError::Decode {
151157
inner: format!(
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
use ethers::types::GethTrace;
2+
use eyre::format_err;
3+
use serde::Deserialize;
4+
5+
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
6+
pub struct LogInfo {
7+
pub topics: Vec<String>,
8+
pub data: String,
9+
}
10+
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
11+
pub struct ExecutorTracerResult {
12+
pub reverts: Vec<String>,
13+
#[serde(rename = "validationOOG")]
14+
pub validation_oog: bool,
15+
#[serde(rename = "executionOOG")]
16+
pub execution_oog: bool,
17+
#[serde(rename = "executionGasLimit")]
18+
pub execution_gas_limit: u64,
19+
#[serde(rename = "userOperationEvent")]
20+
pub user_op_event: Option<LogInfo>,
21+
#[serde(rename = "userOperationRevertEvent")]
22+
pub user_op_revert_event: Option<LogInfo>,
23+
pub output: String,
24+
pub error: String,
25+
}
26+
impl TryFrom<GethTrace> for ExecutorTracerResult {
27+
type Error = eyre::Error;
28+
fn try_from(val: GethTrace) -> Result<Self, Self::Error> {
29+
match val {
30+
GethTrace::Known(val) => Err(format_err!("Invalid geth trace: {val:?}")),
31+
GethTrace::Unknown(val) => serde_json::from_value(val.clone())
32+
.map_err(|error| format_err!("Failed to parse geth trace: {error}, {val:#}")),
33+
}
34+
}
35+
}
36+
pub const EXECUTOR_TRACER: &str = r#"
37+
{
38+
reverts: [],
39+
validationOOG: false,
40+
executionOOG: false,
41+
executionGasLimit: 0,
42+
43+
_depth: 0,
44+
_executionGasStack: [],
45+
_defaultGasItem: { used: 0, required: 0 },
46+
_marker: 0,
47+
_validationMarker: 1,
48+
_executionMarker: 3,
49+
_userOperationEventTopics0:
50+
"0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f",
51+
_userOperationRevertEventTopics0:
52+
"0x1c4fada7374c0a9ee8841fc38afe82932dc0f8e69012e927f061a8bae611a201",
53+
54+
_isValidation: function () {
55+
return (
56+
this._marker >= this._validationMarker &&
57+
this._marker < this._executionMarker
58+
);
59+
},
60+
61+
_isExecution: function () {
62+
return this._marker === this._executionMarker;
63+
},
64+
65+
_isUserOperationEvent: function (log) {
66+
var topics0 = "0x" + log.stack.peek(2).toString(16);
67+
return topics0 === this._userOperationEventTopics0;
68+
},
69+
70+
_setUserOperationEvent: function (opcode, log) {
71+
var count = parseInt(opcode.substring(3));
72+
var ofs = parseInt(log.stack.peek(0).toString());
73+
var len = parseInt(log.stack.peek(1).toString());
74+
var topics = [];
75+
for (var i = 0; i < count; i++) {
76+
topics.push(log.stack.peek(2 + i).toString(16));
77+
}
78+
var data = toHex(log.memory.slice(ofs, ofs + len));
79+
this.userOperationEvent = {
80+
topics: topics,
81+
data: data,
82+
};
83+
},
84+
85+
_isUserOperationRevertEvent: function (log) {
86+
var topics0 = "0x" + log.stack.peek(2).toString(16);
87+
return topics0 === this._userOperationRevertEventTopics0;
88+
},
89+
90+
_setUserOperationRevertEvent: function (opcode, log) {
91+
var count = parseInt(opcode.substring(3));
92+
var ofs = parseInt(log.stack.peek(0).toString());
93+
var len = parseInt(log.stack.peek(1).toString());
94+
var topics = [];
95+
for (var i = 0; i < count; i++) {
96+
topics.push(log.stack.peek(2 + i).toString(16));
97+
}
98+
var data = toHex(log.memory.slice(ofs, ofs + len));
99+
this.userOperationRevertEvent = {
100+
topics: topics,
101+
data: data,
102+
};
103+
},
104+
fault: function fault(log, db) {},
105+
result: function result(ctx, db) {
106+
return {
107+
reverts: this.reverts,
108+
validationOOG: this.validationOOG,
109+
executionOOG: this.executionOOG,
110+
executionGasLimit: this.executionGasLimit,
111+
userOperationEvent: this.userOperationEvent,
112+
userOperationRevertEvent: this.userOperationRevertEvent,
113+
output: toHex(ctx.output),
114+
error: ctx.error,
115+
};
116+
},
117+
118+
enter: function enter(frame) {
119+
if (this._isExecution()) {
120+
var next = this._depth + 1;
121+
if (this._executionGasStack[next] === undefined)
122+
this._executionGasStack[next] = Object.assign({}, this._defaultGasItem);
123+
}
124+
},
125+
exit: function exit(frame) {
126+
if (this._isExecution()) {
127+
if (frame.getError() !== undefined) {
128+
this.reverts.push(toHex(frame.getOutput()));
129+
}
130+
131+
if (this._depth >= 2) {
132+
// Get the final gas item for the nested frame.
133+
var nested = Object.assign(
134+
{},
135+
this._executionGasStack[this._depth + 1] || this._defaultGasItem
136+
);
137+
138+
// Reset the nested gas item to prevent double counting on re-entry.
139+
this._executionGasStack[this._depth + 1] = Object.assign(
140+
{},
141+
this._defaultGasItem
142+
);
143+
144+
// Keep track of the total gas used by all frames at this depth.
145+
// This does not account for the gas required due to the 63/64 rule.
146+
var used = frame.getGasUsed();
147+
this._executionGasStack[this._depth].used += used;
148+
149+
// Keep track of the total gas required by all frames at this depth.
150+
// This accounts for additional gas needed due to the 63/64 rule.
151+
this._executionGasStack[this._depth].required +=
152+
used - nested.used + Math.ceil((nested.required * 64) / 63);
153+
154+
// Keep track of the final gas limit.
155+
this.executionGasLimit = this._executionGasStack[this._depth].required;
156+
}
157+
}
158+
},
159+
160+
step: function step(log, db) {
161+
var opcode = log.op.toString();
162+
this._depth = log.getDepth();
163+
if (this._depth === 1 && opcode === "NUMBER") this._marker++;
164+
165+
if (
166+
this._depth <= 2 &&
167+
opcode.startsWith("LOG") &&
168+
this._isUserOperationEvent(log)
169+
)
170+
this._setUserOperationEvent(opcode, log);
171+
if (
172+
this._depth <= 2 &&
173+
opcode.startsWith("LOG") &&
174+
this._isUserOperationRevertEvent(log)
175+
)
176+
this._setUserOperationRevertEvent(opcode, log);
177+
178+
if (log.getGas() < log.getCost() && this._isValidation())
179+
this.validationOOG = true;
180+
181+
if (log.getGas() < log.getCost() && this._isExecution())
182+
this.executionOOG = true;
183+
},
184+
}
185+
"#;
186+
187+
#[cfg(test)]
188+
mod test {
189+
use serde::{Deserialize, Serialize};
190+
use serde_json::Value;
191+
192+
// Json Test for the `ExecutorTracerResult` struct
193+
#[test]
194+
fn test_json() {
195+
#[derive(Serialize, Deserialize, Debug)]
196+
struct A {
197+
data: Vec<u8>,
198+
}
199+
let data = r#"
200+
{
201+
"data": [0,0,195,0,0]
202+
}"#;
203+
let v: Value = serde_json::from_str(data).unwrap();
204+
println!("{:?}", v);
205+
let a: A = serde_json::from_value(v).unwrap();
206+
println!("{:?}", a);
207+
}
208+
}

crates/contracts/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
pub mod entry_point;
44
mod error;
5+
pub mod executor_tracer;
56
mod gen;
67
pub mod tracer;
78
pub mod utils;
89

910
pub use entry_point::EntryPoint;
10-
pub use error::EntryPointError;
11+
pub use error::{decode_revert_string, EntryPointError};
12+
pub use gen::{
13+
ExecutionResult, FailedOp, UserOperationEventFilter, UserOperationRevertReasonFilter,
14+
};

crates/mempool/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ tokio = { workspace = true }
3535

3636
# misc
3737
bin-layout = "7.1.0"
38+
const-hex = "1.10.0"
3839
enumset = "1.1.3"
3940
eyre = { workspace = true }
4041
page_size = "0.6.0"

0 commit comments

Comments
 (0)