From 55c6ced1fba919e67ced1c6900c2a017742d7712 Mon Sep 17 00:00:00 2001 From: Dan Coombs Date: Fri, 29 Mar 2024 16:55:23 -0500 Subject: [PATCH] feat: end to end entry point routing (#649) --- bin/rundler/src/cli/builder.rs | 37 ++- bin/rundler/src/cli/mod.rs | 34 ++- bin/rundler/src/cli/pool.rs | 47 ++- bin/rundler/src/cli/rpc.rs | 2 + crates/builder/src/task.rs | 170 +++++++---- crates/pool/proto/op_pool/op_pool.proto | 61 +++- crates/pool/src/server/remote/client.rs | 8 +- crates/pool/src/server/remote/error.rs | 172 +++++++---- crates/pool/src/server/remote/protos.rs | 113 ++++++-- crates/pool/src/task.rs | 141 +++++---- .../provider/src/ethers/entry_point/v0_6.rs | 19 +- .../provider/src/ethers/entry_point/v0_7.rs | 67 ++++- crates/provider/src/lib.rs | 6 +- crates/provider/src/traits/entry_point.rs | 6 +- crates/provider/src/traits/mod.rs | 5 +- crates/provider/src/traits/test_utils.rs | 4 +- crates/rpc/src/eth/api.rs | 2 +- crates/rpc/src/eth/error.rs | 43 ++- crates/rpc/src/eth/events/common.rs | 263 +++++++++++++++++ crates/rpc/src/eth/events/mod.rs | 3 + crates/rpc/src/eth/events/v0_6.rs | 273 +++--------------- crates/rpc/src/eth/events/v0_7.rs | 106 ++++++- crates/rpc/src/eth/mod.rs | 2 +- crates/rpc/src/eth/router.rs | 5 +- crates/rpc/src/eth/server.rs | 11 +- crates/rpc/src/rundler.rs | 6 +- crates/rpc/src/task.rs | 69 ++++- crates/rpc/src/types/mod.rs | 19 +- crates/rpc/src/types/v0_6.rs | 6 +- crates/rpc/src/types/v0_7.rs | 79 ++++- crates/sim/src/estimation/v0_7.rs | 87 +++++- crates/sim/src/gas/gas.rs | 26 +- crates/sim/src/simulation/mempool.rs | 15 + crates/sim/src/simulation/mod.rs | 3 + .../src/simulation/{v0_6 => }/unsafe_sim.rs | 55 ++-- crates/sim/src/simulation/v0_6/mod.rs | 3 - crates/task/src/grpc/protos.rs | 57 +++- crates/types/Cargo.toml | 2 +- crates/types/src/chain.rs | 10 +- crates/types/src/lib.rs | 3 +- crates/types/src/pool/error.rs | 7 +- crates/types/src/user_operation/v0_7.rs | 222 ++++++++++---- crates/types/src/validation_results.rs | 25 ++ test/spec-tests/local/.env | 1 + test/spec-tests/remote/docker-compose.yml | 3 + 45 files changed, 1666 insertions(+), 632 deletions(-) create mode 100644 crates/rpc/src/eth/events/common.rs rename crates/sim/src/simulation/{v0_6 => }/unsafe_sim.rs (77%) diff --git a/bin/rundler/src/cli/builder.rs b/bin/rundler/src/cli/builder.rs index f103bb7b1..a8c4f7377 100644 --- a/bin/rundler/src/cli/builder.rs +++ b/bin/rundler/src/cli/builder.rs @@ -191,7 +191,6 @@ impl BuilderArgs { .context("should have a node HTTP URL")?; let submit_url = self.submit_url.clone().unwrap_or_else(|| rpc_url.clone()); - // TODO these should be scoped by entry point let mempool_configs = match &common.mempool_config_path { Some(path) => { get_json_config::>(path, &common.aws_region).await? @@ -199,15 +198,37 @@ impl BuilderArgs { None => HashMap::from([(H256::zero(), MempoolConfig::default())]), }; - Ok(BuilderTaskArgs { - // TODO: support multiple entry points - entry_points: vec![EntryPointBuilderSettings { - address: chain_spec.entry_point_address, + let mut entry_points = vec![]; + + if common.entry_point_v0_6_enabled { + entry_points.push(EntryPointBuilderSettings { + address: chain_spec.entry_point_address_v0_6, version: EntryPointVersion::V0_6, - num_bundle_builders: common.num_builders, + num_bundle_builders: common.num_builders_v0_6, + bundle_builder_index_offset: self.builder_index_offset, + mempool_configs: mempool_configs + .iter() + .filter(|(_, v)| v.entry_point() == chain_spec.entry_point_address_v0_6) + .map(|(k, v)| (*k, v.clone())) + .collect(), + }); + } + if common.entry_point_v0_7_enabled { + entry_points.push(EntryPointBuilderSettings { + address: chain_spec.entry_point_address_v0_7, + version: EntryPointVersion::V0_7, + num_bundle_builders: common.num_builders_v0_7, bundle_builder_index_offset: self.builder_index_offset, - mempool_configs, - }], + mempool_configs: mempool_configs + .iter() + .filter(|(_, v)| v.entry_point() == chain_spec.entry_point_address_v0_7) + .map(|(k, v)| (*k, v.clone())) + .collect(), + }); + } + + Ok(BuilderTaskArgs { + entry_points, chain_spec, unsafe_mode: common.unsafe_mode, rpc_url, diff --git a/bin/rundler/src/cli/mod.rs b/bin/rundler/src/cli/mod.rs index 6085a5f5c..6569093c5 100644 --- a/bin/rundler/src/cli/mod.rs +++ b/bin/rundler/src/cli/mod.rs @@ -258,12 +258,38 @@ pub struct CommonArgs { pub mempool_config_path: Option, #[arg( - long = "num_builders", - name = "num_builders", - env = "NUM_BUILDERS", + long = "entry_point_v0_6_enabled", + name = "entry_point_v0_6_enabled", + env = "ENTRY_POINT_V0_6_ENABLED", + default_value = "true" + )] + pub entry_point_v0_6_enabled: bool, + + // Ignored if entry_point_v0_6_enabled is false + #[arg( + long = "num_builders_v0_6", + name = "num_builders_v0_6", + env = "NUM_BUILDERS_V0_6", + default_value = "1" + )] + pub num_builders_v0_6: u64, + + #[arg( + long = "entry_point_v0_7_enabled", + name = "entry_point_v0_7_enabled", + env = "ENTRY_POINT_V0_7_ENABLED", + default_value = "true" + )] + pub entry_point_v0_7_enabled: bool, + + // Ignored if entry_point_v0_7_enabled is false + #[arg( + long = "num_builders_v0_7", + name = "num_builders_v0_7", + env = "NUM_BUILDERS_V0_7", default_value = "1" )] - pub num_builders: u64, + pub num_builders_v0_7: u64, } const SIMULATION_GAS_OVERHEAD: u64 = 100_000; diff --git a/bin/rundler/src/cli/pool.rs b/bin/rundler/src/cli/pool.rs index 5e4169690..40ec31f34 100644 --- a/bin/rundler/src/cli/pool.rs +++ b/bin/rundler/src/cli/pool.rs @@ -15,7 +15,7 @@ use std::{collections::HashMap, net::SocketAddr, time::Duration}; use anyhow::Context; use clap::Args; -use ethers::types::H256; +use ethers::types::{Address, H256}; use rundler_pool::{LocalPoolBuilder, PoolConfig, PoolTask, PoolTaskArgs}; use rundler_sim::MempoolConfig; use rundler_task::spawn_tasks_with_shutdown; @@ -181,13 +181,14 @@ impl PoolArgs { tracing::info!("Mempool channel configs: {:?}", mempool_channel_configs); let chain_id = chain_spec.id; - // TODO(danc): multiple pool configs - let pool_config = PoolConfig { - entry_point: chain_spec.entry_point_address, - entry_point_version: EntryPointVersion::V0_6, + let pool_config_base = PoolConfig { + // update per entry point + entry_point: Address::default(), + entry_point_version: EntryPointVersion::Unspecified, + num_shards: 0, + mempool_channel_configs: HashMap::new(), + // Base config chain_id, - // Currently use the same shard count as the number of builders - num_shards: common.num_builders, same_sender_mempool_count: self.same_sender_mempool_count, min_replacement_fee_increase_percentage: self.min_replacement_fee_increase_percentage, max_size_of_pool_bytes: self.max_size_in_bytes, @@ -195,7 +196,6 @@ impl PoolArgs { allowlist: allowlist.clone(), precheck_settings: common.try_into()?, sim_settings: common.into(), - mempool_channel_configs: mempool_channel_configs.clone(), throttled_entity_mempool_count: self.throttled_entity_mempool_count, throttled_entity_live_blocks: self.throttled_entity_live_blocks, paymaster_tracking_enabled: self.paymaster_tracking_enabled, @@ -204,6 +204,35 @@ impl PoolArgs { drop_min_num_blocks: self.drop_min_num_blocks, }; + let mut pool_configs = vec![]; + + if common.entry_point_v0_6_enabled { + pool_configs.push(PoolConfig { + entry_point: chain_spec.entry_point_address_v0_6, + entry_point_version: EntryPointVersion::V0_6, + num_shards: common.num_builders_v0_6, + mempool_channel_configs: mempool_channel_configs + .iter() + .filter(|(_, v)| v.entry_point() == chain_spec.entry_point_address_v0_6) + .map(|(k, v)| (*k, v.clone())) + .collect(), + ..pool_config_base.clone() + }); + } + if common.entry_point_v0_7_enabled { + pool_configs.push(PoolConfig { + entry_point: chain_spec.entry_point_address_v0_7, + entry_point_version: EntryPointVersion::V0_7, + num_shards: common.num_builders_v0_7, + mempool_channel_configs: mempool_channel_configs + .iter() + .filter(|(_, v)| v.entry_point() == chain_spec.entry_point_address_v0_7) + .map(|(k, v)| (*k, v.clone())) + .collect(), + ..pool_config_base.clone() + }); + } + Ok(PoolTaskArgs { chain_spec, unsafe_mode: common.unsafe_mode, @@ -212,7 +241,7 @@ impl PoolArgs { .clone() .context("pool requires node_http arg")?, http_poll_interval: Duration::from_millis(common.eth_poll_interval_millis), - pool_configs: vec![pool_config], + pool_configs, remote_address, chain_update_channel_capacity: self.chain_update_channel_capacity.unwrap_or(1024), }) diff --git a/bin/rundler/src/cli/rpc.rs b/bin/rundler/src/cli/rpc.rs index 64d5da482..65d994855 100644 --- a/bin/rundler/src/cli/rpc.rs +++ b/bin/rundler/src/cli/rpc.rs @@ -111,6 +111,8 @@ impl RpcArgs { estimation_settings, rpc_timeout: Duration::from_secs(self.timeout_seconds.parse()?), max_connections: self.max_connections, + entry_point_v0_6_enabled: common.entry_point_v0_6_enabled, + entry_point_v0_7_enabled: common.entry_point_v0_7_enabled, }) } } diff --git a/crates/builder/src/task.rs b/crates/builder/src/task.rs index 4c2853249..26c9edce6 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -22,17 +22,21 @@ use ethers::{ use ethers_signers::Signer; use futures::future; use futures_util::TryFutureExt; -use rundler_provider::{EntryPointProvider, EthersEntryPointV0_6, Provider}; +use rundler_provider::{EntryPointProvider, EthersEntryPointV0_6, EthersEntryPointV0_7, Provider}; use rundler_sim::{ - simulation::v0_6::{ - SimulateValidationTracerImpl as SimulateValidationTracerImplV0_6, - Simulator as SimulatorV0_6, UnsafeSimulator as UnsafeSimulatorV0_6, + simulation::{ + v0_6::{ + SimulateValidationTracerImpl as SimulateValidationTracerImplV0_6, + Simulator as SimulatorV0_6, + }, + UnsafeSimulator, }, MempoolConfig, PriorityFeeMode, SimulationSettings, Simulator, }; use rundler_task::Task; use rundler_types::{ - chain::ChainSpec, pool::Pool, v0_6, EntryPointVersion, UserOperation, UserOperationVariant, + chain::ChainSpec, pool::Pool, v0_6, v0_7, EntryPointVersion, UserOperation, + UserOperationVariant, }; use rundler_utils::{emit::WithEntryPoint, handle}; use rusoto_core::Region; @@ -143,7 +147,11 @@ where rundler_provider::new_provider(&self.args.rpc_url, Some(self.args.eth_poll_interval))?; let ep_v0_6 = EthersEntryPointV0_6::new( - self.args.chain_spec.entry_point_address, + self.args.chain_spec.entry_point_address_v0_6, + Arc::clone(&provider), + ); + let ep_v0_7 = EthersEntryPointV0_7::new( + self.args.chain_spec.entry_point_address_v0_7, Arc::clone(&provider), ); @@ -151,37 +159,24 @@ where let mut bundle_sender_actions = vec![]; for ep in &self.args.entry_points { - // TODO entry point v0.7: needs 0.7 EP and simulator - if ep.version != EntryPointVersion::V0_6 { - bail!("Unsupported entry point version: {:?}", ep.version); - } - - info!("Mempool config for ep v0.6: {:?}", ep.mempool_configs); - - for i in 0..ep.num_bundle_builders { - let (spawn_guard, bundle_sender_action) = if self.args.unsafe_mode { - self.create_bundle_builder( - i + ep.bundle_builder_index_offset, - Arc::clone(&provider), - ep_v0_6.clone(), - self.create_unsafe_simulator_v0_6(Arc::clone(&provider), ep_v0_6.clone()), - ) - .await? - } else { - self.create_bundle_builder( - i + ep.bundle_builder_index_offset, - Arc::clone(&provider), - ep_v0_6.clone(), - self.create_simulator_v0_6( - Arc::clone(&provider), - ep_v0_6.clone(), - ep.mempool_configs.clone(), - ), - ) - .await? - }; - sender_handles.push(spawn_guard); - bundle_sender_actions.push(bundle_sender_action); + match ep.version { + EntryPointVersion::V0_6 => { + let (handles, actions) = self + .create_builders_v0_6(ep, Arc::clone(&provider), ep_v0_6.clone()) + .await?; + sender_handles.extend(handles); + bundle_sender_actions.extend(actions); + } + EntryPointVersion::V0_7 => { + let (handles, actions) = self + .create_builders_v0_7(ep, Arc::clone(&provider), ep_v0_7.clone()) + .await?; + sender_handles.extend(handles); + bundle_sender_actions.extend(actions); + } + EntryPointVersion::Unspecified => { + panic!("Unspecified entry point version") + } } } @@ -195,7 +190,7 @@ where let builder_handle = self.builder_builder.get_handle(); let builder_runnder_handle = self.builder_builder.run( bundle_sender_actions, - vec![self.args.chain_spec.entry_point_address], + vec![self.args.chain_spec.entry_point_address_v0_6], shutdown_token.clone(), ); @@ -255,6 +250,93 @@ where Box::new(self) } + // TODO(danc): Can we DRY these create functions? + async fn create_builders_v0_6( + &self, + ep: &EntryPointBuilderSettings, + provider: Arc>, + ep_v0_6: E, + ) -> anyhow::Result<( + Vec>>, + Vec>, + )> + where + C: JsonRpcClient + 'static, + E: EntryPointProvider + Clone, + { + info!("Mempool config for ep v0.6: {:?}", ep.mempool_configs); + let mut sender_handles = vec![]; + let mut bundle_sender_actions = vec![]; + for i in 0..ep.num_bundle_builders { + let (spawn_guard, bundle_sender_action) = if self.args.unsafe_mode { + self.create_bundle_builder( + i + ep.bundle_builder_index_offset, + Arc::clone(&provider), + ep_v0_6.clone(), + UnsafeSimulator::new( + Arc::clone(&provider), + ep_v0_6.clone(), + self.args.sim_settings, + ), + ) + .await? + } else { + self.create_bundle_builder( + i + ep.bundle_builder_index_offset, + Arc::clone(&provider), + ep_v0_6.clone(), + self.create_simulator_v0_6( + Arc::clone(&provider), + ep_v0_6.clone(), + ep.mempool_configs.clone(), + ), + ) + .await? + }; + sender_handles.push(spawn_guard); + bundle_sender_actions.push(bundle_sender_action); + } + Ok((sender_handles, bundle_sender_actions)) + } + + async fn create_builders_v0_7( + &self, + ep: &EntryPointBuilderSettings, + provider: Arc>, + ep_v0_7: E, + ) -> anyhow::Result<( + Vec>>, + Vec>, + )> + where + C: JsonRpcClient + 'static, + E: EntryPointProvider + Clone, + { + info!("Mempool config for ep v0.7: {:?}", ep.mempool_configs); + let mut sender_handles = vec![]; + let mut bundle_sender_actions = vec![]; + for i in 0..ep.num_bundle_builders { + let (spawn_guard, bundle_sender_action) = if self.args.unsafe_mode { + self.create_bundle_builder( + i + ep.bundle_builder_index_offset, + Arc::clone(&provider), + ep_v0_7.clone(), + UnsafeSimulator::new( + Arc::clone(&provider), + ep_v0_7.clone(), + self.args.sim_settings, + ), + ) + .await? + } else { + panic!("V0.7 safe simulation not implemented") + }; + sender_handles.push(spawn_guard); + bundle_sender_actions.push(bundle_sender_action); + } + Ok((sender_handles, bundle_sender_actions)) + } + async fn create_bundle_builder( &self, index: u64, @@ -395,16 +477,4 @@ where mempool_configs, ) } - - fn create_unsafe_simulator_v0_6( - &self, - provider: Arc, - ep: E, - ) -> UnsafeSimulatorV0_6 - where - C: Provider, - E: EntryPointProvider + Clone, - { - UnsafeSimulatorV0_6::new(Arc::clone(&provider), ep, self.args.sim_settings) - } } diff --git a/crates/pool/proto/op_pool/op_pool.proto b/crates/pool/proto/op_pool/op_pool.proto index 8f5056800..76fe77f5a 100644 --- a/crates/pool/proto/op_pool/op_pool.proto +++ b/crates/pool/proto/op_pool/op_pool.proto @@ -20,6 +20,7 @@ package op_pool; message UserOperation { oneof uo { UserOperationV06 v06 = 1; + UserOperationV07 v07 = 2; } } @@ -49,11 +50,48 @@ message UserOperationV06 { // Address of paymaster sponsoring the transaction, followed by extra data to // send to the paymaster (empty for self-sponsored transaction) bytes paymaster_and_data = 10; - // Data passed into the account along with the nonce during the verification - // step + // Signature over the hash of the packed representation of the user operation bytes signature = 11; } +message UserOperationV07 { + // The account making the operation + bytes sender = 1; + // Anti-replay parameter (see “Semi-abstracted Nonce Support” ) + bytes nonce = 2; + // The data to pass to the sender during the main execution call + bytes call_data = 3; + // The amount of gas to allocate the main execution call + bytes call_gas_limit = 4; + // The amount of gas to allocate for the verification step + bytes verification_gas_limit = 5; + // The amount of gas to pay for to compensate the bundler for pre-verification + // execution and calldata + bytes pre_verification_gas = 6; + // Maximum fee per gas (similar to EIP-1559 max_fee_per_gas) + bytes max_fee_per_gas = 7; + // Maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas) + bytes max_priority_fee_per_gas = 8; + // Signature over the hash of the packed representation of the user operation + bytes signature = 9; + // Address of paymaster sponsoring the transaction, empty if none + bytes paymaster = 10; + // Extra data to send to the paymaster, zero if no paymaster + bytes paymaster_data = 11; + // Paymaster verification gas limit, zero if no paymaster + bytes paymaster_verification_gas_limit = 12; + // Paymaster post-op gas limit, zero if no paymaster + bytes paymaster_post_op_gas_limit = 13; + // Address of the factory to use to create the sender account, empty if none + bytes factory = 14; + // Extra data to send to the factory, empty if no factory + bytes factory_data = 15; + + // Extra data to compute the hash of the user operation + bytes entry_point = 16; + uint64 chain_id = 17; +} + enum EntityType { ENTITY_TYPE_UNSPECIFIED = 0; ENTITY_TYPE_ACCOUNT = 1; @@ -609,6 +647,7 @@ message SimulationViolationError { UnstakedPaymasterContext unstaked_paymaster_context = 17; UnstakedAggregator unstaked_aggregator = 18; VerificationGasLimitBufferTooLow verification_gas_limit_buffer_too_low = 19; + ValidationRevert validation_revert = 20; } } @@ -689,3 +728,21 @@ message VerificationGasLimitBufferTooLow { bytes limit = 1; bytes needed = 2; } + +message ValidationRevert { + oneof revert { + EntryPointRevert entry_point = 1; + OperationRevert operation = 2; + UnknownRevert unknown = 3; + } +} +message EntryPointRevert { + string reason = 1; +} +message OperationRevert { + string reason = 1; + bytes revert_bytes = 2; +} +message UnknownRevert { + bytes revert_bytes = 1; +} diff --git a/crates/pool/src/server/remote/client.rs b/crates/pool/src/server/remote/client.rs index e8cce9a46..3d64e4ca7 100644 --- a/crates/pool/src/server/remote/client.rs +++ b/crates/pool/src/server/remote/client.rs @@ -16,7 +16,7 @@ use std::{pin::Pin, str::FromStr}; use ethers::types::{Address, H256}; use futures_util::Stream; use rundler_task::{ - grpc::protos::{from_bytes, to_le_bytes, ConversionError}, + grpc::protos::{from_bytes, ConversionError, ToProtoBytes}, server::{HealthCheck, ServerStatus}, }; use rundler_types::{ @@ -255,9 +255,9 @@ impl Pool for RemotePoolClient { .op_pool_client .clone() .remove_op_by_id(protos::RemoveOpByIdRequest { - entry_point: entry_point.as_bytes().to_vec(), - sender: id.sender.as_bytes().to_vec(), - nonce: to_le_bytes(id.nonce), + entry_point: entry_point.to_proto_bytes(), + sender: id.sender.to_proto_bytes(), + nonce: id.nonce.to_proto_bytes(), }) .await .map_err(anyhow::Error::from)? diff --git a/crates/pool/src/server/remote/error.rs b/crates/pool/src/server/remote/error.rs index add8ee6ff..d8f95de34 100644 --- a/crates/pool/src/server/remote/error.rs +++ b/crates/pool/src/server/remote/error.rs @@ -13,31 +13,31 @@ use anyhow::{bail, Context}; use ethers::types::Opcode; -use rundler_task::grpc::protos::{from_bytes, to_le_bytes}; +use rundler_task::grpc::protos::{from_bytes, ToProtoBytes}; use rundler_types::{ pool::{ MempoolError, NeedsStakeInformation, PoolError, PrecheckViolation, SimulationViolation, }, - StorageSlot, ViolationOpCode, + StorageSlot, ValidationRevert, ViolationOpCode, }; use super::protos::{ - mempool_error, precheck_violation_error, simulation_violation_error, + mempool_error, precheck_violation_error, simulation_violation_error, validation_revert, AccessedUndeployedContract, AggregatorValidationFailed, AssociatedStorageIsAlternateSender, CallGasLimitTooLow, CallHadValue, CalledBannedEntryPointMethod, CodeHashChanged, DidNotRevert, - DiscardedOnInsertError, Entity, EntityThrottledError, EntityType, ExistingSenderWithInitCode, - FactoryCalledCreate2Twice, FactoryIsNotContract, InvalidSignature, InvalidStorageAccess, - MaxFeePerGasTooLow, MaxOperationsReachedError, MaxPriorityFeePerGasTooLow, - MempoolError as ProtoMempoolError, MultipleRolesViolation, NotStaked, - OperationAlreadyKnownError, OperationDropTooSoon, OutOfGas, PaymasterBalanceTooLow, - PaymasterDepositTooLow, PaymasterIsNotContract, PreVerificationGasTooLow, - PrecheckViolationError as ProtoPrecheckViolationError, ReplacementUnderpricedError, - SenderAddressUsedAsAlternateEntity, SenderFundsTooLow, SenderIsNotContractAndNoInitCode, - SimulationViolationError as ProtoSimulationViolationError, TotalGasLimitTooHigh, - UnintendedRevert, UnintendedRevertWithMessage, UnknownEntryPointError, UnstakedAggregator, - UnstakedPaymasterContext, UnsupportedAggregatorError, UsedForbiddenOpcode, - UsedForbiddenPrecompile, VerificationGasLimitBufferTooLow, VerificationGasLimitTooHigh, - WrongNumberOfPhases, + DiscardedOnInsertError, Entity, EntityThrottledError, EntityType, EntryPointRevert, + ExistingSenderWithInitCode, FactoryCalledCreate2Twice, FactoryIsNotContract, InvalidSignature, + InvalidStorageAccess, MaxFeePerGasTooLow, MaxOperationsReachedError, + MaxPriorityFeePerGasTooLow, MempoolError as ProtoMempoolError, MultipleRolesViolation, + NotStaked, OperationAlreadyKnownError, OperationDropTooSoon, OperationRevert, OutOfGas, + PaymasterBalanceTooLow, PaymasterDepositTooLow, PaymasterIsNotContract, + PreVerificationGasTooLow, PrecheckViolationError as ProtoPrecheckViolationError, + ReplacementUnderpricedError, SenderAddressUsedAsAlternateEntity, SenderFundsTooLow, + SenderIsNotContractAndNoInitCode, SimulationViolationError as ProtoSimulationViolationError, + TotalGasLimitTooHigh, UnintendedRevert, UnintendedRevertWithMessage, UnknownEntryPointError, + UnknownRevert, UnstakedAggregator, UnstakedPaymasterContext, UnsupportedAggregatorError, + UsedForbiddenOpcode, UsedForbiddenPrecompile, ValidationRevert as ProtoValidationRevert, + VerificationGasLimitBufferTooLow, VerificationGasLimitTooHigh, WrongNumberOfPhases, }; impl TryFrom for PoolError { @@ -155,15 +155,15 @@ impl From for ProtoMempoolError { MempoolError::SenderAddressUsedAsAlternateEntity(addr) => ProtoMempoolError { error: Some(mempool_error::Error::SenderAddressUsedAsAlternateEntity( SenderAddressUsedAsAlternateEntity { - sender_address: addr.as_bytes().to_vec(), + sender_address: addr.to_proto_bytes(), }, )), }, MempoolError::ReplacementUnderpriced(fee, priority_fee) => ProtoMempoolError { error: Some(mempool_error::Error::ReplacementUnderpriced( ReplacementUnderpricedError { - current_fee: to_le_bytes(fee), - current_priority_fee: to_le_bytes(priority_fee), + current_fee: fee.to_proto_bytes(), + current_priority_fee: priority_fee.to_proto_bytes(), }, )), }, @@ -171,7 +171,7 @@ impl From for ProtoMempoolError { error: Some(mempool_error::Error::MaxOperationsReached( MaxOperationsReachedError { num_ops: ops as u64, - entity_address: addr.as_bytes().to_vec(), + entity_address: addr.to_proto_bytes(), }, )), }, @@ -191,8 +191,8 @@ impl From for ProtoMempoolError { ProtoMempoolError { error: Some(mempool_error::Error::PaymasterBalanceTooLow( PaymasterBalanceTooLow { - current_balance: to_le_bytes(current_balance), - required_balance: to_le_bytes(required_balance), + current_balance: current_balance.to_proto_bytes(), + required_balance: required_balance.to_proto_bytes(), }, )), } @@ -206,14 +206,14 @@ impl From for ProtoMempoolError { MempoolError::UnsupportedAggregator(agg) => ProtoMempoolError { error: Some(mempool_error::Error::UnsupportedAggregator( UnsupportedAggregatorError { - aggregator_address: agg.as_bytes().to_vec(), + aggregator_address: agg.to_proto_bytes(), }, )), }, MempoolError::UnknownEntryPoint(entry_point) => ProtoMempoolError { error: Some(mempool_error::Error::UnknownEntryPoint( UnknownEntryPointError { - entry_point: entry_point.as_bytes().to_vec(), + entry_point: entry_point.to_proto_bytes(), }, )), }, @@ -240,7 +240,7 @@ impl From for ProtoPrecheckViolationError { violation: Some( precheck_violation_error::Violation::SenderIsNotContractAndNoInitCode( SenderIsNotContractAndNoInitCode { - sender_address: addr.as_bytes().to_vec(), + sender_address: addr.to_proto_bytes(), }, ), ), @@ -250,7 +250,7 @@ impl From for ProtoPrecheckViolationError { violation: Some( precheck_violation_error::Violation::ExistingSenderWithInitCode( ExistingSenderWithInitCode { - sender_address: addr.as_bytes().to_vec(), + sender_address: addr.to_proto_bytes(), }, ), ), @@ -258,15 +258,15 @@ impl From for ProtoPrecheckViolationError { PrecheckViolation::FactoryIsNotContract(addr) => ProtoPrecheckViolationError { violation: Some(precheck_violation_error::Violation::FactoryIsNotContract( FactoryIsNotContract { - factory_address: addr.as_bytes().to_vec(), + factory_address: addr.to_proto_bytes(), }, )), }, PrecheckViolation::TotalGasLimitTooHigh(actual, max) => ProtoPrecheckViolationError { violation: Some(precheck_violation_error::Violation::TotalGasLimitTooHigh( TotalGasLimitTooHigh { - actual_gas: to_le_bytes(actual), - max_gas: to_le_bytes(max), + actual_gas: actual.to_proto_bytes(), + max_gas: max.to_proto_bytes(), }, )), }, @@ -275,8 +275,8 @@ impl From for ProtoPrecheckViolationError { violation: Some( precheck_violation_error::Violation::VerificationGasLimitTooHigh( VerificationGasLimitTooHigh { - actual_gas: to_le_bytes(actual), - max_gas: to_le_bytes(max), + actual_gas: actual.to_proto_bytes(), + max_gas: max.to_proto_bytes(), }, ), ), @@ -287,8 +287,8 @@ impl From for ProtoPrecheckViolationError { violation: Some( precheck_violation_error::Violation::PreVerificationGasTooLow( PreVerificationGasTooLow { - actual_gas: to_le_bytes(actual), - min_gas: to_le_bytes(min), + actual_gas: actual.to_proto_bytes(), + min_gas: min.to_proto_bytes(), }, ), ), @@ -297,31 +297,31 @@ impl From for ProtoPrecheckViolationError { PrecheckViolation::PaymasterIsNotContract(addr) => ProtoPrecheckViolationError { violation: Some(precheck_violation_error::Violation::PaymasterIsNotContract( PaymasterIsNotContract { - paymaster_address: addr.as_bytes().to_vec(), + paymaster_address: addr.to_proto_bytes(), }, )), }, PrecheckViolation::PaymasterDepositTooLow(actual, min) => ProtoPrecheckViolationError { violation: Some(precheck_violation_error::Violation::PaymasterDepositTooLow( PaymasterDepositTooLow { - actual_deposit: to_le_bytes(actual), - min_deposit: to_le_bytes(min), + actual_deposit: actual.to_proto_bytes(), + min_deposit: min.to_proto_bytes(), }, )), }, PrecheckViolation::SenderFundsTooLow(actual, min) => ProtoPrecheckViolationError { violation: Some(precheck_violation_error::Violation::SenderFundsTooLow( SenderFundsTooLow { - actual_funds: to_le_bytes(actual), - min_funds: to_le_bytes(min), + actual_funds: actual.to_proto_bytes(), + min_funds: min.to_proto_bytes(), }, )), }, PrecheckViolation::MaxFeePerGasTooLow(actual, min) => ProtoPrecheckViolationError { violation: Some(precheck_violation_error::Violation::MaxFeePerGasTooLow( MaxFeePerGasTooLow { - actual_fee: to_le_bytes(actual), - min_fee: to_le_bytes(min), + actual_fee: actual.to_proto_bytes(), + min_fee: min.to_proto_bytes(), }, )), }, @@ -330,8 +330,8 @@ impl From for ProtoPrecheckViolationError { violation: Some( precheck_violation_error::Violation::MaxPriorityFeePerGasTooLow( MaxPriorityFeePerGasTooLow { - actual_fee: to_le_bytes(actual), - min_fee: to_le_bytes(min), + actual_fee: actual.to_proto_bytes(), + min_fee: min.to_proto_bytes(), }, ), ), @@ -340,8 +340,8 @@ impl From for ProtoPrecheckViolationError { PrecheckViolation::CallGasLimitTooLow(actual, min) => ProtoPrecheckViolationError { violation: Some(precheck_violation_error::Violation::CallGasLimitTooLow( CallGasLimitTooLow { - actual_gas_limit: to_le_bytes(actual), - min_gas_limit: to_le_bytes(min), + actual_gas_limit: actual.to_proto_bytes(), + min_gas_limit: min.to_proto_bytes(), }, )), }, @@ -444,7 +444,7 @@ impl From for ProtoSimulationViolationError { entity: Some(Entity { kind: EntityType::from(et) as i32, address: maybe_address - .map_or(vec![], |addr| addr.as_bytes().to_vec()), + .map_or(vec![], |addr| addr.to_proto_bytes()), }), reason, }, @@ -457,7 +457,7 @@ impl From for ProtoSimulationViolationError { violation: Some(simulation_violation_error::Violation::UsedForbiddenOpcode( UsedForbiddenOpcode { entity: Some((&entity).into()), - contract_address: addr.as_bytes().to_vec(), + contract_address: addr.to_proto_bytes(), opcode: opcode.0 as u32, }, )), @@ -472,8 +472,8 @@ impl From for ProtoSimulationViolationError { simulation_violation_error::Violation::UsedForbiddenPrecompile( UsedForbiddenPrecompile { entity: Some((&entity).into()), - contract_address: contract_addr.as_bytes().to_vec(), - precompile_address: precompile_addr.as_bytes().to_vec(), + contract_address: contract_addr.to_proto_bytes(), + precompile_address: precompile_addr.to_proto_bytes(), }, ), ), @@ -482,7 +482,7 @@ impl From for ProtoSimulationViolationError { violation: Some( simulation_violation_error::Violation::FactoryCalledCreate2Twice( FactoryCalledCreate2Twice { - factory_address: addr.as_bytes().to_vec(), + factory_address: addr.to_proto_bytes(), }, ), ), @@ -492,8 +492,8 @@ impl From for ProtoSimulationViolationError { violation: Some(simulation_violation_error::Violation::InvalidStorageAccess( InvalidStorageAccess { entity: Some((&entity).into()), - contract_address: slot.address.as_bytes().to_vec(), - slot: to_le_bytes(slot.slot), + contract_address: slot.address.to_proto_bytes(), + slot: slot.slot.to_proto_bytes(), }, )), } @@ -502,11 +502,11 @@ impl From for ProtoSimulationViolationError { violation: Some(simulation_violation_error::Violation::NotStaked( NotStaked { entity: Some((&stake_data.entity).into()), - accessed_address: stake_data.accessed_address.as_bytes().to_vec(), + accessed_address: stake_data.accessed_address.to_proto_bytes(), accessed_entity: EntityType::from(stake_data.accessed_entity) as i32, - slot: to_le_bytes(stake_data.slot), - min_stake: to_le_bytes(stake_data.min_stake), - min_unstake_delay: to_le_bytes(stake_data.min_unstake_delay), + slot: stake_data.slot.to_proto_bytes(), + min_stake: stake_data.min_stake.to_proto_bytes(), + min_unstake_delay: stake_data.min_unstake_delay.to_proto_bytes(), }, )), }, @@ -516,13 +516,17 @@ impl From for ProtoSimulationViolationError { UnintendedRevert { entity: Some(Entity { kind: EntityType::from(et) as i32, - address: maybe_address - .map_or(vec![], |addr| addr.as_bytes().to_vec()), + address: maybe_address.map_or(vec![], |addr| addr.to_proto_bytes()), }), }, )), } } + SimulationViolation::ValidationRevert(revert) => ProtoSimulationViolationError { + violation: Some(simulation_violation_error::Violation::ValidationRevert( + revert.into(), + )), + }, SimulationViolation::DidNotRevert => ProtoSimulationViolationError { violation: Some(simulation_violation_error::Violation::DidNotRevert( DidNotRevert {}, @@ -556,7 +560,7 @@ impl From for ProtoSimulationViolationError { simulation_violation_error::Violation::AccessedUndeployedContract( AccessedUndeployedContract { entity: Some((&entity).into()), - contract_address: contract_addr.as_bytes().to_vec(), + contract_address: contract_addr.to_proto_bytes(), }, ), ), @@ -590,8 +594,8 @@ impl From for ProtoSimulationViolationError { violation: Some( simulation_violation_error::Violation::VerificationGasLimitBufferTooLow( VerificationGasLimitBufferTooLow { - limit: to_le_bytes(limit), - needed: to_le_bytes(needed), + limit: limit.to_proto_bytes(), + needed: needed.to_proto_bytes(), }, ), ), @@ -685,6 +689,9 @@ impl TryFrom for SimulationViolation { }, ) } + Some(simulation_violation_error::Violation::ValidationRevert(e)) => { + SimulationViolation::ValidationRevert(e.try_into()?) + } Some(simulation_violation_error::Violation::DidNotRevert(_)) => { SimulationViolation::DidNotRevert } @@ -734,6 +741,51 @@ impl TryFrom for SimulationViolation { } } +impl From for ProtoValidationRevert { + fn from(revert: ValidationRevert) -> Self { + let inner = match revert { + ValidationRevert::EntryPoint(reason) => { + validation_revert::Revert::EntryPoint(EntryPointRevert { reason }) + } + ValidationRevert::Operation(reason, revert_bytes) => { + validation_revert::Revert::Operation(OperationRevert { + reason, + revert_bytes: revert_bytes.to_vec(), + }) + } + ValidationRevert::Unknown(revert_bytes) => { + validation_revert::Revert::Unknown(UnknownRevert { + revert_bytes: revert_bytes.to_vec(), + }) + } + }; + ProtoValidationRevert { + revert: Some(inner), + } + } +} + +impl TryFrom for ValidationRevert { + type Error = anyhow::Error; + + fn try_from(value: ProtoValidationRevert) -> Result { + Ok(match value.revert { + Some(validation_revert::Revert::EntryPoint(e)) => { + ValidationRevert::EntryPoint(e.reason) + } + Some(validation_revert::Revert::Operation(e)) => { + ValidationRevert::Operation(e.reason, e.revert_bytes.into()) + } + Some(validation_revert::Revert::Unknown(e)) => { + ValidationRevert::Unknown(e.revert_bytes.into()) + } + None => { + bail!("unknown proto validation revert") + } + }) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/pool/src/server/remote/protos.rs b/crates/pool/src/server/remote/protos.rs index ec4f034ce..90b0948fc 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -13,14 +13,14 @@ use anyhow::{anyhow, Context}; use ethers::types::{Address, H256}; -use rundler_task::grpc::protos::{from_bytes, to_le_bytes, ConversionError}; +use rundler_task::grpc::protos::{from_bytes, ConversionError, ToProtoBytes}; use rundler_types::{ pool::{ NewHead as PoolNewHead, PaymasterMetadata as PoolPaymasterMetadata, PoolOperation, Reputation as PoolReputation, ReputationStatus as PoolReputationStatus, StakeStatus as RundlerStakeStatus, }, - v0_6, Entity as RundlerEntity, EntityInfos, EntityType as RundlerEntityType, + v0_6, v0_7, Entity as RundlerEntity, EntityInfos, EntityType as RundlerEntityType, EntityUpdate as RundlerEntityUpdate, EntityUpdateType as RundlerEntityUpdateType, StakeInfo as RundlerStakeInfo, UserOperationVariant, ValidTimeRange, }; @@ -34,9 +34,7 @@ impl From<&UserOperationVariant> for UserOperation { fn from(op: &UserOperationVariant) -> Self { match op { UserOperationVariant::V0_6(op) => op.into(), - UserOperationVariant::V0_7(_) => { - unimplemented!("V0_7 user operation is not supported") - } + UserOperationVariant::V0_7(op) => op.into(), } } } @@ -44,17 +42,17 @@ impl From<&UserOperationVariant> for UserOperation { impl From<&v0_6::UserOperation> for UserOperation { fn from(op: &v0_6::UserOperation) -> Self { let op = UserOperationV06 { - sender: op.sender.0.to_vec(), - nonce: to_le_bytes(op.nonce), - init_code: op.init_code.to_vec(), - call_data: op.call_data.to_vec(), - call_gas_limit: to_le_bytes(op.call_gas_limit), - verification_gas_limit: to_le_bytes(op.verification_gas_limit), - pre_verification_gas: to_le_bytes(op.pre_verification_gas), - max_fee_per_gas: to_le_bytes(op.max_fee_per_gas), - max_priority_fee_per_gas: to_le_bytes(op.max_priority_fee_per_gas), - paymaster_and_data: op.paymaster_and_data.to_vec(), - signature: op.signature.to_vec(), + sender: op.sender.to_proto_bytes(), + nonce: op.nonce.to_proto_bytes(), + init_code: op.init_code.to_proto_bytes(), + call_data: op.call_data.to_proto_bytes(), + call_gas_limit: op.call_gas_limit.to_proto_bytes(), + verification_gas_limit: op.verification_gas_limit.to_proto_bytes(), + pre_verification_gas: op.pre_verification_gas.to_proto_bytes(), + max_fee_per_gas: op.max_fee_per_gas.to_proto_bytes(), + max_priority_fee_per_gas: op.max_priority_fee_per_gas.to_proto_bytes(), + paymaster_and_data: op.paymaster_and_data.to_proto_bytes(), + signature: op.signature.to_proto_bytes(), }; UserOperation { uo: Some(user_operation::Uo::V06(op)), @@ -82,6 +80,70 @@ impl TryFrom for v0_6::UserOperation { } } +impl From<&v0_7::UserOperation> for UserOperation { + fn from(op: &v0_7::UserOperation) -> Self { + let op = UserOperationV07 { + sender: op.sender.to_proto_bytes(), + nonce: op.nonce.to_proto_bytes(), + call_data: op.call_data.to_proto_bytes(), + call_gas_limit: op.call_gas_limit.to_proto_bytes(), + verification_gas_limit: op.verification_gas_limit.to_proto_bytes(), + pre_verification_gas: op.pre_verification_gas.to_proto_bytes(), + max_fee_per_gas: op.max_fee_per_gas.to_proto_bytes(), + max_priority_fee_per_gas: op.max_priority_fee_per_gas.to_proto_bytes(), + signature: op.signature.to_proto_bytes(), + paymaster: op.paymaster.map(|p| p.to_proto_bytes()).unwrap_or_default(), + paymaster_data: op.paymaster_data.to_proto_bytes(), + paymaster_verification_gas_limit: op.paymaster_verification_gas_limit.to_proto_bytes(), + paymaster_post_op_gas_limit: op.paymaster_post_op_gas_limit.to_proto_bytes(), + factory: op.factory.map(|f| f.to_proto_bytes()).unwrap_or_default(), + factory_data: op.factory_data.to_proto_bytes(), + entry_point: op.entry_point.to_proto_bytes(), + chain_id: op.chain_id, + }; + UserOperation { + uo: Some(user_operation::Uo::V07(op)), + } + } +} + +impl TryFrom for v0_7::UserOperation { + type Error = ConversionError; + + fn try_from(op: UserOperationV07) -> Result { + let mut builder = v0_7::UserOperationBuilder::new( + from_bytes(&op.entry_point)?, + op.chain_id, + v0_7::UserOperationRequiredFields { + sender: from_bytes(&op.sender)?, + nonce: from_bytes(&op.nonce)?, + call_data: op.call_data.into(), + call_gas_limit: from_bytes(&op.call_gas_limit)?, + verification_gas_limit: from_bytes(&op.verification_gas_limit)?, + pre_verification_gas: from_bytes(&op.pre_verification_gas)?, + max_priority_fee_per_gas: from_bytes(&op.max_priority_fee_per_gas)?, + max_fee_per_gas: from_bytes(&op.max_fee_per_gas)?, + signature: op.signature.into(), + }, + ); + + if !op.paymaster.is_empty() { + builder = builder.paymaster( + from_bytes(&op.paymaster)?, + from_bytes(&op.paymaster_verification_gas_limit)?, + from_bytes(&op.paymaster_post_op_gas_limit)?, + op.paymaster_data.into(), + ); + } + + if !op.factory.is_empty() { + builder = builder.factory(from_bytes(&op.factory)?, op.factory_data.into()); + } + + Ok(builder.build()) + } +} + impl TryFrom for UserOperationVariant { type Error = ConversionError; @@ -92,6 +154,7 @@ impl TryFrom for UserOperationVariant { match op { user_operation::Uo::V06(op) => Ok(UserOperationVariant::V0_6(op.try_into()?)), + user_operation::Uo::V07(op) => Ok(UserOperationVariant::V0_7(op.try_into()?)), } } } @@ -172,7 +235,7 @@ impl From<&RundlerEntity> for Entity { fn from(entity: &RundlerEntity) -> Self { Entity { kind: EntityType::from(entity.kind).into(), - address: entity.address.as_bytes().to_vec(), + address: entity.address.to_proto_bytes(), } } } @@ -221,7 +284,7 @@ impl TryFrom for PoolReputationStatus { impl From for Reputation { fn from(rep: PoolReputation) -> Self { Reputation { - address: rep.address.as_bytes().to_vec(), + address: rep.address.to_proto_bytes(), ops_seen: rep.ops_seen, ops_included: rep.ops_included, } @@ -274,12 +337,12 @@ impl From<&PoolOperation> for MempoolOp { fn from(op: &PoolOperation) -> Self { MempoolOp { uo: Some(UserOperation::from(&op.uo)), - entry_point: op.entry_point.as_bytes().to_vec(), - aggregator: op.aggregator.map_or(vec![], |a| a.as_bytes().to_vec()), + entry_point: op.entry_point.to_proto_bytes(), + aggregator: op.aggregator.map_or(vec![], |a| a.to_proto_bytes()), valid_after: op.valid_time_range.valid_after.seconds_since_epoch(), valid_until: op.valid_time_range.valid_until.seconds_since_epoch(), - expected_code_hash: op.expected_code_hash.as_bytes().to_vec(), - sim_block_hash: op.sim_block_hash.as_bytes().to_vec(), + expected_code_hash: op.expected_code_hash.to_proto_bytes(), + sim_block_hash: op.sim_block_hash.to_proto_bytes(), entities_needing_stake: op .entities_needing_stake .iter() @@ -348,7 +411,7 @@ impl TryFrom for PoolNewHead { impl From for NewHead { fn from(head: PoolNewHead) -> Self { Self { - block_hash: head.block_hash.as_bytes().to_vec(), + block_hash: head.block_hash.to_proto_bytes(), block_number: head.block_number, } } @@ -370,8 +433,8 @@ impl From for PaymasterBalance { fn from(paymaster_metadata: PoolPaymasterMetadata) -> Self { Self { address: paymaster_metadata.address.as_bytes().to_vec(), - confirmed_balance: to_le_bytes(paymaster_metadata.confirmed_balance), - pending_balance: to_le_bytes(paymaster_metadata.pending_balance), + confirmed_balance: paymaster_metadata.confirmed_balance.to_proto_bytes(), + pending_balance: paymaster_metadata.pending_balance.to_proto_bytes(), } } } diff --git a/crates/pool/src/task.rs b/crates/pool/src/task.rs index ed8a13de4..1313cf823 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -16,10 +16,13 @@ use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; use anyhow::{bail, Context}; use async_trait::async_trait; use ethers::providers::Middleware; -use rundler_provider::{EthersEntryPointV0_6, Provider}; -use rundler_sim::{simulation::v0_6 as sim_v0_6, PrecheckerImpl}; +use rundler_provider::{EntryPointProvider, EthersEntryPointV0_6, EthersEntryPointV0_7, Provider}; +use rundler_sim::{ + simulation::{v0_6 as sim_v0_6, UnsafeSimulator}, + PrecheckerImpl, Simulator, +}; use rundler_task::Task; -use rundler_types::{chain::ChainSpec, EntryPointVersion}; +use rundler_types::{chain::ChainSpec, EntryPointVersion, UserOperation, UserOperationVariant}; use rundler_utils::{emit::WithEntryPoint, handle}; use tokio::{sync::broadcast, try_join}; use tokio_util::sync::CancellationToken; @@ -100,7 +103,6 @@ impl Task for PoolTask { self.event_sender.clone(), provider.clone(), ) - .await .context("should have created mempool")?; mempools.insert(pool_config.entry_point, pool); @@ -109,10 +111,10 @@ impl Task for PoolTask { let pool = PoolTask::create_mempool_v0_7( self.args.chain_spec.clone(), pool_config, + self.args.unsafe_mode, self.event_sender.clone(), provider.clone(), ) - .await .context("should have created mempool")?; mempools.insert(pool_config.entry_point, pool); @@ -182,26 +184,88 @@ impl PoolTask { Box::new(self) } - async fn create_mempool_v0_7( - _chain_spec: ChainSpec, - _pool_config: &PoolConfig, - _event_sender: broadcast::Sender>, - _provider: Arc

, + // TODO(danc): when safe simulation for 0.7 is implemented, DRY these functions + fn create_mempool_v0_6( + chain_spec: ChainSpec, + pool_config: &PoolConfig, + unsafe_mode: bool, + event_sender: broadcast::Sender>, + provider: Arc

, ) -> anyhow::Result>> { - // TODO: implement - // requires 0.7 simulation - todo!() + let ep = EthersEntryPointV0_6::new(pool_config.entry_point, Arc::clone(&provider)); + + if unsafe_mode { + let simulator = + UnsafeSimulator::new(Arc::clone(&provider), ep.clone(), pool_config.sim_settings); + Self::create_mempool( + chain_spec, + pool_config, + event_sender, + provider, + ep, + simulator, + ) + } else { + let simulate_validation_tracer = + sim_v0_6::SimulateValidationTracerImpl::new(Arc::clone(&provider), ep.clone()); + let simulator = sim_v0_6::Simulator::new( + Arc::clone(&provider), + ep.clone(), + simulate_validation_tracer, + pool_config.sim_settings, + pool_config.mempool_channel_configs.clone(), + ); + Self::create_mempool( + chain_spec, + pool_config, + event_sender, + provider, + ep, + simulator, + ) + } } - async fn create_mempool_v0_6( + fn create_mempool_v0_7( chain_spec: ChainSpec, pool_config: &PoolConfig, unsafe_mode: bool, event_sender: broadcast::Sender>, provider: Arc

, ) -> anyhow::Result>> { - let ep = EthersEntryPointV0_6::new(pool_config.entry_point, Arc::clone(&provider)); + let ep = EthersEntryPointV0_7::new(pool_config.entry_point, Arc::clone(&provider)); + + if unsafe_mode { + let simulator = + UnsafeSimulator::new(Arc::clone(&provider), ep.clone(), pool_config.sim_settings); + Self::create_mempool( + chain_spec, + pool_config, + event_sender, + provider, + ep, + simulator, + ) + } else { + panic!("V0_7 safe simulation not supported") + } + } + fn create_mempool( + chain_spec: ChainSpec, + pool_config: &PoolConfig, + event_sender: broadcast::Sender>, + provider: Arc

, + ep: E, + simulator: S, + ) -> anyhow::Result>> + where + UO: UserOperation + From + Into, + UserOperationVariant: From, + P: Provider, + E: EntryPointProvider + Clone, + S: Simulator, + { let prechecker = PrecheckerImpl::new( chain_spec, Arc::clone(&provider), @@ -229,44 +293,15 @@ impl PoolTask { ), ); - if unsafe_mode { - let simulator = sim_v0_6::UnsafeSimulator::new( - Arc::clone(&provider), - ep.clone(), - pool_config.sim_settings, - ); - - let uo_pool = UoPool::new( - pool_config.clone(), - event_sender, - prechecker, - simulator, - paymaster, - reputation, - ); - - Ok(Arc::new(Box::new(uo_pool))) - } else { - let simulate_validation_tracer = - sim_v0_6::SimulateValidationTracerImpl::new(Arc::clone(&provider), ep.clone()); - let simulator = sim_v0_6::Simulator::new( - Arc::clone(&provider), - ep.clone(), - simulate_validation_tracer, - pool_config.sim_settings, - pool_config.mempool_channel_configs.clone(), - ); - - let uo_pool = UoPool::new( - pool_config.clone(), - event_sender, - prechecker, - simulator, - paymaster, - reputation, - ); + let uo_pool = UoPool::new( + pool_config.clone(), + event_sender, + prechecker, + simulator, + paymaster, + reputation, + ); - Ok(Arc::new(Box::new(uo_pool))) - } + Ok(Arc::new(Box::new(uo_pool))) } } diff --git a/crates/provider/src/ethers/entry_point/v0_6.rs b/crates/provider/src/ethers/entry_point/v0_6.rs index b70c4212f..b2fd860fb 100644 --- a/crates/provider/src/ethers/entry_point/v0_6.rs +++ b/crates/provider/src/ethers/entry_point/v0_6.rs @@ -39,7 +39,7 @@ use rundler_types::{ }, }, v0_6::UserOperation, - GasFees, UserOpsPerAggregator, ValidationOutput, + GasFees, UserOpsPerAggregator, ValidationError, ValidationOutput, ValidationRevert, }; use rundler_utils::eth::{self, ContractRevertError}; @@ -316,7 +316,7 @@ where user_op: UserOperation, max_validation_gas: u64, block_hash: Option, - ) -> anyhow::Result { + ) -> Result { let pvg = user_op.pre_verification_gas; let blockless = self .i_entry_point @@ -328,9 +328,18 @@ where }; match call.call().await { - Ok(()) => anyhow::bail!("simulateValidation should always revert"), - Err(ContractError::Revert(revert_data)) => ValidationOutput::decode_v0_6(revert_data) - .context("entry point should return validation output"), + Ok(()) => Err(anyhow::anyhow!("simulateValidation should always revert"))?, + Err(ContractError::Revert(revert_data)) => { + if let Ok(result) = ValidationOutput::decode_v0_6(&revert_data) { + Ok(result) + } else if let Ok(failed_op) = FailedOp::decode(&revert_data) { + Err(ValidationRevert::EntryPoint(failed_op.reason))? + } else if let Ok(err) = ContractRevertError::decode(&revert_data) { + Err(ValidationRevert::EntryPoint(err.reason))? + } else { + Err(ValidationRevert::Unknown(revert_data))? + } + } Err(error) => Err(error).context("call simulation RPC failed")?, } } diff --git a/crates/provider/src/ethers/entry_point/v0_7.rs b/crates/provider/src/ethers/entry_point/v0_7.rs index a30086590..ecb9c4e94 100644 --- a/crates/provider/src/ethers/entry_point/v0_7.rs +++ b/crates/provider/src/ethers/entry_point/v0_7.rs @@ -31,18 +31,18 @@ use rundler_types::{ v0_7::{ entry_point_simulations::{ EntryPointSimulations, ExecutionResult as ExecutionResultV0_7, - ENTRYPOINTSIMULATIONS_BYTECODE, + ENTRYPOINTSIMULATIONS_DEPLOYED_BYTECODE, }, get_balances::{GetBalancesResult, GETBALANCES_BYTECODE}, i_aggregator::IAggregator, i_entry_point::{ - DepositInfo as DepositInfoV0_7, FailedOp, IEntryPoint, SignatureValidationFailed, - UserOpsPerAggregator as UserOpsPerAggregatorV0_7, + DepositInfo as DepositInfoV0_7, FailedOp, FailedOpWithRevert, IEntryPoint, + SignatureValidationFailed, UserOpsPerAggregator as UserOpsPerAggregatorV0_7, }, }, }, v0_7::UserOperation, - GasFees, UserOpsPerAggregator, ValidationOutput, + GasFees, UserOpsPerAggregator, ValidationError, ValidationOutput, ValidationRevert, }; use rundler_utils::eth::{self, ContractRevertError}; @@ -64,7 +64,7 @@ const OPTIMISM_BEDROCK_GAS_ORACLE_ADDRESS: Address = H160([ ]); /// Entry point for the v0.7 contract. -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct EntryPoint

{ i_entry_point: IEntryPoint

, provider: Arc

, @@ -90,6 +90,20 @@ where } } +impl

Clone for EntryPoint

+where + P: Provider + Middleware, +{ + fn clone(&self) -> Self { + Self { + i_entry_point: self.i_entry_point.clone(), + provider: self.provider.clone(), + arb_node: self.arb_node.clone(), + opt_gas_oracle: self.opt_gas_oracle.clone(), + } + } +} + #[async_trait::async_trait] impl

crate::traits::EntryPoint for EntryPoint

where @@ -295,7 +309,7 @@ where let mut spoof_ep = spoof::State::default(); spoof_ep .account(addr) - .code(ENTRYPOINTSIMULATIONS_BYTECODE.clone()); + .code(ENTRYPOINTSIMULATIONS_DEPLOYED_BYTECODE.clone()); let ep_simulations = EntryPointSimulations::new(addr, Arc::clone(&self.provider)); let call = ep_simulations @@ -311,29 +325,30 @@ where user_op: UserOperation, max_validation_gas: u64, block_hash: Option, - ) -> anyhow::Result { + ) -> Result { let addr = self.i_entry_point.address(); let pvg = user_op.pre_verification_gas; let mut spoof_ep = spoof::State::default(); spoof_ep .account(addr) - .code(ENTRYPOINTSIMULATIONS_BYTECODE.clone()); + .code(ENTRYPOINTSIMULATIONS_DEPLOYED_BYTECODE.clone()); let ep_simulations = EntryPointSimulations::new(addr, Arc::clone(&self.provider)); let blockless = ep_simulations - .simulate_validation(user_op.pack()) + .simulate_validation(user_op.clone().pack()) .gas(U256::from(max_validation_gas) + pvg); let call = match block_hash { Some(block_hash) => blockless.block(block_hash), None => blockless, }; - let result = call - .call_raw() - .state(&spoof_ep) - .await - .context("should simulate validation")?; - Ok(result.into()) + match call.call_raw().state(&spoof_ep).await { + Ok(output) => Ok(output.into()), + Err(ContractError::Revert(revert_data)) => { + Err(decode_simulate_validation_revert(revert_data))? + } + Err(error) => Err(error).context("call simulation RPC failed")?, + } } fn decode_simulate_handle_ops_revert( @@ -365,7 +380,7 @@ where let mut spoof_ep = spoofed_state.clone(); spoof_ep .account(addr) - .code(ENTRYPOINTSIMULATIONS_BYTECODE.clone()); + .code(ENTRYPOINTSIMULATIONS_DEPLOYED_BYTECODE.clone()); let ep_simulations = EntryPointSimulations::new(addr, Arc::clone(&self.provider)); let contract_error = ep_simulations @@ -388,6 +403,26 @@ impl

EntryPointProvider for EntryPoint

where { } +// Return a human readable string from the revert data +fn decode_simulate_validation_revert(revert_data: Bytes) -> ValidationRevert { + if let Ok(result) = FailedOpWithRevert::decode(&revert_data) { + if let Ok(inner_result) = ContractRevertError::decode(&result.inner) { + ValidationRevert::Operation( + format!("{} : {}", result.reason, inner_result.reason), + Bytes::default(), + ) + } else { + ValidationRevert::Operation(result.reason, result.inner) + } + } else if let Ok(failed_op) = FailedOp::decode(&revert_data) { + ValidationRevert::EntryPoint(failed_op.reason) + } else if let Ok(err) = ContractRevertError::decode(&revert_data) { + ValidationRevert::EntryPoint(err.reason) + } else { + ValidationRevert::Unknown(revert_data) + } +} + fn get_handle_ops_call( entry_point: &IEntryPoint, ops_per_aggregator: Vec>, diff --git a/crates/provider/src/lib.rs b/crates/provider/src/lib.rs index 9501c6dd7..4b98ddcbd 100644 --- a/crates/provider/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -32,8 +32,4 @@ mod traits; pub use traits::test_utils::*; #[cfg(any(test, feature = "test-utils"))] pub use traits::MockProvider; -pub use traits::{ - AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, EntryPointProvider, - ExecutionResult, HandleOpsOut, L1GasProvider, Provider, ProviderError, ProviderResult, - SignatureAggregator, SimulationProvider, -}; +pub use traits::*; diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index e77c03fba..b85be3922 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -14,7 +14,9 @@ use ethers::types::{ spoof, transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, H256, U256, }; -use rundler_types::{GasFees, Timestamp, UserOperation, UserOpsPerAggregator, ValidationOutput}; +use rundler_types::{ + GasFees, Timestamp, UserOperation, UserOpsPerAggregator, ValidationError, ValidationOutput, +}; /// Output of a successful signature aggregator simulation call #[derive(Clone, Debug, Default)] @@ -193,7 +195,7 @@ pub trait SimulationProvider: Send + Sync + 'static { user_op: Self::UO, max_validation_gas: u64, block_hash: Option, - ) -> anyhow::Result; + ) -> Result; /// Call the entry point contract's `simulateHandleOps` function /// with a spoofed state diff --git a/crates/provider/src/traits/mod.rs b/crates/provider/src/traits/mod.rs index be618c09e..73d9a54c0 100644 --- a/crates/provider/src/traits/mod.rs +++ b/crates/provider/src/traits/mod.rs @@ -17,10 +17,7 @@ mod error; pub use error::ProviderError; mod entry_point; -pub use entry_point::{ - AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, EntryPointProvider, - ExecutionResult, HandleOpsOut, L1GasProvider, SignatureAggregator, SimulationProvider, -}; +pub use entry_point::*; mod provider; #[cfg(feature = "test-utils")] diff --git a/crates/provider/src/traits/test_utils.rs b/crates/provider/src/traits/test_utils.rs index 08b90a8c6..28913f643 100644 --- a/crates/provider/src/traits/test_utils.rs +++ b/crates/provider/src/traits/test_utils.rs @@ -14,7 +14,7 @@ use ethers::types::{ spoof, transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, H256, U256, }; -use rundler_types::{v0_6, GasFees, UserOpsPerAggregator, ValidationOutput}; +use rundler_types::{v0_6, GasFees, UserOpsPerAggregator, ValidationError, ValidationOutput}; use crate::{ AggregatorOut, BundleHandler, DepositInfo, EntryPoint, ExecutionResult, HandleOpsOut, @@ -62,7 +62,7 @@ mockall::mock! { user_op: v0_6::UserOperation, max_validation_gas: u64, block_hash: Option - ) -> anyhow::Result; + ) -> Result; async fn call_spoofed_simulate_op( &self, op: v0_6::UserOperation, diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 37f6e2ce8..57a761def 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -45,7 +45,7 @@ impl Settings { } pub(crate) struct EthApi

{ - chain_spec: ChainSpec, + pub(crate) chain_spec: ChainSpec, pool: P, router: EntryPointRouter, } diff --git a/crates/rpc/src/eth/error.rs b/crates/rpc/src/eth/error.rs index 1b5181e2b..42154190c 100644 --- a/crates/rpc/src/eth/error.rs +++ b/crates/rpc/src/eth/error.rs @@ -11,6 +11,8 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. +use std::fmt::Display; + use ethers::types::{Address, Bytes, Opcode, U256}; use jsonrpsee::types::{ error::{CALL_EXECUTION_FAILED_CODE, INTERNAL_ERROR_CODE, INVALID_PARAMS_CODE}, @@ -20,7 +22,7 @@ use rundler_provider::ProviderError; use rundler_sim::GasEstimationError; use rundler_types::{ pool::{MempoolError, PoolError, PrecheckViolation, SimulationViolation}, - Entity, EntityType, Timestamp, + Entity, EntityType, Timestamp, ValidationRevert, }; use serde::Serialize; @@ -117,6 +119,8 @@ pub enum EthRpcError { PrecheckFailed(PrecheckViolation), #[error("validation simulation failed: {0}")] SimulationFailed(SimulationViolation), + #[error("validation reverted: {0}")] + ValidationRevert(ValidationRevertData), #[error("{0}")] ExecutionReverted(String), #[error("execution reverted")] @@ -171,6 +175,25 @@ impl StakeTooLowData { } } +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ValidationRevertData { + reason: Option, + data: Option, +} + +impl Display for ValidationRevertData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(reason) = &self.reason { + write!(f, "{}", reason)?; + } + if let Some(data) = &self.data { + write!(f, " data len: {}", data.len())?; + } + Ok(()) + } +} + #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReplacementUnderpricedData { @@ -302,6 +325,23 @@ impl From for EthRpcError { } SimulationViolation::AggregatorValidationFailed => Self::SignatureCheckFailed, SimulationViolation::OutOfGas(entity) => Self::OutOfGas(entity), + SimulationViolation::ValidationRevert(revert) => { + let data = match revert { + ValidationRevert::EntryPoint(reason) => ValidationRevertData { + reason: Some(reason), + data: None, + }, + ValidationRevert::Operation(reason, data) => ValidationRevertData { + reason: Some(reason), + data: Some(data), + }, + ValidationRevert::Unknown(data) => ValidationRevertData { + reason: None, + data: Some(data), + }, + }; + Self::ValidationRevert(data) + } _ => Self::SimulationFailed(value), } } @@ -351,6 +391,7 @@ impl From for ErrorObjectOwned { EthRpcError::ExecutionRevertedWithBytes(data) => { rpc_err_with_data(EXECUTION_REVERTED, msg, data) } + EthRpcError::ValidationRevert(data) => rpc_err_with_data(EXECUTION_REVERTED, msg, data), EthRpcError::OperationRejected(_) => rpc_err(INVALID_PARAMS_CODE, msg), } } diff --git a/crates/rpc/src/eth/events/common.rs b/crates/rpc/src/eth/events/common.rs new file mode 100644 index 000000000..f0486b3ed --- /dev/null +++ b/crates/rpc/src/eth/events/common.rs @@ -0,0 +1,263 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::{collections::VecDeque, sync::Arc}; + +use anyhow::Context; +use ethers::{ + prelude::EthEvent, + types::{ + Address, Bytes, Filter, GethDebugBuiltInTracerType, GethDebugTracerType, + GethDebugTracingOptions, GethTrace, GethTraceFrame, Log, TransactionReceipt, H256, U256, + }, +}; +use rundler_provider::Provider; +use rundler_types::{UserOperation, UserOperationVariant}; +use rundler_utils::{eth, log::LogOnError}; + +use super::UserOperationEventProvider; +use crate::types::{RpcUserOperationByHash, RpcUserOperationReceipt}; + +#[derive(Debug)] +pub(crate) struct UserOperationEventProviderImpl { + chain_id: u64, + address: Address, + provider: Arc

, + event_block_distance: Option, + _f_type: std::marker::PhantomData, +} + +pub(crate) trait EntryPointFilters: Send + Sync + 'static { + type UO: UserOperation + Into; + type UserOperationEventFilter: EthEvent; + type UserOperationRevertReasonFilter: EthEvent; + + fn construct_receipt( + event: Self::UserOperationEventFilter, + hash: H256, + entry_point: Address, + logs: Vec, + tx_receipt: TransactionReceipt, + ) -> RpcUserOperationReceipt; + + fn get_user_operations_from_tx_data( + tx_data: Bytes, + address: Address, + chain_id: u64, + ) -> Vec; +} + +#[async_trait::async_trait] +impl UserOperationEventProvider for UserOperationEventProviderImpl +where + P: Provider, + F: EntryPointFilters, +{ + async fn get_mined_by_hash( + &self, + hash: H256, + ) -> anyhow::Result> { + // Get event associated with hash (need to check all entry point addresses associated with this API) + let event = self + .get_event_by_hash(hash) + .await + .log_on_error("should have successfully queried for user op events by hash")?; + + let Some(event) = event else { return Ok(None) }; + + // If the event is found, get the TX and entry point + let transaction_hash = event + .transaction_hash + .context("tx_hash should be present")?; + + let tx = self + .provider + .get_transaction(transaction_hash) + .await + .context("should have fetched tx from provider")? + .context("should have found tx")?; + + // We should return null if the tx isn't included in the block yet + if tx.block_hash.is_none() && tx.block_number.is_none() { + return Ok(None); + } + let to = tx + .to + .context("tx.to should be present on transaction containing user operation event")?; + + // Find first op matching the hash + let user_operation = if self.address == to { + F::get_user_operations_from_tx_data(tx.input, self.address, self.chain_id) + .into_iter() + .find(|op| op.hash(to, self.chain_id) == hash) + .context("matching user operation should be found in tx data")? + } else { + self.trace_find_user_operation(transaction_hash, hash) + .await + .context("error running trace")? + .context("should have found user operation in trace")? + }; + + Ok(Some(RpcUserOperationByHash { + user_operation: user_operation.into().into(), + entry_point: event.address.into(), + block_number: Some( + tx.block_number + .map(|n| U256::from(n.as_u64())) + .unwrap_or_default(), + ), + block_hash: Some(tx.block_hash.unwrap_or_default()), + transaction_hash: Some(transaction_hash), + })) + } + + async fn get_receipt(&self, hash: H256) -> anyhow::Result> { + let event = self + .get_event_by_hash(hash) + .await + .log_on_error("should have successfully queried for user op events by hash")?; + let Some(event) = event else { return Ok(None) }; + + let entry_point = event.address; + + let tx_hash = event + .transaction_hash + .context("tx_hash should be present")?; + + // get transaction receipt + let tx_receipt = self + .provider + .get_transaction_receipt(tx_hash) + .await + .context("should have fetched tx receipt")? + .context("Failed to fetch tx receipt")?; + + // filter receipt logs + let filtered_logs = super::filter_receipt_logs_matching_user_op(&event, &tx_receipt) + .context("should have found receipt logs matching user op")?; + + // decode uo event + let uo_event = self + .decode_user_operation_event(event) + .context("should have decoded user operation event")?; + + Ok(Some(F::construct_receipt( + uo_event, + hash, + entry_point, + filtered_logs, + tx_receipt, + ))) + } +} + +impl UserOperationEventProviderImpl +where + P: Provider, + F: EntryPointFilters, +{ + pub(crate) fn new( + chain_id: u64, + address: Address, + provider: Arc

, + event_block_distance: Option, + ) -> Self { + Self { + chain_id, + address, + provider, + event_block_distance, + _f_type: std::marker::PhantomData, + } + } + + async fn get_event_by_hash(&self, hash: H256) -> anyhow::Result> { + let to_block = self.provider.get_block_number().await?; + + let from_block = match self.event_block_distance { + Some(distance) => to_block.saturating_sub(distance), + None => 0, + }; + + let filter = Filter::new() + .address(self.address) + .event(&F::UserOperationEventFilter::abi_signature()) + .from_block(from_block) + .to_block(to_block) + .topic1(hash); + + let logs = self.provider.get_logs(&filter).await?; + Ok(logs.into_iter().next()) + } + + fn decode_user_operation_event(&self, log: Log) -> anyhow::Result { + F::UserOperationEventFilter::decode_log(ð::log_to_raw_log(log)) + .context("log should be a user operation event") + } + + /// This method takes a transaction hash and a user operation hash and returns the full user operation if it exists. + /// This is meant to be used when a user operation event is found in the logs of a transaction, but the top level call + /// wasn't to an entrypoint, so we need to trace the transaction to find the user operation by inspecting each call frame + /// and returning the user operation that matches the hash. + async fn trace_find_user_operation( + &self, + tx_hash: H256, + user_op_hash: H256, + ) -> anyhow::Result> { + // initial call wasn't to an entrypoint, so we need to trace the transaction to find the user operation + let trace_options = GethDebugTracingOptions { + tracer: Some(GethDebugTracerType::BuiltInTracer( + GethDebugBuiltInTracerType::CallTracer, + )), + ..Default::default() + }; + let trace = self + .provider + .debug_trace_transaction(tx_hash, trace_options) + .await + .context("should have fetched trace from provider")?; + + // breadth first search for the user operation in the trace + let mut frame_queue = VecDeque::new(); + + if let GethTrace::Known(GethTraceFrame::CallTracer(call_frame)) = trace { + frame_queue.push_back(call_frame); + } + + while let Some(call_frame) = frame_queue.pop_front() { + // check if the call is to an entrypoint, if not enqueue the child calls if any + if let Some(to) = call_frame + .to + .as_ref() + .and_then(|to| to.as_address()) + .filter(|to| **to == self.address) + { + // check if the user operation is in the call frame + if let Some(uo) = F::get_user_operations_from_tx_data( + call_frame.input, + self.address, + self.chain_id, + ) + .into_iter() + .find(|op| op.hash(*to, self.chain_id) == user_op_hash) + { + return Ok(Some(uo)); + } + } else if let Some(calls) = call_frame.calls { + frame_queue.extend(calls) + } + } + + Ok(None) + } +} diff --git a/crates/rpc/src/eth/events/mod.rs b/crates/rpc/src/eth/events/mod.rs index 3ef20a9f5..eba1f184f 100644 --- a/crates/rpc/src/eth/events/mod.rs +++ b/crates/rpc/src/eth/events/mod.rs @@ -16,9 +16,12 @@ use ethers::types::{Log, TransactionReceipt, H256}; use crate::types::{RpcUserOperationByHash, RpcUserOperationReceipt}; +mod common; + mod v0_6; pub(crate) use v0_6::UserOperationEventProviderV0_6; mod v0_7; +pub(crate) use v0_7::UserOperationEventProviderV0_7; #[async_trait::async_trait] pub(crate) trait UserOperationEventProvider: Send + Sync + 'static { diff --git a/crates/rpc/src/eth/events/v0_6.rs b/crates/rpc/src/eth/events/v0_6.rs index d2efdacd8..803e436c4 100644 --- a/crates/rpc/src/eth/events/v0_6.rs +++ b/crates/rpc/src/eth/events/v0_6.rs @@ -11,188 +11,79 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use std::{collections::VecDeque, sync::Arc}; - -use anyhow::Context; use ethers::{ abi::{AbiDecode, RawLog}, prelude::EthEvent, - types::{ - Address, Bytes, Filter, GethDebugBuiltInTracerType, GethDebugTracerType, - GethDebugTracingOptions, GethTrace, GethTraceFrame, Log, H256, U256, - }, + types::{Address, Bytes, Log, TransactionReceipt, H256}, }; -use rundler_provider::Provider; use rundler_types::{ contracts::v0_6::i_entry_point::{ IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, }, v0_6::UserOperation, - UserOperation as UserOperationTrait, UserOperationVariant, }; -use rundler_utils::{eth, log::LogOnError}; - -use super::UserOperationEventProvider; -use crate::types::{RpcUserOperationByHash, RpcUserOperationReceipt}; - -#[derive(Debug)] -pub(crate) struct UserOperationEventProviderV0_6 { - chain_id: u64, - address: Address, - provider: Arc

, - event_block_distance: Option, -} - -#[async_trait::async_trait] -impl UserOperationEventProvider for UserOperationEventProviderV0_6

{ - async fn get_mined_by_hash( - &self, - hash: H256, - ) -> anyhow::Result> { - // Get event associated with hash (need to check all entry point addresses associated with this API) - let event = self - .get_event_by_hash(hash) - .await - .log_on_error("should have successfully queried for user op events by hash")?; - - let Some(event) = event else { return Ok(None) }; - - // If the event is found, get the TX and entry point - let transaction_hash = event - .transaction_hash - .context("tx_hash should be present")?; - - let tx = self - .provider - .get_transaction(transaction_hash) - .await - .context("should have fetched tx from provider")? - .context("should have found tx")?; - - // We should return null if the tx isn't included in the block yet - if tx.block_hash.is_none() && tx.block_number.is_none() { - return Ok(None); - } - let to = tx - .to - .context("tx.to should be present on transaction containing user operation event")?; - // Find first op matching the hash - let user_operation = if self.address == to { - self.get_user_operations_from_tx_data(tx.input) - .into_iter() - .find(|op| op.hash(to, self.chain_id) == hash) - .context("matching user operation should be found in tx data")? - } else { - self.trace_find_user_operation(transaction_hash, hash) - .await - .context("error running trace")? - .context("should have found user operation in trace")? - }; - - Ok(Some(RpcUserOperationByHash { - user_operation: UserOperationVariant::from(user_operation).into(), - entry_point: event.address.into(), - block_number: Some( - tx.block_number - .map(|n| U256::from(n.as_u64())) - .unwrap_or_default(), - ), - block_hash: Some(tx.block_hash.unwrap_or_default()), - transaction_hash: Some(transaction_hash), - })) - } - - async fn get_receipt(&self, hash: H256) -> anyhow::Result> { - let event = self - .get_event_by_hash(hash) - .await - .log_on_error("should have successfully queried for user op events by hash")?; - let Some(event) = event else { return Ok(None) }; - - let entry_point = event.address; +use super::common::{EntryPointFilters, UserOperationEventProviderImpl}; +use crate::types::RpcUserOperationReceipt; - let tx_hash = event - .transaction_hash - .context("tx_hash should be present")?; +pub(crate) type UserOperationEventProviderV0_6

= + UserOperationEventProviderImpl; - // get transaction receipt - let tx_receipt = self - .provider - .get_transaction_receipt(tx_hash) - .await - .context("should have fetched tx receipt")? - .context("Failed to fetch tx receipt")?; +pub(crate) struct EntryPointFiltersV0_6; - // filter receipt logs - let filtered_logs = super::filter_receipt_logs_matching_user_op(&event, &tx_receipt) - .context("should have found receipt logs matching user op")?; - - // decode uo event - let uo_event = self - .decode_user_operation_event(event) - .context("should have decoded user operation event")?; +impl EntryPointFilters for EntryPointFiltersV0_6 { + type UO = UserOperation; + type UserOperationEventFilter = UserOperationEventFilter; + type UserOperationRevertReasonFilter = UserOperationRevertReasonFilter; + fn construct_receipt( + event: Self::UserOperationEventFilter, + hash: H256, + entry_point: Address, + logs: Vec, + tx_receipt: TransactionReceipt, + ) -> RpcUserOperationReceipt { // get failure reason - let reason: String = if uo_event.success { + let reason: String = if event.success { "".to_owned() } else { - Self::get_failure_reason(&tx_receipt.logs, hash) - .context("should have found revert reason if tx wasn't successful")? + let revert_reason_evt: Option = logs + .iter() + .filter(|l| l.topics.len() > 1 && l.topics[1] == hash) + .map_while(|l| { + Self::UserOperationRevertReasonFilter::decode_log(&RawLog { + topics: l.topics.clone(), + data: l.data.to_vec(), + }) + .ok() + }) + .next(); + + revert_reason_evt + .map(|r| r.revert_reason.to_string()) .unwrap_or_default() }; - Ok(Some(RpcUserOperationReceipt { + RpcUserOperationReceipt { user_op_hash: hash, entry_point: entry_point.into(), - sender: uo_event.sender.into(), - nonce: uo_event.nonce, - paymaster: uo_event.paymaster.into(), - actual_gas_cost: uo_event.actual_gas_cost, - actual_gas_used: uo_event.actual_gas_used, - success: uo_event.success, - logs: filtered_logs, + sender: event.sender.into(), + nonce: event.nonce, + paymaster: event.paymaster.into(), + actual_gas_cost: event.actual_gas_cost, + actual_gas_used: event.actual_gas_used, + success: event.success, + logs, receipt: tx_receipt, reason, - })) - } -} - -impl UserOperationEventProviderV0_6

{ - pub(crate) fn new( - chain_id: u64, - address: Address, - provider: Arc

, - event_block_distance: Option, - ) -> Self { - Self { - chain_id, - address, - provider, - event_block_distance, } } - async fn get_event_by_hash(&self, hash: H256) -> anyhow::Result> { - let to_block = self.provider.get_block_number().await?; - - let from_block = match self.event_block_distance { - Some(distance) => to_block.saturating_sub(distance), - None => 0, - }; - - let filter = Filter::new() - .address(self.address) - .event(&UserOperationEventFilter::abi_signature()) - .from_block(from_block) - .to_block(to_block) - .topic1(hash); - - let logs = self.provider.get_logs(&filter).await?; - Ok(logs.into_iter().next()) - } - - fn get_user_operations_from_tx_data(&self, tx_data: Bytes) -> Vec { + fn get_user_operations_from_tx_data( + tx_data: Bytes, + _address: Address, + _chain_id: u64, + ) -> Vec { let entry_point_calls = match IEntryPointCalls::decode(tx_data) { Ok(entry_point_calls) => entry_point_calls, Err(_) => return vec![], @@ -210,78 +101,4 @@ impl UserOperationEventProviderV0_6

{ _ => vec![], } } - - fn decode_user_operation_event(&self, log: Log) -> anyhow::Result { - UserOperationEventFilter::decode_log(ð::log_to_raw_log(log)) - .context("log should be a user operation event") - } - - /// This method takes a transaction hash and a user operation hash and returns the full user operation if it exists. - /// This is meant to be used when a user operation event is found in the logs of a transaction, but the top level call - /// wasn't to an entrypoint, so we need to trace the transaction to find the user operation by inspecting each call frame - /// and returning the user operation that matches the hash. - async fn trace_find_user_operation( - &self, - tx_hash: H256, - user_op_hash: H256, - ) -> anyhow::Result> { - // initial call wasn't to an entrypoint, so we need to trace the transaction to find the user operation - let trace_options = GethDebugTracingOptions { - tracer: Some(GethDebugTracerType::BuiltInTracer( - GethDebugBuiltInTracerType::CallTracer, - )), - ..Default::default() - }; - let trace = self - .provider - .debug_trace_transaction(tx_hash, trace_options) - .await - .context("should have fetched trace from provider")?; - - // breadth first search for the user operation in the trace - let mut frame_queue = VecDeque::new(); - - if let GethTrace::Known(GethTraceFrame::CallTracer(call_frame)) = trace { - frame_queue.push_back(call_frame); - } - - while let Some(call_frame) = frame_queue.pop_front() { - // check if the call is to an entrypoint, if not enqueue the child calls if any - if let Some(to) = call_frame - .to - .as_ref() - .and_then(|to| to.as_address()) - .filter(|to| **to == self.address) - { - // check if the user operation is in the call frame - if let Some(uo) = self - .get_user_operations_from_tx_data(call_frame.input) - .into_iter() - .find(|op| op.hash(*to, self.chain_id) == user_op_hash) - { - return Ok(Some(uo)); - } - } else if let Some(calls) = call_frame.calls { - frame_queue.extend(calls) - } - } - - Ok(None) - } - - fn get_failure_reason(logs: &[Log], hash: H256) -> anyhow::Result> { - let revert_reason_evt: Option = logs - .iter() - .filter(|l| l.topics.len() > 1 && l.topics[1] == hash) - .map_while(|l| { - UserOperationRevertReasonFilter::decode_log(&RawLog { - topics: l.topics.clone(), - data: l.data.to_vec(), - }) - .ok() - }) - .next(); - - Ok(revert_reason_evt.map(|r| r.revert_reason.to_string())) - } } diff --git a/crates/rpc/src/eth/events/v0_7.rs b/crates/rpc/src/eth/events/v0_7.rs index f892b64bb..ffa1fbeab 100644 --- a/crates/rpc/src/eth/events/v0_7.rs +++ b/crates/rpc/src/eth/events/v0_7.rs @@ -11,24 +11,102 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use ethers::types::H256; +use ethers::{ + abi::{AbiDecode, RawLog}, + prelude::EthEvent, + types::{Address, Bytes, Log, TransactionReceipt, H256}, +}; +use rundler_types::{ + contracts::v0_7::i_entry_point::{ + IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, + }, + v0_7::UserOperation, +}; -use super::UserOperationEventProvider; -use crate::types::{RpcUserOperationByHash, RpcUserOperationReceipt}; +use super::common::{EntryPointFilters, UserOperationEventProviderImpl}; +use crate::types::RpcUserOperationReceipt; -#[derive(Debug)] -pub(crate) struct UserOperationEventProviderV0_7; +pub(crate) type UserOperationEventProviderV0_7

= + UserOperationEventProviderImpl; -#[async_trait::async_trait] -impl UserOperationEventProvider for UserOperationEventProviderV0_7 { - async fn get_mined_by_hash( - &self, - _hash: H256, - ) -> anyhow::Result> { - unimplemented!() +pub(crate) struct EntryPointFiltersV0_7; + +impl EntryPointFilters for EntryPointFiltersV0_7 { + type UO = UserOperation; + type UserOperationEventFilter = UserOperationEventFilter; + type UserOperationRevertReasonFilter = UserOperationRevertReasonFilter; + + fn construct_receipt( + event: Self::UserOperationEventFilter, + hash: H256, + entry_point: Address, + logs: Vec, + tx_receipt: TransactionReceipt, + ) -> RpcUserOperationReceipt { + // get failure reason + let reason: String = if event.success { + "".to_owned() + } else { + let revert_reason_evt: Option = logs + .iter() + .filter(|l| l.topics.len() > 1 && l.topics[1] == hash) + .map_while(|l| { + Self::UserOperationRevertReasonFilter::decode_log(&RawLog { + topics: l.topics.clone(), + data: l.data.to_vec(), + }) + .ok() + }) + .next(); + + revert_reason_evt + .map(|r| r.revert_reason.to_string()) + .unwrap_or_default() + }; + + RpcUserOperationReceipt { + user_op_hash: hash, + entry_point: entry_point.into(), + sender: event.sender.into(), + nonce: event.nonce, + paymaster: event.paymaster.into(), + actual_gas_cost: event.actual_gas_cost, + actual_gas_used: event.actual_gas_used, + success: event.success, + logs, + receipt: tx_receipt, + reason, + } } - async fn get_receipt(&self, _hash: H256) -> anyhow::Result> { - unimplemented!() + fn get_user_operations_from_tx_data( + tx_data: Bytes, + address: Address, + chain_id: u64, + ) -> Vec { + let entry_point_calls = match IEntryPointCalls::decode(tx_data) { + Ok(entry_point_calls) => entry_point_calls, + Err(_) => return vec![], + }; + + match entry_point_calls { + IEntryPointCalls::HandleOps(handle_ops_call) => handle_ops_call + .ops + .into_iter() + .map(|op| op.unpack(address, chain_id)) + .collect(), + IEntryPointCalls::HandleAggregatedOps(handle_aggregated_ops_call) => { + handle_aggregated_ops_call + .ops_per_aggregator + .into_iter() + .flat_map(|ops| { + ops.user_ops + .into_iter() + .map(|op| op.unpack(address, chain_id)) + }) + .collect() + } + _ => vec![], + } } } diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index dfd9b4f71..d0751050a 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -21,7 +21,7 @@ pub(crate) use router::*; mod error; pub(crate) use error::EthRpcError; mod events; -pub(crate) use events::UserOperationEventProviderV0_6; +pub(crate) use events::{UserOperationEventProviderV0_6, UserOperationEventProviderV0_7}; mod server; use ethers::types::{spoof, Address, H256, U64}; diff --git a/crates/rpc/src/eth/router.rs b/crates/rpc/src/eth/router.rs index 982d27f49..2743762cc 100644 --- a/crates/rpc/src/eth/router.rs +++ b/crates/rpc/src/eth/router.rs @@ -50,7 +50,7 @@ impl EntryPointRouterBuilder { self } - pub(crate) fn _v0_7(mut self, route: R) -> Self + pub(crate) fn v0_7(mut self, route: R) -> Self where R: EntryPointRoute, { @@ -186,7 +186,8 @@ impl EntryPointRouter { if addr == *entry_point { return Ok(EntryPointVersion::V0_6); } - } else if let Some((addr, _)) = self.v0_7 { + } + if let Some((addr, _)) = self.v0_7 { if addr == *entry_point { return Ok(EntryPointVersion::V0_7); } diff --git a/crates/rpc/src/eth/server.rs b/crates/rpc/src/eth/server.rs index e07736825..6f5576f49 100644 --- a/crates/rpc/src/eth/server.rs +++ b/crates/rpc/src/eth/server.rs @@ -13,11 +13,11 @@ use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::core::RpcResult; -use rundler_types::pool::Pool; +use rundler_types::{pool::Pool, UserOperationVariant}; use super::{api::EthApi, EthApiServer}; use crate::types::{ - RpcGasEstimate, RpcUserOperation, RpcUserOperationByHash, RpcUserOperationOptionalGas, + FromRpc, RpcGasEstimate, RpcUserOperation, RpcUserOperationByHash, RpcUserOperationOptionalGas, RpcUserOperationReceipt, }; @@ -31,7 +31,12 @@ where op: RpcUserOperation, entry_point: Address, ) -> RpcResult { - Ok(EthApi::send_user_operation(self, op.into(), entry_point).await?) + Ok(EthApi::send_user_operation( + self, + UserOperationVariant::from_rpc(op, entry_point, self.chain_spec.id), + entry_point, + ) + .await?) } async fn estimate_user_operation_gas( diff --git a/crates/rpc/src/rundler.rs b/crates/rpc/src/rundler.rs index 953c81741..2bf0cd05e 100644 --- a/crates/rpc/src/rundler.rs +++ b/crates/rpc/src/rundler.rs @@ -27,7 +27,7 @@ use rundler_types::{chain::ChainSpec, pool::Pool, UserOperation, UserOperationVa use crate::{ error::rpc_err, eth::{EntryPointRouter, EthRpcError}, - types::RpcUserOperation, + types::{FromRpc, RpcUserOperation}, }; /// Settings for the `rundler_` API @@ -65,6 +65,7 @@ pub trait RundlerApi { } pub(crate) struct RundlerApi { + chain_spec: ChainSpec, settings: Settings, fee_estimator: FeeEstimator

, pool_server: PL, @@ -84,6 +85,7 @@ where settings: Settings, ) -> Self { Self { + chain_spec: chain_spec.clone(), settings, fee_estimator: FeeEstimator::new( chain_spec, @@ -120,7 +122,7 @@ where user_op: RpcUserOperation, entry_point: Address, ) -> RpcResult> { - let uo = UserOperationVariant::from(user_op); + let uo = UserOperationVariant::from_rpc(user_op, entry_point, self.chain_spec.id); let id = uo.id(); if uo.pre_verification_gas() != U256::zero() diff --git a/crates/rpc/src/task.rs b/crates/rpc/src/task.rs index 2962ca2bf..1ce33b418 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -20,8 +20,10 @@ use jsonrpsee::{ server::{middleware::ProxyGetRequestLayer, ServerBuilder}, RpcModule, }; -use rundler_provider::EthersEntryPointV0_6; -use rundler_sim::{EstimationSettings, FeeEstimator, GasEstimatorV0_6, PrecheckSettings}; +use rundler_provider::{EthersEntryPointV0_6, EthersEntryPointV0_7}; +use rundler_sim::{ + EstimationSettings, FeeEstimator, GasEstimatorV0_6, GasEstimatorV0_7, PrecheckSettings, +}; use rundler_task::{ server::{format_socket_addr, HealthCheck}, Task, @@ -35,7 +37,7 @@ use crate::{ debug::{DebugApi, DebugApiServer}, eth::{ EntryPointRouteImpl, EntryPointRouter, EntryPointRouterBuilder, EthApi, EthApiServer, - EthApiSettings, UserOperationEventProviderV0_6, + EthApiSettings, UserOperationEventProviderV0_6, UserOperationEventProviderV0_7, }, health::{HealthChecker, SystemApiServer}, metrics::RpcMetricsLogger, @@ -70,6 +72,10 @@ pub struct Args { pub rpc_timeout: Duration, /// Max number of connections. pub max_connections: u32, + /// Whether to enable entry point v0.6. + pub entry_point_v0_6_enabled: bool, + /// Whether to enable entry point v0.7. + pub entry_point_v0_7_enabled: bool, } /// JSON-RPC server task. @@ -91,18 +97,23 @@ where tracing::info!("Starting rpc server on {}", addr); let provider = rundler_provider::new_provider(&self.args.rpc_url, None)?; - let ep = - EthersEntryPointV0_6::new(self.args.chain_spec.entry_point_address, provider.clone()); + let ep_v0_6 = EthersEntryPointV0_6::new( + self.args.chain_spec.entry_point_address_v0_6, + provider.clone(), + ); + let ep_v0_7 = EthersEntryPointV0_7::new( + self.args.chain_spec.entry_point_address_v0_7, + provider.clone(), + ); - // create the entry point router - // TODO(danc) create 0.7 route, requires 0.7 estimator and 0.7 event provider - let router = EntryPointRouterBuilder::default() - .v0_6(EntryPointRouteImpl::new( - ep.clone(), + let mut router_builder = EntryPointRouterBuilder::default(); + if self.args.entry_point_v0_6_enabled { + router_builder = router_builder.v0_6(EntryPointRouteImpl::new( + ep_v0_6.clone(), GasEstimatorV0_6::new( self.args.chain_spec.clone(), provider.clone(), - ep, + ep_v0_6, self.args.estimation_settings, FeeEstimator::new( &self.args.chain_spec, @@ -115,14 +126,44 @@ where ), UserOperationEventProviderV0_6::new( self.args.chain_spec.id, - self.args.chain_spec.entry_point_address, + self.args.chain_spec.entry_point_address_v0_6, + provider.clone(), + self.args + .eth_api_settings + .user_operation_event_block_distance, + ), + )); + } + + if self.args.entry_point_v0_7_enabled { + router_builder = router_builder.v0_7(EntryPointRouteImpl::new( + ep_v0_7.clone(), + GasEstimatorV0_7::new( + self.args.chain_spec.clone(), + ep_v0_7, + self.args.estimation_settings, + FeeEstimator::new( + &self.args.chain_spec, + Arc::clone(&provider), + self.args.precheck_settings.priority_fee_mode, + self.args + .precheck_settings + .bundle_priority_fee_overhead_percent, + ), + ), + UserOperationEventProviderV0_7::new( + self.args.chain_spec.id, + self.args.chain_spec.entry_point_address_v0_7, provider.clone(), self.args .eth_api_settings .user_operation_event_block_distance, ), - )) - .build(); + )); + } + + // create the entry point router + let router = router_builder.build(); let mut module = RpcModule::new(()); self.attach_namespaces(provider, router, &mut module)?; diff --git a/crates/rpc/src/types/mod.rs b/crates/rpc/src/types/mod.rs index c97bf3017..235a0a92e 100644 --- a/crates/rpc/src/types/mod.rs +++ b/crates/rpc/src/types/mod.rs @@ -17,6 +17,8 @@ use ethers::{ }; use rundler_types::{ pool::{Reputation, ReputationStatus}, + v0_6::UserOperation as UserOperationV0_6, + v0_7::UserOperation as UserOperationV0_7, GasEstimate, UserOperationOptionalGas, UserOperationVariant, }; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -42,6 +44,11 @@ pub enum ApiNamespace { Admin, } +/// Conversion trait for RPC types adding the context of the entry point and chain id +pub(crate) trait FromRpc { + fn from_rpc(rpc: R, entry_point: Address, chain_id: u64) -> Self; +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct RpcAddress(H160); @@ -108,11 +115,15 @@ impl From for RpcUserOperation { } } -impl From for UserOperationVariant { - fn from(op: RpcUserOperation) -> Self { +impl FromRpc for UserOperationVariant { + fn from_rpc(op: RpcUserOperation, entry_point: Address, chain_id: u64) -> Self { match op { - RpcUserOperation::V0_6(op) => UserOperationVariant::V0_6(op.into()), - RpcUserOperation::V0_7(op) => UserOperationVariant::V0_7(op.into()), + RpcUserOperation::V0_6(op) => { + UserOperationVariant::V0_6(UserOperationV0_6::from_rpc(op, entry_point, chain_id)) + } + RpcUserOperation::V0_7(op) => { + UserOperationVariant::V0_7(UserOperationV0_7::from_rpc(op, entry_point, chain_id)) + } } } } diff --git a/crates/rpc/src/types/v0_6.rs b/crates/rpc/src/types/v0_6.rs index 1c764fc86..3c1958304 100644 --- a/crates/rpc/src/types/v0_6.rs +++ b/crates/rpc/src/types/v0_6.rs @@ -15,7 +15,7 @@ use ethers::types::{Address, Bytes, U256}; use rundler_types::v0_6::{UserOperation, UserOperationOptionalGas}; use serde::{Deserialize, Serialize}; -use super::RpcAddress; +use super::{FromRpc, RpcAddress}; /// User operation definition for RPC #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] @@ -52,8 +52,8 @@ impl From for RpcUserOperation { } } -impl From for UserOperation { - fn from(def: RpcUserOperation) -> Self { +impl FromRpc for UserOperation { + fn from_rpc(def: RpcUserOperation, _entry_point: Address, _chain_id: u64) -> Self { UserOperation { sender: def.sender.into(), nonce: def.nonce, diff --git a/crates/rpc/src/types/v0_7.rs b/crates/rpc/src/types/v0_7.rs index 8fcb53d83..17162347e 100644 --- a/crates/rpc/src/types/v0_7.rs +++ b/crates/rpc/src/types/v0_7.rs @@ -12,10 +12,12 @@ // If not, see https://www.gnu.org/licenses/. use ethers::types::{Address, Bytes, H256, U128, U256}; -use rundler_types::v0_7::{UserOperation, UserOperationOptionalGas}; +use rundler_types::v0_7::{ + UserOperation, UserOperationBuilder, UserOperationOptionalGas, UserOperationRequiredFields, +}; use serde::{Deserialize, Serialize}; -use super::RpcAddress; +use super::{FromRpc, RpcAddress}; /// User operation definition for RPC #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] @@ -39,14 +41,57 @@ pub(crate) struct RpcUserOperation { } impl From for RpcUserOperation { - fn from(_op: UserOperation) -> Self { - todo!() + fn from(op: UserOperation) -> Self { + RpcUserOperation { + sender: op.sender, + nonce: op.nonce, + call_data: op.call_data, + call_gas_limit: op.call_gas_limit, + verification_gas_limit: op.verification_gas_limit, + pre_verification_gas: op.pre_verification_gas, + max_priority_fee_per_gas: op.max_priority_fee_per_gas, + max_fee_per_gas: op.max_fee_per_gas, + factory: op.factory, + factory_data: Some(op.factory_data), + paymaster: op.paymaster, + paymaster_verification_gas_limit: Some(op.paymaster_verification_gas_limit), + paymaster_post_op_gas_limit: Some(op.paymaster_post_op_gas_limit), + paymaster_data: Some(op.paymaster_data), + signature: op.signature, + } } } -impl From for UserOperation { - fn from(_def: RpcUserOperation) -> Self { - todo!() +impl FromRpc for UserOperation { + fn from_rpc(def: RpcUserOperation, entry_point: Address, chain_id: u64) -> Self { + let mut builder = UserOperationBuilder::new( + entry_point, + chain_id, + UserOperationRequiredFields { + sender: def.sender, + nonce: def.nonce, + call_data: def.call_data, + call_gas_limit: def.call_gas_limit, + verification_gas_limit: def.verification_gas_limit, + pre_verification_gas: def.pre_verification_gas, + max_priority_fee_per_gas: def.max_priority_fee_per_gas, + max_fee_per_gas: def.max_fee_per_gas, + signature: def.signature, + }, + ); + if def.paymaster.is_some() { + builder = builder.paymaster( + def.paymaster.unwrap(), + def.paymaster_verification_gas_limit.unwrap_or_default(), + def.paymaster_post_op_gas_limit.unwrap_or_default(), + def.paymaster_data.unwrap_or_default(), + ); + } + if def.factory.is_some() { + builder = builder.factory(def.factory.unwrap(), def.factory_data.unwrap_or_default()); + } + + builder.build() } } @@ -82,7 +127,23 @@ pub(crate) struct RpcUserOperationOptionalGas { } impl From for UserOperationOptionalGas { - fn from(_def: RpcUserOperationOptionalGas) -> Self { - todo!() + fn from(def: RpcUserOperationOptionalGas) -> Self { + UserOperationOptionalGas { + sender: def.sender, + nonce: def.nonce, + call_data: def.call_data, + call_gas_limit: def.call_gas_limit, + verification_gas_limit: def.verification_gas_limit, + pre_verification_gas: def.pre_verification_gas, + max_priority_fee_per_gas: def.max_priority_fee_per_gas, + max_fee_per_gas: def.max_fee_per_gas, + factory: def.factory, + factory_data: def.factory_data.unwrap_or_default(), + paymaster: def.paymaster, + paymaster_verification_gas_limit: def.paymaster_verification_gas_limit, + paymaster_post_op_gas_limit: def.paymaster_post_op_gas_limit, + paymaster_data: def.paymaster_data.unwrap_or_default(), + signature: def.signature, + } } } diff --git a/crates/sim/src/estimation/v0_7.rs b/crates/sim/src/estimation/v0_7.rs index a4a2b9646..41df315c6 100644 --- a/crates/sim/src/estimation/v0_7.rs +++ b/crates/sim/src/estimation/v0_7.rs @@ -11,26 +11,99 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use ethers::types::spoof; -use rundler_types::{v0_7::UserOperationOptionalGas, GasEstimate}; +use std::cmp; -use super::GasEstimationError; +use ethers::types::{spoof, U256}; +use rundler_provider::{EntryPoint, L1GasProvider, Provider, SimulationProvider}; +use rundler_types::{ + chain::ChainSpec, + v0_7::{UserOperation, UserOperationOptionalGas}, + GasEstimate, +}; + +use super::{GasEstimationError, Settings}; +use crate::{gas, FeeEstimator}; /// Gas estimator for entry point v0.7 #[derive(Debug)] -pub struct GasEstimator {} +pub struct GasEstimator { + chain_spec: ChainSpec, + entry_point: E, + _settings: Settings, + fee_estimator: FeeEstimator

, +} #[async_trait::async_trait] -impl super::GasEstimator for GasEstimator { +impl super::GasEstimator for GasEstimator +where + P: Provider, + E: EntryPoint + SimulationProvider + L1GasProvider, +{ type UserOperationOptionalGas = UserOperationOptionalGas; /// Returns a gas estimate or a revert message, or an anyhow error on any /// other error. async fn estimate_op_gas( &self, - _op: UserOperationOptionalGas, + op: UserOperationOptionalGas, _state_override: spoof::State, ) -> Result { - unimplemented!() + // TODO(danc): Implement this for real + + // Estimate pre verification gas at the current fees + // If the user provides fees, use them, otherwise use the current bundle fees + let (bundle_fees, base_fee) = self.fee_estimator.required_bundle_fees(None).await?; + let gas_price = if let (Some(max_fee), Some(prio_fee)) = + (op.max_fee_per_gas, op.max_priority_fee_per_gas) + { + cmp::min(U256::from(max_fee), base_fee + prio_fee) + } else { + base_fee + bundle_fees.max_priority_fee_per_gas + }; + let pre_verification_gas = self.estimate_pre_verification_gas(&op, gas_price).await?; + + Ok(GasEstimate { + pre_verification_gas, + call_gas_limit: 1_000_000.into(), + verification_gas_limit: 1_000_000.into(), + paymaster_verification_gas_limit: op.paymaster.map(|_| 1_000_000.into()), + paymaster_post_op_gas_limit: op.paymaster.map(|_| 1_000_000.into()), + }) + } +} + +impl GasEstimator +where + P: Provider, + E: EntryPoint + SimulationProvider + L1GasProvider, +{ + /// Create a new gas estimator + pub fn new( + chain_spec: ChainSpec, + entry_point: E, + settings: Settings, + fee_estimator: FeeEstimator

, + ) -> Self { + Self { + chain_spec, + entry_point, + _settings: settings, + fee_estimator, + } + } + + async fn estimate_pre_verification_gas( + &self, + op: &UserOperationOptionalGas, + gas_price: U256, + ) -> Result { + Ok(gas::estimate_pre_verification_gas( + &self.chain_spec, + &self.entry_point, + &op.max_fill(self.entry_point.address(), self.chain_spec.id), + &op.random_fill(self.entry_point.address(), self.chain_spec.id), + gas_price, + ) + .await?) } } diff --git a/crates/sim/src/gas/gas.rs b/crates/sim/src/gas/gas.rs index 8652a2a2a..b57d164e6 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.rs @@ -15,7 +15,7 @@ use std::{cmp, fmt::Debug, sync::Arc}; use anyhow::Context; use ethers::types::U256; -use rundler_provider::{L1GasProvider, Provider}; +use rundler_provider::{EntryPoint, L1GasProvider, Provider}; use rundler_types::{ chain::{self, ChainSpec, L1GasOracleContractType}, GasFees, UserOperation, @@ -40,9 +40,12 @@ use super::oracle::{ /// /// Networks that require dynamic pre_verification_gas are typically those that charge extra calldata fees /// that can scale based on dynamic gas prices. -pub async fn estimate_pre_verification_gas>( +pub async fn estimate_pre_verification_gas< + UO: UserOperation, + E: EntryPoint + L1GasProvider, +>( chain_spec: &ChainSpec, - enty_point: &E, + entry_point: &E, full_op: &UO, random_op: &UO, gas_price: U256, @@ -55,13 +58,13 @@ pub async fn estimate_pre_verification_gas panic!("Chain spec requires calldata pre_verification_gas but no l1_gas_oracle_contract_type is set"), L1GasOracleContractType::ArbitrumNitro => { - enty_point - .calc_arbitrum_l1_gas(chain_spec.entry_point_address, random_op.clone()) + entry_point + .calc_arbitrum_l1_gas(entry_point.address(), random_op.clone()) .await? }, L1GasOracleContractType::OptimismBedrock => { - enty_point - .calc_optimism_l1_gas(chain_spec.entry_point_address, random_op.clone(), gas_price) + entry_point + .calc_optimism_l1_gas(entry_point.address(), random_op.clone(), gas_price) .await? }, }; @@ -72,7 +75,10 @@ pub async fn estimate_pre_verification_gas>( +pub async fn calc_required_pre_verification_gas< + UO: UserOperation, + E: EntryPoint + L1GasProvider, +>( chain_spec: &ChainSpec, entry_point: &E, op: &UO, @@ -87,14 +93,14 @@ pub async fn calc_required_pre_verification_gas panic!("Chain spec requires calldata pre_verification_gas but no l1_gas_oracle_contract_type is set"), L1GasOracleContractType::ArbitrumNitro => { entry_point - .calc_arbitrum_l1_gas(chain_spec.entry_point_address, op.clone()) + .calc_arbitrum_l1_gas(entry_point.address(), op.clone()) .await? }, L1GasOracleContractType::OptimismBedrock => { let gas_price = cmp::min(base_fee + op.max_priority_fee_per_gas(), op.max_fee_per_gas()); entry_point - .calc_optimism_l1_gas(chain_spec.entry_point_address, op.clone(), gas_price) + .calc_optimism_l1_gas(entry_point.address(), op.clone(), gas_price) .await? }, }; diff --git a/crates/sim/src/simulation/mempool.rs b/crates/sim/src/simulation/mempool.rs index 9c64b8316..829f6a87d 100644 --- a/crates/sim/src/simulation/mempool.rs +++ b/crates/sim/src/simulation/mempool.rs @@ -25,10 +25,20 @@ use crate::simulation::SimulationViolation; /// Typically read from a JSON file using the `Deserialize` trait. #[derive(Debug, Clone, Deserialize, Default)] pub struct MempoolConfig { + /// Entry point address this mempool is associated with. + #[serde(rename = "camelCase")] + pub(crate) entry_point: Address, /// Allowlist to match violations against. pub(crate) allowlist: Vec, } +impl MempoolConfig { + /// Return the entrypoint address this mempool is associated with + pub fn entry_point(&self) -> Address { + self.entry_point + } +} + /// The entity allowed by an allowlist entry. #[derive(Debug, Copy, Clone)] pub(crate) enum AllowEntity { @@ -472,6 +482,7 @@ mod tests { ( H256::random(), MempoolConfig { + entry_point: Address::random(), allowlist: vec![AllowlistEntry::new( AllowEntity::Type(EntityType::Account), AllowRule::ForbiddenOpcode { @@ -504,6 +515,7 @@ mod tests { ( H256::random(), MempoolConfig { + entry_point: Address::random(), allowlist: vec![AllowlistEntry::new( AllowEntity::Type(EntityType::Account), AllowRule::ForbiddenOpcode { @@ -548,6 +560,7 @@ mod tests { ( mempool1, MempoolConfig { + entry_point: Address::random(), allowlist: vec![AllowlistEntry::new( AllowEntity::Type(EntityType::Account), AllowRule::ForbiddenOpcode { @@ -583,6 +596,7 @@ mod tests { ( mempool1, MempoolConfig { + entry_point: Address::random(), allowlist: vec![ AllowlistEntry::new( AllowEntity::Type(EntityType::Account), @@ -604,6 +618,7 @@ mod tests { ( mempool2, MempoolConfig { + entry_point: Address::random(), allowlist: vec![ AllowlistEntry::new( AllowEntity::Type(EntityType::Account), diff --git a/crates/sim/src/simulation/mod.rs b/crates/sim/src/simulation/mod.rs index 42d13bde9..6c49730c6 100644 --- a/crates/sim/src/simulation/mod.rs +++ b/crates/sim/src/simulation/mod.rs @@ -31,6 +31,9 @@ pub mod v0_6; mod mempool; pub use mempool::MempoolConfig; +mod unsafe_sim; +pub use unsafe_sim::UnsafeSimulator; + use crate::{ExpectedStorage, ViolationError}; /// The result of a successful simulation diff --git a/crates/sim/src/simulation/v0_6/unsafe_sim.rs b/crates/sim/src/simulation/unsafe_sim.rs similarity index 77% rename from crates/sim/src/simulation/v0_6/unsafe_sim.rs rename to crates/sim/src/simulation/unsafe_sim.rs index 032519e02..03f7169f0 100644 --- a/crates/sim/src/simulation/v0_6/unsafe_sim.rs +++ b/crates/sim/src/simulation/unsafe_sim.rs @@ -18,8 +18,8 @@ use rundler_provider::{ AggregatorOut, EntryPoint, Provider, SignatureAggregator, SimulationProvider, }; use rundler_types::{ - pool::SimulationViolation, v0_6::UserOperation, EntityInfo, EntityInfos, - UserOperation as UserOperationTrait, ValidTimeRange, + pool::SimulationViolation, EntityInfo, EntityInfos, UserOperation, ValidTimeRange, + ValidationError, }; use crate::{ @@ -32,40 +32,40 @@ use crate::{ /// /// WARNING: This is "unsafe" for a reason. None of the ERC-7562 checks are /// performed. -pub struct UnsafeSimulator { +pub struct UnsafeSimulator { provider: Arc

, entry_point: E, sim_settings: Settings, + _uo_type: std::marker::PhantomData, } -impl UnsafeSimulator { +impl UnsafeSimulator { /// Creates a new unsafe simulator pub fn new(provider: Arc

, entry_point: E, sim_settings: Settings) -> Self { Self { provider, entry_point, sim_settings, + _uo_type: std::marker::PhantomData, } } } #[async_trait::async_trait] -impl Simulator for UnsafeSimulator +impl Simulator for UnsafeSimulator where + UO: UserOperation, P: Provider, - E: EntryPoint - + SimulationProvider - + SignatureAggregator - + Clone, + E: EntryPoint + SimulationProvider + SignatureAggregator + Clone, { - type UO = UserOperation; + type UO = UO; // Run an unsafe simulation // // The only validation checks that are performed are signature checks async fn simulate_validation( &self, - op: UserOperation, + op: UO, block_hash: Option, _expected_code_hash: Option, ) -> Result { @@ -92,14 +92,35 @@ where self.sim_settings.max_verification_gas, Some(block_hash), ) - .await - .map_err(anyhow::Error::from)?; + .await; + + let validation_result = match validation_result { + Ok(res) => res, + Err(err) => match err { + ValidationError::Revert(revert) => { + return Err(SimulationError { + violation_error: vec![SimulationViolation::ValidationRevert(revert)].into(), + entity_infos: None, + }) + } + ValidationError::Other(err) => { + return Err(SimulationError { + violation_error: ViolationError::Other(err), + entity_infos: None, + }) + } + }, + }; + + let valid_until = if validation_result.return_info.valid_until == 0.into() { + u64::MAX.into() + } else { + validation_result.return_info.valid_until + }; let pre_op_gas = validation_result.return_info.pre_op_gas; - let valid_time_range = ValidTimeRange::new( - validation_result.return_info.valid_after, - validation_result.return_info.valid_until, - ); + let valid_time_range = + ValidTimeRange::new(validation_result.return_info.valid_after, valid_until); let requires_post_op = !validation_result.return_info.paymaster_context.is_empty(); let entity_infos = EntityInfos { diff --git a/crates/sim/src/simulation/v0_6/mod.rs b/crates/sim/src/simulation/v0_6/mod.rs index cf71b0816..40dbc3a14 100644 --- a/crates/sim/src/simulation/v0_6/mod.rs +++ b/crates/sim/src/simulation/v0_6/mod.rs @@ -19,8 +19,5 @@ pub use simulator::Simulator; mod tracer; pub use tracer::{SimulateValidationTracer, SimulateValidationTracerImpl}; -mod unsafe_sim; -pub use unsafe_sim::UnsafeSimulator; - /// Required buffer for verification gas limit when targeting the 0.6 entrypoint contract pub(crate) const REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER: U256 = U256([2000, 0, 0, 0]); diff --git a/crates/task/src/grpc/protos.rs b/crates/task/src/grpc/protos.rs index 97b8a059b..8d4013786 100644 --- a/crates/task/src/grpc/protos.rs +++ b/crates/task/src/grpc/protos.rs @@ -13,7 +13,7 @@ //! Protobuf utilities -use ethers::types::{Address, H256, U256}; +use ethers::types::{Address, Bytes, H256, U128, U256}; /// Error type for conversions from protobuf types to Ethers/local types. #[derive(Debug, thiserror::Error)] @@ -29,13 +29,6 @@ pub enum ConversionError { InvalidEnumValue(i32), } -/// Convert an Ethers U256 to little endian bytes for packing into a proto struct. -pub fn to_le_bytes(n: U256) -> Vec { - let mut vec = vec![0_u8; 32]; - n.to_little_endian(&mut vec); - vec -} - /// Convert proto bytes into a type that implements `FromProtoBytes`. /// /// Returns a `ConversionError` if the bytes could not be converted. @@ -79,6 +72,14 @@ impl FromFixedLengthProtoBytes for Address { } } +impl FromFixedLengthProtoBytes for U128 { + const LEN: usize = 16; + + fn from_fixed_length_bytes(bytes: &[u8]) -> Self { + Self::from_little_endian(bytes) + } +} + impl FromFixedLengthProtoBytes for U256 { const LEN: usize = 32; @@ -94,3 +95,43 @@ impl FromFixedLengthProtoBytes for H256 { Self::from_slice(bytes) } } + +/// Trait for a type that can be converted to protobuf bytes. +pub trait ToProtoBytes { + /// Convert to protobuf bytes. + fn to_proto_bytes(&self) -> Vec; +} + +impl ToProtoBytes for Address { + fn to_proto_bytes(&self) -> Vec { + self.as_bytes().to_vec() + } +} + +impl ToProtoBytes for U128 { + fn to_proto_bytes(&self) -> Vec { + let mut vec = vec![0_u8; 16]; + self.to_little_endian(&mut vec); + vec + } +} + +impl ToProtoBytes for U256 { + fn to_proto_bytes(&self) -> Vec { + let mut vec = vec![0_u8; 32]; + self.to_little_endian(&mut vec); + vec + } +} + +impl ToProtoBytes for H256 { + fn to_proto_bytes(&self) -> Vec { + self.as_bytes().to_vec() + } +} + +impl ToProtoBytes for Bytes { + fn to_proto_bytes(&self) -> Vec { + self.to_vec() + } +} diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index a53917109..7a41e1ac3 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -15,7 +15,7 @@ chrono = "0.4.24" constcat = "0.4.1" ethers.workspace = true futures-util.workspace = true -parse-display = "0.9.0" +parse-display.workspace = true rand.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/types/src/chain.rs b/crates/types/src/chain.rs index 84cb3696b..bb762a594 100644 --- a/crates/types/src/chain.rs +++ b/crates/types/src/chain.rs @@ -19,6 +19,7 @@ use ethers::types::{Address, U256}; use serde::{Deserialize, Serialize}; const ENTRY_POINT_ADDRESS_V6_0: &str = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; +const ENTRY_POINT_ADDRESS_V7_0: &str = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; /// Chain specification for Rundler #[derive(Clone, Debug, Deserialize, Serialize)] @@ -30,8 +31,10 @@ pub struct ChainSpec { pub name: String, /// chain id pub id: u64, - /// entry point address - pub entry_point_address: Address, + /// entry point address for v0_6 + pub entry_point_address_v0_6: Address, + /// entry point address for v0_7 + pub entry_point_address_v0_7: Address, /* * Gas estimation @@ -113,7 +116,8 @@ impl Default for ChainSpec { Self { name: "Unknown".to_string(), id: 0, - entry_point_address: Address::from_str(ENTRY_POINT_ADDRESS_V6_0).unwrap(), + entry_point_address_v0_6: Address::from_str(ENTRY_POINT_ADDRESS_V6_0).unwrap(), + entry_point_address_v0_7: Address::from_str(ENTRY_POINT_ADDRESS_V7_0).unwrap(), eip1559_enabled: true, calldata_pre_verification_gas: false, l1_gas_oracle_contract_type: L1GasOracleContractType::default(), diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index febe53299..30d11dd48 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -49,5 +49,6 @@ pub use storage::StorageSlot; mod validation_results; pub use validation_results::{ - parse_validation_data, AggregatorInfo, StakeInfo, ValidationOutput, ValidationReturnInfo, + parse_validation_data, AggregatorInfo, StakeInfo, ValidationError, ValidationOutput, + ValidationReturnInfo, ValidationRevert, }; diff --git a/crates/types/src/pool/error.rs b/crates/types/src/pool/error.rs index cdd1a24a7..529d1f1c8 100644 --- a/crates/types/src/pool/error.rs +++ b/crates/types/src/pool/error.rs @@ -13,7 +13,9 @@ use ethers::types::{Address, U256}; -use crate::{Entity, EntityType, StorageSlot, ViolationOpCode}; +use crate::{ + validation_results::ValidationRevert, Entity, EntityType, StorageSlot, ViolationOpCode, +}; /// Pool server error type #[derive(Debug, thiserror::Error)] @@ -188,6 +190,9 @@ pub enum SimulationViolation { /// Simulation reverted with an unintended reason #[display("reverted while simulating {0} validation")] UnintendedRevert(EntityType, Option

), + /// Validation revert (only used for unsafe sim) + #[display("validation revert: {0}")] + ValidationRevert(ValidationRevert), /// Simulation did not revert, a revert is always expected #[display("simulateValidation did not revert. Make sure your EntryPoint is valid")] DidNotRevert, diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs index a4a7af70d..c18b5647d 100644 --- a/crates/types/src/user_operation/v0_7.rs +++ b/crates/types/src/user_operation/v0_7.rs @@ -16,6 +16,7 @@ use ethers::{ types::{Address, Bytes, H256, U128, U256}, utils::keccak256, }; +use rand::RngCore; use super::{UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant}; use crate::{ @@ -70,12 +71,16 @@ pub struct UserOperation { /* * Cached fields, not part of the UO */ - // The hash of the user operation - hash: H256, - // The packed user operation - packed: PackedUserOperation, - // The gas cost of the calldata - calldata_gas_cost: U256, + /// Entry point address + pub entry_point: Address, + /// Chain id + pub chain_id: u64, + /// The hash of the user operation + pub hash: H256, + /// The packed user operation + pub packed: PackedUserOperation, + /// The gas cost of the calldata + pub calldata_gas_cost: U256, } impl UserOperationTrait for UserOperation { @@ -290,6 +295,107 @@ pub struct UserOperationOptionalGas { pub paymaster_data: Bytes, } +impl UserOperationOptionalGas { + /// Fill in the optional and dummy fields of the user operation with values + /// that will cause the maximum possible calldata gas cost. + pub fn max_fill(&self, entry_point: Address, chain_id: u64) -> UserOperation { + let max_4 = U128::from(u32::MAX); + let max_8 = U128::from(u64::MAX); + + let mut builder = UserOperationBuilder::new( + entry_point, + chain_id, + UserOperationRequiredFields { + sender: self.sender, + nonce: self.nonce, + call_data: self.call_data.clone(), + signature: vec![255_u8; self.signature.len()].into(), + call_gas_limit: max_4, + verification_gas_limit: max_4, + pre_verification_gas: max_4.into(), + max_priority_fee_per_gas: max_8, + max_fee_per_gas: max_8, + }, + ); + + if self.paymaster.is_some() { + builder = builder.paymaster( + self.paymaster.unwrap(), + max_4, + max_4, + vec![255_u8; self.paymaster_data.len()].into(), + ); + } + if self.factory.is_some() { + builder = builder.factory( + self.factory.unwrap(), + vec![255_u8; self.factory_data.len()].into(), + ); + } + + builder.build() + } + + /// Fill in the optional and dummy fields of the user operation with random values. + /// + /// When estimating pre-verification gas, specifically on networks that use + /// compression algorithms on their data that they post to their data availability + /// layer (like Arbitrum), it is important to make sure that the data that is + /// random such that it compresses to a representative size. + // + /// Note that this will slightly overestimate the calldata gas needed as it uses + /// the worst case scenario for the unknown gas values and paymaster_and_data. + pub fn random_fill(&self, entry_point: Address, chain_id: u64) -> UserOperation { + let mut builder = UserOperationBuilder::new( + entry_point, + chain_id, + UserOperationRequiredFields { + sender: self.sender, + nonce: self.nonce, + call_data: self.call_data.clone(), + signature: Self::random_bytes(self.signature.len()), + call_gas_limit: U128::from_big_endian(&Self::random_bytes(4)), + verification_gas_limit: U128::from_big_endian(&Self::random_bytes(4)), + pre_verification_gas: U256::from_big_endian(&Self::random_bytes(4)), + max_priority_fee_per_gas: U128::from_big_endian(&Self::random_bytes(8)), + max_fee_per_gas: U128::from_big_endian(&Self::random_bytes(8)), + }, + ); + + if self.paymaster.is_some() { + builder = builder.paymaster( + self.paymaster.unwrap(), + U128::from_big_endian(&Self::random_bytes(4)), + U128::from_big_endian(&Self::random_bytes(4)), + Self::random_bytes(self.paymaster_data.len()), + ) + } + if self.factory.is_some() { + builder = builder.factory( + self.factory.unwrap(), + Self::random_bytes(self.factory_data.len()), + ) + } + + builder.build() + } + + fn random_bytes(len: usize) -> Bytes { + let mut bytes = vec![0_u8; len]; + rand::thread_rng().fill_bytes(&mut bytes); + bytes.into() + } +} + +impl From for UserOperationOptionalGas { + fn from(op: super::UserOperationOptionalGas) -> Self { + match op { + super::UserOperationOptionalGas::V0_7(op) => op, + _ => panic!("Expected UserOperationOptionalGasV0_7"), + } + } +} + /// Builder for UserOperation /// /// Used to create a v0.7 while ensuring all required fields and grouped fields are present @@ -308,6 +414,7 @@ pub struct UserOperationBuilder { paymaster_verification_gas_limit: U128, paymaster_post_op_gas_limit: U128, paymaster_data: Bytes, + packed_uo: Option, } /// Required fields for UserOperation v0.7 @@ -345,6 +452,7 @@ impl UserOperationBuilder { paymaster_verification_gas_limit: U128::zero(), paymaster_post_op_gas_limit: U128::zero(), paymaster_data: Bytes::new(), + packed_uo: None, } } @@ -370,6 +478,12 @@ impl UserOperationBuilder { self } + /// Sets the packed user operation, if known beforehand + pub fn packed(mut self, packed: PackedUserOperation) -> Self { + self.packed_uo = Some(packed); + self + } + /// Builds the UserOperation pub fn build(self) -> UserOperation { let uo = UserOperation { @@ -388,12 +502,16 @@ impl UserOperationBuilder { paymaster_post_op_gas_limit: self.paymaster_post_op_gas_limit, paymaster_data: self.paymaster_data, signature: self.required.signature, + entry_point: self.entry_point, + chain_id: self.chain_id, hash: H256::zero(), packed: PackedUserOperation::default(), calldata_gas_cost: U256::zero(), }; - let packed = pack_user_operation(uo.clone()); + let packed = self + .packed_uo + .unwrap_or_else(|| pack_user_operation(uo.clone())); let hash = hash_packed_user_operation(&packed, self.entry_point, self.chain_id); let calldata_gas_cost = super::op_calldata_gas_cost(packed.clone()); @@ -416,21 +534,21 @@ fn pack_user_operation(uo: UserOperation) -> PackedUserOperation { }; let account_gas_limits = concat_128( - uo.verification_gas_limit.low_u128().to_le_bytes(), - uo.call_gas_limit.low_u128().to_le_bytes(), + uo.verification_gas_limit.low_u128().to_be_bytes(), + uo.call_gas_limit.low_u128().to_be_bytes(), ); let gas_fees = concat_128( - uo.max_priority_fee_per_gas.low_u128().to_le_bytes(), - uo.max_fee_per_gas.low_u128().to_le_bytes(), + uo.max_priority_fee_per_gas.low_u128().to_be_bytes(), + uo.max_fee_per_gas.low_u128().to_be_bytes(), ); let paymaster_and_data = if let Some(paymaster) = uo.paymaster { let mut paymaster_and_data = paymaster.as_bytes().to_vec(); paymaster_and_data - .extend_from_slice(&uo.paymaster_verification_gas_limit.low_u128().to_le_bytes()); + .extend_from_slice(&uo.paymaster_verification_gas_limit.low_u128().to_be_bytes()); paymaster_and_data - .extend_from_slice(&uo.paymaster_post_op_gas_limit.low_u128().to_le_bytes()); + .extend_from_slice(&uo.paymaster_post_op_gas_limit.low_u128().to_be_bytes()); paymaster_and_data.extend_from_slice(&uo.paymaster_data); Bytes::from(paymaster_and_data) } else { @@ -450,46 +568,52 @@ fn pack_user_operation(uo: UserOperation) -> PackedUserOperation { } } -fn unpack_user_operation(puo: PackedUserOperation) -> UserOperation { - let mut factory = None; - let mut factory_data = Bytes::new(); - let mut paymaster = None; - let mut paymaster_verification_gas_limit = U128::zero(); - let mut paymaster_post_op_gas_limit = U128::zero(); - let mut paymaster_data = Bytes::new(); +fn unpack_user_operation( + puo: PackedUserOperation, + entry_point: Address, + chain_id: u64, +) -> UserOperation { + let mut builder = UserOperationBuilder::new( + entry_point, + chain_id, + UserOperationRequiredFields { + sender: puo.sender, + nonce: puo.nonce, + call_data: puo.call_data.clone(), + call_gas_limit: U128::from_big_endian(&puo.account_gas_limits[..16]), + verification_gas_limit: U128::from_big_endian(&puo.account_gas_limits[16..]), + pre_verification_gas: puo.pre_verification_gas, + max_priority_fee_per_gas: U128::from_big_endian(&puo.gas_fees[..16]), + max_fee_per_gas: U128::from_big_endian(&puo.gas_fees[16..]), + signature: puo.signature.clone(), + }, + ); + + builder = builder.packed(puo.clone()); if !puo.init_code.is_empty() { - factory = Some(Address::from_slice(&puo.init_code)); - factory_data = Bytes::from_iter(&puo.init_code[20..]); + let factory = Address::from_slice(&puo.init_code); + let factory_data = Bytes::from_iter(&puo.init_code[20..]); + + builder = builder.factory(factory, factory_data); } if !puo.paymaster_and_data.is_empty() { - paymaster = Some(Address::from_slice(&puo.paymaster_and_data)); - paymaster_verification_gas_limit = U128::from_big_endian(&puo.paymaster_and_data[20..36]); - paymaster_post_op_gas_limit = U128::from_big_endian(&puo.paymaster_and_data[36..52]); - paymaster_data = Bytes::from_iter(&puo.paymaster_and_data[52..]); - } - - UserOperation { - sender: puo.sender, - nonce: puo.nonce, - call_data: puo.call_data.clone(), - call_gas_limit: U128::from_big_endian(&puo.account_gas_limits[..16]), - verification_gas_limit: U128::from_big_endian(&puo.account_gas_limits[16..]), - pre_verification_gas: puo.pre_verification_gas, - max_priority_fee_per_gas: U128::from_big_endian(&puo.gas_fees[..16]), - max_fee_per_gas: U128::from_big_endian(&puo.gas_fees[16..]), - signature: puo.signature.clone(), - factory, - factory_data, - paymaster, - paymaster_verification_gas_limit, - paymaster_post_op_gas_limit, - paymaster_data, - calldata_gas_cost: super::op_calldata_gas_cost(puo.clone()), - packed: puo, - hash: H256::zero(), + let paymaster = Address::from_slice(&puo.paymaster_and_data[..20]); + let paymaster_verification_gas_limit = + U128::from_big_endian(&puo.paymaster_and_data[20..36]); + let paymaster_post_op_gas_limit = U128::from_big_endian(&puo.paymaster_and_data[36..52]); + let paymaster_data = Bytes::from_iter(&puo.paymaster_and_data[52..]); + + builder = builder.paymaster( + paymaster, + paymaster_verification_gas_limit, + paymaster_post_op_gas_limit, + paymaster_data, + ); } + + builder.build() } fn hash_packed_user_operation( @@ -536,9 +660,7 @@ fn concat_128(a: [u8; 16], b: [u8; 16]) -> [u8; 32] { impl PackedUserOperation { /// Unpacks the user operation to its offchain representation pub fn unpack(self, entry_point: Address, chain_id: u64) -> UserOperation { - let hash = hash_packed_user_operation(&self, entry_point, chain_id); - let unpacked = unpack_user_operation(self.clone()); - UserOperation { hash, ..unpacked } + unpack_user_operation(self.clone(), entry_point, chain_id) } fn heap_size(&self) -> usize { diff --git a/crates/types/src/validation_results.rs b/crates/types/src/validation_results.rs index 4317b9a4d..1af3cbfd0 100644 --- a/crates/types/src/validation_results.rs +++ b/crates/types/src/validation_results.rs @@ -35,6 +35,31 @@ use crate::{ const SIG_VALIDATION_FAILED: Address = H160([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); +/// Error during validation simulation +#[derive(Clone, Debug, thiserror::Error, Ord, PartialOrd, Eq, PartialEq)] +pub enum ValidationRevert { + /// The entry point reverted + #[error("{0}")] + EntryPoint(String), + /// The operation reverted + #[error("{0} : {1:?}")] + Operation(String, Bytes), + /// Validation everted with an unknown signature + #[error("revert with bytes: {0:?}")] + Unknown(Bytes), +} + +/// Error during validation simulation +#[derive(Debug, thiserror::Error)] +pub enum ValidationError { + /// The validation reverted + #[error(transparent)] + Revert(#[from] ValidationRevert), + /// Other error + #[error(transparent)] + Other(#[from] anyhow::Error), +} + /// Equivalent to the generated `ValidationResult` or /// `ValidationResultWithAggregation` from `EntryPoint`, but with named structs /// instead of tuples and with a helper for deserializing. diff --git a/test/spec-tests/local/.env b/test/spec-tests/local/.env index 2860fc59d..f37ea5f34 100644 --- a/test/spec-tests/local/.env +++ b/test/spec-tests/local/.env @@ -8,3 +8,4 @@ MIN_UNSTAKE_DELAY=2 PRIORITY_FEE_MODE_KIND=base_fee_percent PRIORITY_FEE_MODE_VALUE=0 BUILDER_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +ENTRY_POINT_V0_7_ENABLED=false diff --git a/test/spec-tests/remote/docker-compose.yml b/test/spec-tests/remote/docker-compose.yml index 847d73eb6..a56a97b79 100644 --- a/test/spec-tests/remote/docker-compose.yml +++ b/test/spec-tests/remote/docker-compose.yml @@ -39,6 +39,7 @@ services: - PRIORITY_FEE_MODE_KIND=base_fee_percent - PRIORITY_FEE_MODE_VALUE=0 - POOL_HOST=0.0.0.0 + - ENTRY_POINT_V0_7_ENABLED=false rundler-builder: image: alchemy-platform/rundler:latest @@ -57,6 +58,7 @@ services: - BUILDER_POOL_URL=https://rundler-pool:50051 - BUILDER_HOST=0.0.0.0 - BUILDER_PORT=50051 + - ENTRY_POINT_V0_7_ENABLED=false rundler-rpc: image: alchemy-platform/rundler:latest @@ -75,6 +77,7 @@ services: - RPC_API=eth,debug - RPC_POOL_URL=https://rundler-pool:50051 - RPC_BUILDER_URL=https://rundler-builder:50051 + - ENTRY_POINT_V0_7_ENABLED=false healthcheck: test: curl --fail http://localhost:3000/health || exit 1 interval: 1s