Skip to content

Commit

Permalink
feat: new estimate method
Browse files Browse the repository at this point in the history
  • Loading branch information
zsluedem authored and Vid201 committed Feb 28, 2024
1 parent 7c5dacf commit a266eb2
Show file tree
Hide file tree
Showing 10 changed files with 653 additions and 136 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 42 additions & 3 deletions crates/contracts/src/entry_point.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@ use super::{
},
tracer::JS_TRACER,
};
use crate::{error::decode_revert_error, gen::ExecutionResult};
use crate::{error::decode_revert_error, executor_tracer::EXECUTOR_TRACER, gen::ExecutionResult};
use ethers::{
prelude::{ContractError, Event},
providers::Middleware,
types::{
Address, Bytes, GethDebugTracerType, GethDebugTracingCallOptions, GethDebugTracingOptions,
GethTrace, TransactionRequest, U256,
spoof, transaction::eip2718::TypedTransaction, Address, Bytes, GethDebugTracerType,
GethDebugTracingCallOptions, GethDebugTracingOptions, GethTrace, TransactionRequest, U256,
},
};
use std::sync::Arc;

const UINT96_MAX: u128 = 5192296858534827628530496329220095;

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SimulateValidationResult {
ValidationResult(ValidationResult),
Expand Down Expand Up @@ -133,6 +135,43 @@ impl<M: Middleware + 'static> EntryPoint<M> {
Ok(res)
}

pub async fn simulate_handle_op_trace<U: Into<UserOperation>>(
&self,
uo: U,
) -> Result<GethTrace, EntryPointError> {
let uo = uo.into();
let max_fee_per_gas = uo.max_fee_per_gas;
let call = self.entry_point_api.simulate_handle_op(uo, Address::zero(), Bytes::default());
let mut tx: TypedTransaction = call.tx;
tx.set_from(Address::zero());
tx.set_gas_price(max_fee_per_gas);
tx.set_gas(u64::MAX);
let res = self
.eth_client
.debug_trace_call(
tx,
None,
GethDebugTracingCallOptions {
tracing_options: GethDebugTracingOptions {
disable_storage: None,
disable_stack: None,
enable_memory: None,
enable_return_data: None,
tracer: Some(GethDebugTracerType::JsTracer(EXECUTOR_TRACER.into())),
tracer_config: None,
timeout: None,
},
state_overrides: Some(spoof::balance(Address::zero(), UINT96_MAX.into())),
},
)
.await
.map_err(|e| {
EntryPointError::from_middleware_error::<M>(e).expect_err("trace err is expected")
})?;

Ok(res)
}

pub async fn handle_ops<U: Into<UserOperation>>(
&self,
uos: Vec<U>,
Expand Down
30 changes: 18 additions & 12 deletions crates/contracts/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ pub enum EntryPointError {
#[error("{0}")]
FailedOp(FailedOp),

/// execution reverted
#[error("execution reverted: {0}")]
ExecutionReverted(String),

/// There is no revert when there should be
#[error("{function} should revert")]
NoRevert {
Expand Down Expand Up @@ -129,23 +133,25 @@ impl EntryPointError {
Err(Self::Provider { inner: format!("middleware error: {err:?}") })
}
}

// ethers-rs could not handle `require (true, "reason")` or `revert("test failed")` well in this
// case revert with `require` error would ends up with error event signature `0x08c379a0`
// we need to handle it manually
pub fn decode_revert_string(data: Bytes) -> Option<String> {
let (error_sig, reason) = data.split_at(4);
if error_sig == [0x08, 0xc3, 0x79, 0xa0] {
<String as AbiDecode>::decode(reason).ok()
} else {
None
}
}
pub fn decode_revert_error(data: Bytes) -> Result<EntryPointAPIErrors, EntryPointError> {
let decoded = EntryPointAPIErrors::decode(data.as_ref());
match decoded {
Ok(res) => Ok(res),
Err(e) => {
// ethers-rs could not handle `require (true, "reason")` well in this case
// revert with `require` error would ends up with error event signature `0x08c379a0`
// we need to handle it manually
let (error_sig, reason) = data.split_at(4);
if error_sig == [0x08, 0xc3, 0x79, 0xa0] {
return <String as AbiDecode>::decode(reason)
.map(EntryPointAPIErrors::RevertString)
.map_err(|e| EntryPointError::Decode {
inner: format!("data field can't be deserialized to revert error: {e:?}",),
});
}
if let Some(error_str) = decode_revert_string(data) {
return Ok(EntryPointAPIErrors::RevertString(error_str));
};

Err(EntryPointError::Decode {
inner: format!(
Expand Down
208 changes: 208 additions & 0 deletions crates/contracts/src/executor_tracer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
use ethers::types::GethTrace;
use eyre::format_err;
use serde::Deserialize;

#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct LogInfo {
pub topics: Vec<String>,
pub data: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct ExecutorTracerResult {
pub reverts: Vec<String>,
#[serde(rename = "validationOOG")]
pub validation_oog: bool,
#[serde(rename = "executionOOG")]
pub execution_oog: bool,
#[serde(rename = "executionGasLimit")]
pub execution_gas_limit: u64,
#[serde(rename = "userOperationEvent")]
pub user_op_event: Option<LogInfo>,
#[serde(rename = "userOperationRevertEvent")]
pub user_op_revert_event: Option<LogInfo>,
pub output: String,
pub error: String,
}
impl TryFrom<GethTrace> for ExecutorTracerResult {
type Error = eyre::Error;
fn try_from(val: GethTrace) -> Result<Self, Self::Error> {
match val {
GethTrace::Known(val) => Err(format_err!("Invalid geth trace: {val:?}")),
GethTrace::Unknown(val) => serde_json::from_value(val.clone())
.map_err(|error| format_err!("Failed to parse geth trace: {error}, {val:#}")),
}
}
}
pub const EXECUTOR_TRACER: &str = r#"
{
reverts: [],
validationOOG: false,
executionOOG: false,
executionGasLimit: 0,
_depth: 0,
_executionGasStack: [],
_defaultGasItem: { used: 0, required: 0 },
_marker: 0,
_validationMarker: 1,
_executionMarker: 3,
_userOperationEventTopics0:
"0x49628fd1471006c1482da88028e9ce4dbb080b815c9b0344d39e5a8e6ec1419f",
_userOperationRevertEventTopics0:
"0x1c4fada7374c0a9ee8841fc38afe82932dc0f8e69012e927f061a8bae611a201",
_isValidation: function () {
return (
this._marker >= this._validationMarker &&
this._marker < this._executionMarker
);
},
_isExecution: function () {
return this._marker === this._executionMarker;
},
_isUserOperationEvent: function (log) {
var topics0 = "0x" + log.stack.peek(2).toString(16);
return topics0 === this._userOperationEventTopics0;
},
_setUserOperationEvent: function (opcode, log) {
var count = parseInt(opcode.substring(3));
var ofs = parseInt(log.stack.peek(0).toString());
var len = parseInt(log.stack.peek(1).toString());
var topics = [];
for (var i = 0; i < count; i++) {
topics.push(log.stack.peek(2 + i).toString(16));
}
var data = toHex(log.memory.slice(ofs, ofs + len));
this.userOperationEvent = {
topics: topics,
data: data,
};
},
_isUserOperationRevertEvent: function (log) {
var topics0 = "0x" + log.stack.peek(2).toString(16);
return topics0 === this._userOperationRevertEventTopics0;
},
_setUserOperationRevertEvent: function (opcode, log) {
var count = parseInt(opcode.substring(3));
var ofs = parseInt(log.stack.peek(0).toString());
var len = parseInt(log.stack.peek(1).toString());
var topics = [];
for (var i = 0; i < count; i++) {
topics.push(log.stack.peek(2 + i).toString(16));
}
var data = toHex(log.memory.slice(ofs, ofs + len));
this.userOperationRevertEvent = {
topics: topics,
data: data,
};
},
fault: function fault(log, db) {},
result: function result(ctx, db) {
return {
reverts: this.reverts,
validationOOG: this.validationOOG,
executionOOG: this.executionOOG,
executionGasLimit: this.executionGasLimit,
userOperationEvent: this.userOperationEvent,
userOperationRevertEvent: this.userOperationRevertEvent,
output: toHex(ctx.output),
error: ctx.error,
};
},
enter: function enter(frame) {
if (this._isExecution()) {
var next = this._depth + 1;
if (this._executionGasStack[next] === undefined)
this._executionGasStack[next] = Object.assign({}, this._defaultGasItem);
}
},
exit: function exit(frame) {
if (this._isExecution()) {
if (frame.getError() !== undefined) {
this.reverts.push(toHex(frame.getOutput()));
}
if (this._depth >= 2) {
// Get the final gas item for the nested frame.
var nested = Object.assign(
{},
this._executionGasStack[this._depth + 1] || this._defaultGasItem
);
// Reset the nested gas item to prevent double counting on re-entry.
this._executionGasStack[this._depth + 1] = Object.assign(
{},
this._defaultGasItem
);
// Keep track of the total gas used by all frames at this depth.
// This does not account for the gas required due to the 63/64 rule.
var used = frame.getGasUsed();
this._executionGasStack[this._depth].used += used;
// Keep track of the total gas required by all frames at this depth.
// This accounts for additional gas needed due to the 63/64 rule.
this._executionGasStack[this._depth].required +=
used - nested.used + Math.ceil((nested.required * 64) / 63);
// Keep track of the final gas limit.
this.executionGasLimit = this._executionGasStack[this._depth].required;
}
}
},
step: function step(log, db) {
var opcode = log.op.toString();
this._depth = log.getDepth();
if (this._depth === 1 && opcode === "NUMBER") this._marker++;
if (
this._depth <= 2 &&
opcode.startsWith("LOG") &&
this._isUserOperationEvent(log)
)
this._setUserOperationEvent(opcode, log);
if (
this._depth <= 2 &&
opcode.startsWith("LOG") &&
this._isUserOperationRevertEvent(log)
)
this._setUserOperationRevertEvent(opcode, log);
if (log.getGas() < log.getCost() && this._isValidation())
this.validationOOG = true;
if (log.getGas() < log.getCost() && this._isExecution())
this.executionOOG = true;
},
}
"#;

#[cfg(test)]
mod test {
use serde::{Deserialize, Serialize};
use serde_json::Value;

// Json Test for the `ExecutorTracerResult` struct
#[test]
fn test_json() {
#[derive(Serialize, Deserialize, Debug)]
struct A {
data: Vec<u8>,
}
let data = r#"
{
"data": [0,0,195,0,0]
}"#;
let v: Value = serde_json::from_str(data).unwrap();
println!("{:?}", v);
let a: A = serde_json::from_value(v).unwrap();
println!("{:?}", a);
}
}
6 changes: 5 additions & 1 deletion crates/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

pub mod entry_point;
mod error;
pub mod executor_tracer;
mod gen;
pub mod tracer;
pub mod utils;

pub use entry_point::EntryPoint;
pub use error::EntryPointError;
pub use error::{decode_revert_string, EntryPointError};
pub use gen::{
ExecutionResult, FailedOp, UserOperationEventFilter, UserOperationRevertReasonFilter,
};
1 change: 1 addition & 0 deletions crates/mempool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ tokio = { workspace = true }

# misc
bin-layout = "7.1.0"
const-hex = "1.10.0"
enumset = "1.1.3"
eyre = { workspace = true }
page_size = "0.6.0"
Expand Down
Loading

0 comments on commit a266eb2

Please sign in to comment.