Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new estimate method #289

Merged
merged 1 commit into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
}
Vid201 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading