diff --git a/Cargo.lock b/Cargo.lock index 3ecd937d..3bf703fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7007,6 +7007,7 @@ dependencies = [ "alloy-chains", "async-trait", "bin-layout", + "const-hex", "enumset", "ethers 2.0.8", "eyre", diff --git a/crates/contracts/src/entry_point.rs b/crates/contracts/src/entry_point.rs index 8146ea9b..c845fcc8 100644 --- a/crates/contracts/src/entry_point.rs +++ b/crates/contracts/src/entry_point.rs @@ -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), @@ -133,6 +135,43 @@ impl EntryPoint { Ok(res) } + pub async fn simulate_handle_op_trace>( + &self, + uo: U, + ) -> Result { + 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::(e).expect_err("trace err is expected") + })?; + + Ok(res) + } + pub async fn handle_ops>( &self, uos: Vec, diff --git a/crates/contracts/src/error.rs b/crates/contracts/src/error.rs index c93b4cb4..96e9e85e 100644 --- a/crates/contracts/src/error.rs +++ b/crates/contracts/src/error.rs @@ -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 { @@ -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 { + let (error_sig, reason) = data.split_at(4); + if error_sig == [0x08, 0xc3, 0x79, 0xa0] { + ::decode(reason).ok() + } else { + None + } +} pub fn decode_revert_error(data: Bytes) -> Result { 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 ::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!( diff --git a/crates/contracts/src/executor_tracer.rs b/crates/contracts/src/executor_tracer.rs new file mode 100644 index 00000000..aeafd819 --- /dev/null +++ b/crates/contracts/src/executor_tracer.rs @@ -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, + pub data: String, +} +#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)] +pub struct ExecutorTracerResult { + pub reverts: Vec, + #[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, + #[serde(rename = "userOperationRevertEvent")] + pub user_op_revert_event: Option, + pub output: String, + pub error: String, +} +impl TryFrom for ExecutorTracerResult { + type Error = eyre::Error; + fn try_from(val: GethTrace) -> Result { + 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, + } + 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); + } +} diff --git a/crates/contracts/src/lib.rs b/crates/contracts/src/lib.rs index 84b5583e..a6a1ecb1 100644 --- a/crates/contracts/src/lib.rs +++ b/crates/contracts/src/lib.rs @@ -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, +}; diff --git a/crates/mempool/Cargo.toml b/crates/mempool/Cargo.toml index 613ca14c..1a398b6b 100644 --- a/crates/mempool/Cargo.toml +++ b/crates/mempool/Cargo.toml @@ -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" diff --git a/crates/mempool/src/estimate.rs b/crates/mempool/src/estimate.rs new file mode 100644 index 00000000..82ac013b --- /dev/null +++ b/crates/mempool/src/estimate.rs @@ -0,0 +1,272 @@ +use const_hex::hex; +use core::fmt::Debug; +use ethers::{ + abi::{Hash, RawLog}, + contract::EthLogDecode, + providers::Middleware, + types::{Bytes, U256}, +}; +use silius_contracts::{ + decode_revert_string, + executor_tracer::{ExecutorTracerResult, LogInfo}, + EntryPoint, EntryPointError, ExecutionResult, FailedOp, UserOperationEventFilter, + UserOperationRevertReasonFilter, +}; +use silius_primitives::UserOperationSigned; +use std::str::FromStr; + +const FALL_BACK_BINARY_SEARCH_CUT_OFF: u64 = 30000; +const BASE_VGL_BUFFER: u64 = 25; +const MAX_CALL_GAS_LIMIT: u64 = 18_000_000; +const MAX_RETRY: u64 = 7; +const NON_ZERO_GAS: u64 = 12100; // should be different based on diferrent chain +const EXECUTION_REVERTED: &str = "execution reverted"; +const EXECUTION_OOG: &str = "execution OOG"; + +fn is_prefund_not_paid(err: T) -> bool { + let s = err.to_string(); + s.contains("AA21 didn't pay prefund") || + s.contains("AA31 paymaster deposit too low") || + s.contains("AA95 out of gas") +} + +fn is_validation_oog(err: T) -> bool { + let s = err.to_string(); + s.contains("validation OOG") || + s.contains("return data out of bounds") || + s.contains("AA40 over verificationGasLimit") || + s.contains("AA41 too little verificationGas") || + s.contains("AA51 prefund below actualGasCost") || + s.contains("AA13 initCode failed or OOG") || + s.contains("AA23 reverted (or OOG)") || + s.contains("AA33 reverted (or OOG") +} + +fn is_execution_oog(err: T) -> bool { + err.to_string().contains(EXECUTION_OOG) +} +fn is_execution_revert(err: T) -> bool { + err.to_string().contains(EXECUTION_REVERTED) +} + +#[derive(Debug, Default)] +struct TraceOutput { + tracer_result: ExecutorTracerResult, + execution_result: ExecutionResult, + user_op_event: UserOperationEventFilter, + user_op_revert_event: Option, +} + +fn parse_simulate_handle_op_output(output: &str) -> Result { + let output_b = Bytes::from_str(output).map_err(|e| EntryPointError::Other { + inner: format!("parse simulate handle op output failed: {e:?}"), + })?; + if let Ok(decoded) = + ::decode(output_b.as_ref()) + { + return Ok(decoded); + }; + + if let Ok(decoded) = ::decode(output_b.as_ref()) { + return Err(EntryPointError::FailedOp(decoded)); + }; + + Err(EntryPointError::Other { + inner: "output of parse simulate handle op is not valid".to_string(), + }) +} + +fn parse_user_op_event(event: &LogInfo) -> Result { + let topics = event + .topics + .iter() + .map(|t| { + let mut hash_str = t.clone(); + if hash_str.len() % 2 != 0 { + hash_str.insert(0, '0'); + }; + hex::decode(hash_str).map(|mut b| { + b.resize(32, 0); + Hash::from_slice(b.as_ref()) + }) + }) + .collect::, _>>() + .map_err(|e| EntryPointError::Other { + inner: format!( + "simulate handle user op failed on parsing user op event topic hash, {e:?}" + ), + })?; + let data = Bytes::from_str(event.data.as_str()).map_err(|e| EntryPointError::Other { + inner: format!("simulate handle user op failed on parsing user op event data: {e:?}"), + })?; + let log = RawLog::from((topics, data.to_vec())); + ::decode_log(&log).map_err(|err| EntryPointError::Other { + inner: format!("simulate handle user op failed on parsing user op event: {err:?}"), + }) +} + +async fn trace_simulate_handle_op( + user_op: &UserOperationSigned, + entry_point: &EntryPoint, +) -> Result { + let geth_trace = entry_point.simulate_handle_op_trace(user_op.clone()).await?; + + let tracer_result: ExecutorTracerResult = + ExecutorTracerResult::try_from(geth_trace).map_err(|e| EntryPointError::Other { + inner: format!("Estimate trace simulate handle op decode error {e:?}"), + })?; + + let execution_result = parse_simulate_handle_op_output(tracer_result.output.as_str())?; + + let user_op_event = tracer_result.user_op_event.as_ref().ok_or(EntryPointError::Other { + inner: "Estimate trace simulate handle op user op event not found".to_string(), + })?; + let user_op_event = parse_user_op_event::(user_op_event)?; + let user_op_revert_event = tracer_result + .user_op_revert_event + .as_ref() + .and_then(|e| parse_user_op_event::(e).ok()); + + if !user_op_event.success && !tracer_result.reverts.is_empty() { + let revert_data = tracer_result.reverts[tracer_result.reverts.len() - 1].clone(); + if revert_data.is_empty() && user_op_revert_event.is_none() && tracer_result.execution_oog { + return Err(EntryPointError::Other { inner: EXECUTION_OOG.to_string() }); + } + if let Some(revert_event) = &user_op_revert_event { + if let Some(error_str) = decode_revert_string(revert_event.revert_reason.clone()) { + return Err(EntryPointError::ExecutionReverted(format!( + "User op execution revert with {error_str:?}, {revert_event:?}", + ))); + }; + } + return Err(EntryPointError::ExecutionReverted(format!( + "{:?} , {:?} , {:?}, {:?}", + tracer_result.error, execution_result, user_op_event, user_op_revert_event + ))); + } + + Ok(TraceOutput { tracer_result, execution_result, user_op_event, user_op_revert_event }) +} + +pub async fn estimate_user_op_gas( + user_op_ori: &UserOperationSigned, + entry_point: &EntryPoint, +) -> Result<(U256, U256), EntryPointError> { + let mut iteration: u64 = 0; + + let mut user_op = user_op_ori.clone(); + user_op.verification_gas_limit = 0.into(); + user_op.call_gas_limit = 0.into(); + user_op.max_priority_fee_per_gas = user_op_ori.max_fee_per_gas; + + // Binary search + let mut l: u64 = 0; + let mut r: u64 = u64::MAX; + let mut f: u64 = 0; + + while r - l >= FALL_BACK_BINARY_SEARCH_CUT_OFF { + let m = (l + r) / 2; + user_op.verification_gas_limit = m.into(); + match entry_point.simulate_handle_op(user_op.clone()).await { + // VGL too high + Ok(_) => { + r = m - 1; + f = m; + continue; + } + Err(e) => { + if is_prefund_not_paid(&e) { + r = m - 1; + continue; + } else if is_validation_oog(&e) { + l = m + 1; + continue; + } else { + return Err(e); + } + } + } + } + if f == 0 { + return Err(EntryPointError::Other { + inner: "Could not find a valid verification gas limit".to_string(), + }); + } + let out: TraceOutput; + let mut res: Result<(U256, U256), EntryPointError> = Ok((0u64.into(), 0u64.into())); + loop { + if iteration >= MAX_RETRY { + return res; + } + f = (f * (100 + BASE_VGL_BUFFER)) / 100; + user_op.verification_gas_limit = f.into(); + user_op.max_fee_per_gas = 0u64.into(); + user_op.max_priority_fee_per_gas = 0u64.into(); + user_op.call_gas_limit = MAX_CALL_GAS_LIMIT.into(); // max block gas limit, better set as a config parameter + match trace_simulate_handle_op(&user_op, entry_point).await { + Ok(o) => { + out = o; + break; + } + Err(e) => { + iteration += 1; + res = Err(e); + continue; + } + } + } + let verification_gas_limit = user_op.verification_gas_limit; + let mut call_gas_limit = if out.tracer_result.execution_gas_limit < NON_ZERO_GAS { + NON_ZERO_GAS + } else { + out.tracer_result.execution_gas_limit + }; + + user_op.max_priority_fee_per_gas = user_op_ori.max_priority_fee_per_gas; + user_op.max_fee_per_gas = user_op_ori.max_fee_per_gas; + user_op.verification_gas_limit = verification_gas_limit; + user_op.call_gas_limit = call_gas_limit.into(); + loop { + match trace_simulate_handle_op(&user_op, entry_point).await { + Ok(_) => break, + Err(e) => { + if is_execution_oog(&e) || is_execution_revert(&e) { + let mut l = call_gas_limit; + let mut r = u64::MAX; + let mut f = 0u64; + while r - l >= FALL_BACK_BINARY_SEARCH_CUT_OFF { + let m = (l + r) / 2; + user_op.call_gas_limit = m.into(); + let res = trace_simulate_handle_op(&user_op, entry_point).await; + match res { + Ok(_) => { + r = m - 1; + f = m; + continue; + } + Err(err) => { + if is_prefund_not_paid(&err) { + r = m - 1; + continue; + } else if is_execution_oog(&err) || is_execution_revert(&err) { + l = m + 1; + continue; + } else { + return Err(EntryPointError::Other { + inner: "Could not find a valid call gas limit".to_string(), + }); + } + } + } + } + call_gas_limit = f; + } else { + return Err(EntryPointError::Other { + inner: format!("Trace handle op return unhandled error: {:?}", &e), + }); + } + } + } + } + Ok((verification_gas_limit, call_gas_limit.into())) +} diff --git a/crates/mempool/src/lib.rs b/crates/mempool/src/lib.rs index 5b92b09b..c79a1757 100644 --- a/crates/mempool/src/lib.rs +++ b/crates/mempool/src/lib.rs @@ -5,6 +5,7 @@ mod builder; #[cfg(feature = "mdbx")] mod database; pub mod error; +mod estimate; mod memory; mod mempool; pub mod metrics; diff --git a/crates/mempool/src/uopool.rs b/crates/mempool/src/uopool.rs index 16ae3582..8a849ad5 100644 --- a/crates/mempool/src/uopool.rs +++ b/crates/mempool/src/uopool.rs @@ -1,8 +1,8 @@ use crate::{ + estimate::estimate_user_op_gas, mempool::{Mempool, UserOperationAct, UserOperationAddrAct, UserOperationCodeHashAct}, mempool_id, reputation::{HashSetOp, ReputationEntryOp}, - utils::calculate_call_gas_limit, validate::{ UserOperationValidationOutcome, UserOperationValidator, UserOperationValidatorMode, }, @@ -19,6 +19,7 @@ use eyre::format_err; use futures::channel::mpsc::UnboundedSender; use silius_contracts::{ entry_point::UserOperationEventFilter, utils::parse_from_input_data, EntryPoint, + EntryPointError, }; use silius_primitives::{ constants::validation::reputation::THROTTLED_ENTITY_BUNDLE_COUNT, @@ -31,6 +32,7 @@ use std::collections::{HashMap, HashSet}; use tracing::{debug, error, info, trace}; const FILTER_MAX_DEPTH: u64 = 10; +const PRE_VERIFICATION_SAFE_RESERVE: u64 = 1_000; /// The alternative mempool pool implementation that provides functionalities to add, remove, /// validate, and serves data requests from the [RPC API](EthApiServer). Architecturally, the @@ -465,47 +467,40 @@ where &self, uo: &UserOperation, ) -> Result { - let val_out = self - .validator - .validate_user_operation( - uo, - &self.mempool, - &self.reputation, - UserOperationValidatorMode::SimulationTrace.into(), - ) - .await - .map_err(|err| MempoolError { hash: uo.hash, kind: err.into() })?; - - match self.entry_point.simulate_execution(uo.user_operation.clone()).await { - Ok(_) => {} - Err(err) => { - return Err(MempoolError { - hash: uo.hash, - kind: SimulationError::Execution { inner: err.to_string() }.into(), - }) - } - } - - let exec_res = match self.entry_point.simulate_handle_op(uo.user_operation.clone()).await { - Ok(res) => res, - Err(err) => { - return Err(MempoolError { hash: uo.hash, kind: SimulationError::from(err).into() }) - } - }; - - let base_fee_per_gas = self.base_fee_per_gas().await.map_err(|err| MempoolError { - hash: uo.hash, - kind: MempoolErrorKind::Provider { inner: err.to_string() }, - })?; - let call_gas_limit = calculate_call_gas_limit( - exec_res.paid, - exec_res.pre_op_gas, - uo.max_fee_per_gas.min(uo.max_priority_fee_per_gas + base_fee_per_gas), - ); + let (verification_gas_limit, call_gas_limit) = + estimate_user_op_gas(&uo.user_operation, &self.entry_point).await.map_err( + |e| match e { + EntryPointError::FailedOp(f) => MempoolError { + hash: uo.hash, + kind: MempoolErrorKind::InvalidUserOperation( + InvalidMempoolUserOperationError::Simulation( + SimulationError::Validation { inner: format!("{f:?}") }, + ), + ), + }, + EntryPointError::ExecutionReverted(e) => MempoolError { + hash: uo.hash, + kind: MempoolErrorKind::InvalidUserOperation( + InvalidMempoolUserOperationError::Simulation( + SimulationError::Execution { inner: e }, + ), + ), + }, + EntryPointError::Provider { inner } => { + MempoolError { hash: uo.hash, kind: MempoolErrorKind::Provider { inner } } + } + _ => MempoolError { + hash: uo.hash, + kind: MempoolErrorKind::Other { inner: format!("{e:?}") }, + }, + }, + )?; Ok(UserOperationGasEstimation { - pre_verification_gas: Overhead::default().calculate_pre_verification_gas(uo), - verification_gas_limit: val_out.verification_gas_limit, + pre_verification_gas: Overhead::default() + .calculate_pre_verification_gas(uo) + .saturating_add(PRE_VERIFICATION_SAFE_RESERVE.into()), + verification_gas_limit, call_gas_limit, }) } diff --git a/tests/src/estimate_gas_tests.rs b/tests/src/estimate_gas_tests.rs index 0e5e2561..919aa7d0 100644 --- a/tests/src/estimate_gas_tests.rs +++ b/tests/src/estimate_gas_tests.rs @@ -1,8 +1,7 @@ use crate::common::{ deploy_entry_point, deploy_simple_account_factory, gen::{EntryPointContract, SimpleAccountFactory}, - setup_database_mempool_reputation, setup_geth, setup_memory_mempool_reputation, ClientType, - DeployedContract, SEED_PHRASE, + setup_geth, setup_memory_mempool_reputation, ClientType, DeployedContract, SEED_PHRASE, }; use alloy_chains::Chain; use ethers::{ @@ -32,89 +31,80 @@ async fn setup_basic() -> eyre::Result<( Ok((client.clone(), ep, chain_id, geth, simple_account_factory)) } -macro_rules! estimate_gas_with_init_code { - ($setup:expr, $name: ident) => { - #[tokio::test] - async fn $name() -> eyre::Result<()> { - let (client, entry_point, chain_id, _geth, simple_account_factory) = - setup_basic().await?; - let (mempool, reputation) = $setup; - let max_verification_gas = 5000000.into(); - let chain = Chain::from_id(chain_id); - let entry = EntryPoint::new(client.clone(), entry_point.address); - let entry_for_uopool = EntryPoint::new(client.clone(), entry_point.address); - let min_priority_fee_per_gas = 0.into(); - let validator = - new_canonical(entry, chain, max_verification_gas, min_priority_fee_per_gas); - let uopool = UoPool::new( - entry_for_uopool, - validator, - mempool, - reputation, - max_verification_gas, - chain, - None, - ); +#[tokio::test] +async fn estimate_with_zero() -> eyre::Result<()> { + let (client, entry_point, chain_id, _geth, simple_account_factory) = setup_basic().await?; + let (mempool, reputation) = setup_memory_mempool_reputation(); + let max_verification_gas = 5000000.into(); + let chain = Chain::from_id(chain_id); + let entry = EntryPoint::new(client.clone(), entry_point.address); + let entry_for_uopool = EntryPoint::new(client.clone(), entry_point.address); + let min_priority_fee_per_gas = 0.into(); + let validator = new_canonical(entry, chain, max_verification_gas, min_priority_fee_per_gas); + let mut uopool = UoPool::new( + entry_for_uopool, + validator, + mempool, + reputation, + max_verification_gas, + chain, + None, + ); - let wallet = MnemonicBuilder::::default().phrase(SEED_PHRASE).build()?; - let owner_address = wallet.address(); - let address: H160 = simple_account_factory - .contract() - .get_address(owner_address, U256::from(1)) - .call() - .await?; - let nonce = client.get_transaction_count(owner_address, None).await?; - let mut initial_fund = TypedTransaction::default(); - initial_fund - .set_from(owner_address) - .set_to(address) - .set_value(1000000000000000u64) - .set_nonce(nonce); - let _receipt = client.send_transaction(initial_fund, None).await?.await?; - let _balance = client.get_balance(address, None).await?; + let wallet = MnemonicBuilder::::default().phrase(SEED_PHRASE).build()?; + let owner_address = wallet.address(); + let address: H160 = + simple_account_factory.contract().get_address(owner_address, U256::from(1)).call().await?; + let nonce = client.get_transaction_count(owner_address, None).await?; + let mut initial_fund = TypedTransaction::default(); + initial_fund.set_from(owner_address).set_to(address).set_value(u64::MAX).set_nonce(nonce); + let _receipt = client.send_transaction(initial_fund, None).await?.await?; + let balance = client.get_balance(address, None).await?; + assert_eq!(U256::from(u64::MAX), balance); - let call = - simple_account_factory.contract().create_account(owner_address, U256::from(1)); - let tx: TypedTransaction = call.tx; - let mut init_code = Vec::new(); - init_code.extend_from_slice(simple_account_factory.address.as_bytes()); - init_code.extend_from_slice(tx.data().unwrap().to_vec().as_slice()); + let call = simple_account_factory.contract().create_account(owner_address, U256::from(1)); + let tx: TypedTransaction = call.tx; + let mut init_code = Vec::new(); + init_code.extend_from_slice(simple_account_factory.address.as_bytes()); + init_code.extend_from_slice(tx.data().unwrap().to_vec().as_slice()); - // This is the `execute(address dest, uint256 value, bytes calldata func)` call data - // with all empty values. - let call_data: Vec = vec![ - 182, 29, 39, 246, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ]; + // This is the `execute(address dest, uint256 value, bytes calldata func)` call data + // with all empty values. + let call_data: Vec = vec![ + 182, 29, 39, 246, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; - let (_gas_price, priority_fee) = client.estimate_eip1559_fees(None).await?; - let nonce = client.get_transaction_count(address, None).await?; - let user_op = UserOperationSigned { - sender: address, - nonce, - init_code: Bytes::from(init_code), - call_data: Bytes::from(call_data), - call_gas_limit: U256::from(1), - verification_gas_limit: U256::from(1000000u64), - pre_verification_gas: U256::from(1), - max_fee_per_gas: U256::from(1), - max_priority_fee_per_gas: priority_fee, - paymaster_and_data: Bytes::new(), - signature: Bytes::default(), - }; + let (gas_price, priority_fee) = client.estimate_eip1559_fees(None).await?; + let nonce = client.get_transaction_count(address, None).await?; + let user_op = UserOperationSigned { + sender: address, + nonce, + init_code: Bytes::from(init_code), + call_data: Bytes::from(call_data), + call_gas_limit: U256::from(1), + verification_gas_limit: U256::from(1000000u64), + pre_verification_gas: U256::from(1), + max_fee_per_gas: gas_price, + max_priority_fee_per_gas: priority_fee, + paymaster_and_data: Bytes::new(), + signature: Bytes::default(), + }; - let uo_wallet = UoWallet::from_phrase(SEED_PHRASE, chain_id, false)?; - let user_op = - uo_wallet.sign_user_operation(&user_op, &entry_point.address, chain_id).await?; + let uo_wallet = UoWallet::from_phrase(SEED_PHRASE, chain_id, false)?; + let user_op = uo_wallet.sign_user_operation(&user_op, &entry_point.address, chain_id).await?; - let _ = uopool.estimate_user_operation_gas(&user_op).await.expect("estimate done"); - Ok(()) - } + let estimate = uopool.estimate_user_operation_gas(&user_op).await.expect("estimate done"); + let user_op = UserOperationSigned { + verification_gas_limit: estimate.verification_gas_limit, + call_gas_limit: estimate.call_gas_limit, + pre_verification_gas: estimate.pre_verification_gas, + ..user_op.user_operation }; + let user_op = uo_wallet.sign_user_operation(&user_op, &entry_point.address, chain_id).await?; + uopool.add_user_operations(vec![user_op]).await.expect("handle done"); + Ok(()) } - -estimate_gas_with_init_code!(setup_database_mempool_reputation(), estimate_gas_init_code_datbase); -estimate_gas_with_init_code!(setup_memory_mempool_reputation(), estimate_gas_init_code_memory);